diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index d167abe..7253d1c 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -12,7 +12,7 @@ from typing import Any, Generator, Union from warnings import deprecated from dzgui.api.acf import ACF -from dzgui.init.prereqs import has_steam_client +from dzgui.init.prereqs import has_any_client from dzgui.const.constants import ( APPID_DAYZ, APPID_DAYZ_EXP, @@ -61,7 +61,7 @@ class VDFLoadError(Exception): def get_steam_paths() -> list[tuple[Path, str]]: paths = [] - if has_steam_client(): + if has_any_client(): HOME = Path.home() env = os.environ.get("XDG_DATA_HOME") if env is not None: @@ -267,12 +267,12 @@ def find_user_id_32(path: Path) -> int: class SteamAPIKeyError(Exception): - def __init__(self, msg: str | None = None): + def __init__(self, msg: str | None = None) -> None: super().__init__(msg or api_validation_error) class SteamWorkshopEndpointError(Exception): - def __init__(self, msg: str | None = None): + def __init__(self, msg: str | None = None) -> None: super().__init__(msg or workshop_endpoint_error) diff --git a/dzgui/app_init.py b/dzgui/app_init.py index 643d32c..c200189 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -15,7 +15,7 @@ from dzgui.init.migrate import ( migrate_cols_file, ) from dzgui.init.flock import lock_acquire -from dzgui.init.prereqs import has_steam_client +from dzgui.init.prereqs import has_any_client from dzgui.strings import boot from dzgui.util.deck import is_steam_deck, is_game_mode @@ -136,7 +136,7 @@ def load_gui(version: str, is_debug: bool) -> None: # TODO: sudo escalation dialog # count = get_map_count() # TODO: move into module - if has_steam_client() is False: + if has_any_client() is False: EarlyAlertDialog(init.requires_steam) bootwin = BootWindow(XDG, version) diff --git a/dzgui/config/query.py b/dzgui/config/query.py index 602b082..1af14a5 100644 --- a/dzgui/config/query.py +++ b/dzgui/config/query.py @@ -4,8 +4,6 @@ from pathlib import Path from dzgui.util._json import read_json from dzgui.const.enum import Preferences -from dzgui.const.constants import STEAM_CMD, FLATPAK_RUN_CMD, FLATPAK_SANDBOX - class ConfigFileError(Exception): pass @@ -40,23 +38,5 @@ def get_favorites(path: Path) -> list[str]: return list(conf["ip_list"]) -# def is_in_favs(record: str, path: Path) -> bool: -# favs = get_favorites(path) -# if record in favs: -# return True -# return False - - def enum_to_key(enum: Preferences) -> str: return str(enum.dict["key"]) - - -def get_client_index(client: str) -> int: - if client == STEAM_CMD: - return 0 - elif client == FLATPAK_RUN_CMD: - return 1 - elif client == FLATPAK_SANDBOX: - return 2 - else: - return 0 diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index 57c011a..a1fa7a5 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -85,7 +85,8 @@ HERO_PATH = "data/images/_hero.png" CSS_PATH = "data/app.css" VDF_PATH = "steamapps/libraryfolders.vdf" DEFAULT_STEAM_PATH = ".local/share/Steam" -FLATPAK_STEAM_PATH = ".var/app/com.valvesoftware.Steam/data/Steam" +FLATPAK_ROOT_PATH = ".var/app/com.valvesoftware.Steam" +FLATPAK_STEAM_PATH = FLATPAK_ROOT_PATH + "/data/Steam" UBUNTU_STEAM_PATH = ".steam/steam" DEBIAN_STEAM_PATH = ".steam/debian-installation" diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 59d2dd2..c7f3f02 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -541,8 +541,10 @@ class Controller(GObject.GObject): def update_status(self) -> None: self.mediator.preconnect.mark_finished() - def get_steam_client_name(self, cmd: str) -> str: - return self.mediator.options.get_client_name(cmd) + def get_client_name(self) -> str: + # NOTE: only possible values are "Steam" and "Flatpak" + cmd = self.query_config(Preferences.CLIENT) + return str(cmd.split()[0].capitalize()) def present_window(self) -> None: self.mediator.window.present_with_time(Gdk.CURRENT_TIME) diff --git a/dzgui/init/prereqs.py b/dzgui/init/prereqs.py index be92e20..5dce626 100644 --- a/dzgui/init/prereqs.py +++ b/dzgui/init/prereqs.py @@ -7,19 +7,32 @@ from dzgui.const.constants import APP_NAME, FLATPAK_APPID, FLATPAK_CMD, STEAM_CM logger = logging.getLogger(APP_NAME) -def has_steam_client() -> bool: - if shutil.which(STEAM_CMD): - return True - if not shutil.which(FLATPAK_CMD): - return False +def has_flatpak_client() -> bool: try: res = subprocess.run( - [FLATPAK_CMD, "list"], capture_output=True, text=True, check=True + [FLATPAK_CMD, "list", "--columns=application"], + capture_output=True, + text=True, + check=True, ) if FLATPAK_APPID in res.stdout: return True else: return False - except subprocess.CalledProcessError as e: + except Exception as e: logger.critical(e) return False + + +def has_steam_client() -> bool: + if shutil.which(STEAM_CMD): + return True + return False + + +def has_any_client() -> bool: + if shutil.which(STEAM_CMD): + return True + if not shutil.which(FLATPAK_CMD): + return False + return has_flatpak_client() diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index 9be760f..3d76516 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -10,27 +10,22 @@ from dzgui.const.constants import ( FLATPAK_CMD, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, + FLATPAK_ROOT_PATH, + FLATPAK_STEAM_PATH, ) logger = logging.getLogger(APP_NAME) def is_steam_running(cmd: str) -> bool: - is_flatpak_running = is_flatpak_steam_running() if cmd in (FLATPAK_RUN_CMD, FLATPAK_SANDBOX): - return is_flatpak_running + return is_flatpak_steam_running() elif cmd == STEAM_CMD: - if is_flatpak_running: - return False - if has_cmd(STEAM_CMD) is False: - return False - return is_running(STEAM_CMD) + return is_native_steam_running() else: raise TypeError("Not a valid Steam client selection") -# CHORE: test alternate clients -# TODO: has_flatpak_steam def is_flatpak_steam_running() -> bool: if has_cmd(FLATPAK_CMD) is False: return False @@ -43,14 +38,33 @@ def is_flatpak_steam_running() -> bool: return False -def is_running(proc: str) -> bool: - for process in psutil.process_iter(["pid", "name"]): - if process.info["name"] == proc: - return True - return False +def get_steam_procs() -> list[list[str]]: + return [ + p.cmdline() + for p in psutil.process_iter(["pid", "name"]) + if STEAM_CMD in p.info["name"] + ] + + +def is_native_steam_running() -> bool: + if has_cmd(STEAM_CMD) is False: + return False + substrings = [FLATPAK_ROOT_PATH, "steam-runtime-launcher-service"] + steam_procs = get_steam_procs() + native_steam_procs = [ + proc + for proc in steam_procs + if not any(sub in item for item in proc for sub in substrings) + ] + return bool(native_steam_procs) def has_cmd(cmd: str) -> bool: if shutil.which(cmd) is not None: return True return False + + +def path_to_steam_cmd(path: str) -> str: + cmd = FLATPAK_RUN_CMD if FLATPAK_STEAM_PATH in path else STEAM_CMD + return cmd diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 5783b5e..7490b15 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -6,9 +6,6 @@ from typing import Any, TYPE_CHECKING from dzgui.const.constants import ( APP_NAME, - FLATPAK_RUN_CMD, - FLATPAK_SANDBOX, - STEAM_CMD, WINDOW_DEFAULT_X, WINDOW_DEFAULT_Y, ) @@ -129,15 +126,6 @@ class ConfigManager: def enum_to_key(self, enum: Preferences) -> str: return str(enum.dict["key"]) - def get_client_index(self, client: str) -> int: - if client == STEAM_CMD: - return 0 - if client == FLATPAK_RUN_CMD: - return 1 - if client == FLATPAK_SANDBOX: - return 2 - return 0 - def toggle_config(self, key: Preferences) -> None: try: real_key = self.enum_to_key(key) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 4192a03..89c35f0 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -188,7 +188,7 @@ class ConnectionManager: allows_downloads = (True, "") client = self.controller.query_config(Preferences.CLIENT) - client_name = self.controller.get_steam_client_name(client) + client_name = self.controller.get_client_name() running = is_steam_running(client) steam_proc = SteamProcess(client_name, running) self.client = client diff --git a/dzgui/views/components/misc.py b/dzgui/views/components/misc.py index 9186246..cdc3f49 100644 --- a/dzgui/views/components/misc.py +++ b/dzgui/views/components/misc.py @@ -1,49 +1,72 @@ +from typing import Self +from warnings import deprecated + from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD +from dzgui.init.prereqs import has_steam_client, has_flatpak_client from dzgui.strings import options from dzgui.views.components.box import HBox from dzgui.views.components.buttons import Icon - import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa +@deprecated("Client choice is determined at setup time") class ClientCombo(Gtk.ComboBox): def __init__(self) -> None: super().__init__() - self.client_store = Gtk.ListStore(str, str) + has_steam = has_steam_client() + has_flatpak = has_flatpak_client() + self.client_store = Gtk.ListStore(str, str, bool) self.clients = ( ( options.steam_combo, STEAM_CMD, + has_steam, ), ( options.flatpak_combo, FLATPAK_RUN_CMD, + has_flatpak, ), ( options.flatpak_container_combo, FLATPAK_SANDBOX, + has_flatpak, ), ) for client in self.clients: self.client_store.append(client) + self.set_model(self.client_store) renderer_text = Gtk.CellRendererText() self.pack_start(renderer_text, True) self.add_attribute(renderer_text, "text", 0) self.set_active(0) + self.set_cell_data_func(self.get_cells()[0], self.data_func) + + def data_func( + self, + combo: Self, + cell: Gtk.CellRendererText, + model: Gtk.ListStore, + _iter: Gtk.TreeIter, + ) -> None: + sens = model[_iter][2] + cell.set_property("sensitive", sens) + def get_client_human_readable(self, cmd: str) -> str: for client in self.clients: - human, command = client + human, command, sens = client if command == cmd: return human raise TypeError("Not a valid Steam client selection") + class ErrorPopover(Gtk.Popover): def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None: super().__init__(relative_to=relative_to, position=position) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index b755b4a..3aa6aa3 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -17,6 +17,7 @@ from dzgui.const.constants import ( from dzgui.const.boilerplate import config_boilerplate from dzgui.const.endpoints import STEAM_API_SETUP from dzgui.config import freedesktop +from dzgui.init.proc import path_to_steam_cmd from dzgui.init.migrate import migrate_legacy_conf from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings import wizard @@ -25,7 +26,6 @@ from dzgui.util.open_links import open_link_by_url from dzgui.util.css import add_class, load_css from dzgui.views.components.buttons import WebButton from dzgui.views.components.entry import APIEntry -from dzgui.views.components.misc import ClientCombo from dzgui.views.mixins.colorscheme import ColorAwareApp import gi @@ -384,26 +384,17 @@ class PreferencesPage(EnumeratedWizardPage): self.dist_box.add(self.radio_km) self.dist_box.add(self.radio_miles) - client_label = Gtk.Label(label=wizard.label_client, halign=Gtk.Align.START) - self.client_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - self.client_combo = ClientCombo() - self.client_box.add(client_label) - self.client_box.add(self.client_combo) - outer_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER, spacing=10 ) - for el in self.name_box, self.client_box, self.dist_box: + for el in self.name_box, self.dist_box: outer_box.add(el) self.add_start(outer_box) - def get_prefs(self) -> tuple[str, bool, str]: + def get_prefs(self) -> tuple[str, bool]: name = self.name_entry.get_text().strip() use_miles = self.radio_miles.get_active() - model = self.client_combo.get_model() - ind = self.client_combo.get_active() - client = model[ind][1] - return name, use_miles, client + return name, use_miles def _on_entry_changed(self, entry: Gtk.Entry) -> None: text = entry.get_text() @@ -499,15 +490,16 @@ class Assistant(Gtk.Assistant): self.config_values = self.page_migration.migrated_conf return self.get_final_page() case SteamPathPage(): - self.config_values["default_steam_path"] = page.get_path_from_radio() + path = page.get_path_from_radio() + self.config_values["default_steam_path"] = path + self.config_values["client"] = path_to_steam_cmd(path) case SteamValidationPage(): self.config_values["steam_api"] = page.get_api_key() case PreferencesPage(): # NOTE: collects config values before advancing to last page - name, use_miles, client = self.page_prefs.get_prefs() + name, use_miles = self.page_prefs.get_prefs() self.config_values["name"] = name self.config_values["use_miles"] = use_miles - self.config_values["client"] = client self.page_shortcuts.set_steam_path( self.config_values["default_steam_path"] ) diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 6b5da07..91e0b72 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -25,7 +25,7 @@ from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.labels import LeftLabel from dzgui.views.components.buttons import SpinnerButton, WebButton from dzgui.views.components.frame import HeadingFrame -from dzgui.views.components.misc import ClientCombo, ErrorPopover +from dzgui.views.components.misc import ErrorPopover from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.mixins.scrollable_mixin import ScrollableMixin @@ -281,11 +281,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore Preferences.WINDOW, ) - self.client_combo = ClientCombo() - self.client_combo.connect("changed", self._on_client_changed) - - client_hbox = ShortHBox(self.client_combo) - self.distance_toggle = ToggleField( self.controller, strings.options.km, strings.options.mi, Preferences.DIST ) @@ -309,7 +304,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore self.start_tab_combo.connect("changed", self._on_start_tab_changed) pref_rows = [ - [LeftLabel(strings.options.client), client_hbox], [LeftLabel(strings.options.window_size), self.fullscreen_toggle], [LeftLabel(strings.options.distance), self.distance_toggle], [LeftLabel(options.start_tab), start_tab_hbox], @@ -367,17 +361,11 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore self.connect("key-press-event", self._on_keypress) - def get_client_name(self, cmd: str) -> str: - return self.client_combo.get_client_human_readable(cmd) - def block_text_entry(self) -> None: self.steam_box.block_text_entry() - # self.steam_entry.set_position(-1) - # self.steam_entry.set_can_focus(False) def unblock_text_entry(self) -> None: self.steam_box.unblock_text_entry() - # self.steam_entry.set_can_focus(True) def _on_developers_clicked(self, button: Gtk.Button) -> None: self.controller.show_developers_page() @@ -412,13 +400,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore index = enum.value self.controller.update_config(Preferences.START_TAB, index) - def _on_client_changed(self, combo: Gtk.ComboBoxText) -> None: - _iter = combo.get_active_iter() - if _iter is None: - raise ValueError(f"No active iterator set on {combo}") - real_cmd = combo.get_model()[_iter][1] - self.controller.update_config(Preferences.CLIENT, real_cmd) - def populate_settings(self) -> None: prefs = self.controller.get_prefs() # NOTE: re-check in case file was removed by user between runs @@ -464,10 +445,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore self.dayz_version_label.set_text(dayz_version) self.dayz_exp_version_label.set_text(dayz_exp_version) - # TODO: bicolumn list store with no cell renderer on index 1, use raw command names - active_combo = query.get_client_index(config["client"]) - self.client_combo.set_active(active_combo) - start_tab = self.controller.query_config(Preferences.START_TAB) self.start_tab_combo.set_active(start_tab) diff --git a/tests/test_client_name.py b/tests/test_client_name.py new file mode 100644 index 0000000..27f81fc --- /dev/null +++ b/tests/test_client_name.py @@ -0,0 +1,19 @@ +import pytest + +from dzgui.const.constants import FLATPAK_RUN_CMD, STEAM_CMD +from dzgui.controllers.mc import Controller + + +@pytest.mark.parametrize( + "value, expected", [(FLATPAK_RUN_CMD, "Flatpak"), (STEAM_CMD, "Steam")] +) +def test_human_name(monkeypatch, value, expected) -> None: + def mock_query_config(*args) -> str: + return value + + monkeypatch.setattr( + "dzgui.controllers.mc.Controller.query_config", mock_query_config + ) + controller = Controller() + client = controller.get_client_name() + assert client == expected diff --git a/tests/test_process_detection.py b/tests/test_process_detection.py index 2e36063..16909fa 100644 --- a/tests/test_process_detection.py +++ b/tests/test_process_detection.py @@ -3,6 +3,7 @@ from unittest.mock import patch from dzgui.const.constants import ( FLATPAK_APPID, + FLATPAK_ROOT_PATH, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD, @@ -45,16 +46,30 @@ class TestSteamDetection(unittest.TestCase): def test_native_steam(self): with patch.object(proc, "has_cmd", return_value=True): - for running in (True, False): + for output, expected in ( + ( + [ + [FLATPAK_ROOT_PATH], + "/home/user/.local/share/Steam/ubuntu12_64/steamwebhelper", + ], + True, + ), + ([[FLATPAK_ROOT_PATH]], False), + ([[FLATPAK_ROOT_PATH], ["steam-runtime-launcher-service"]], False), + ([["/home/user/.local/share/Steam/ubuntu12_64/steamwebhelper"]], True), + ( + [ + ["/home/user/.local/share/Steam/ubuntu12_64/steamwebhelper"], + "steam-runtime-launcher-service", + ], + True, + ), + ): with ( - self.subTest(running=running), - patch.object(proc, "is_running", return_value=running), + self.subTest(running=output), + patch.object(proc, "get_steam_procs", return_value=output), ): - self.assertEqual(proc.is_steam_running(STEAM_CMD), running) - - def test_native_steam_while_flatpak_is_running(self): - with patch.object(proc, "is_flatpak_steam_running", return_value=True): - self.assertEqual(proc.is_steam_running(STEAM_CMD), False) + self.assertEqual(proc.is_native_steam_running(), expected) def test_invalid_client(self): with self.assertRaises(TypeError):