diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 7253d1c..ab87025 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -198,6 +198,7 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int: """Loads to the menu screen with the selected mods; used for AFK/pre-joining""" concat = concat_mods(mods) client_args = concat_bash_args(client) + logger.debug(f"Setting client arguments to: {client_args}") params = [ "-applaunch", str(appid), diff --git a/dzgui/config/xdg.py b/dzgui/config/xdg.py index 47f747a..77919f0 100644 --- a/dzgui/config/xdg.py +++ b/dzgui/config/xdg.py @@ -3,7 +3,13 @@ import os from pathlib import Path from dataclasses import dataclass -from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG +from dzgui.const.constants import ( + APP_NAME_LOWER, + DEBUG_LOG, + FLATPAK_ROOT_PATH, + SYSTEM_LOG, +) +from dzgui.init.proc import is_sandboxed @dataclass @@ -53,10 +59,12 @@ def get_xdg_paths() -> dict: "XDG_DATA_HOME": home / ".local" / "share", "XDG_CACHE_HOME": home / ".cache", } - resolved_paths = {} for path in xdg_paths: real_path = os.environ.get(path) + if is_sandboxed(): + search_string = FLATPAK_ROOT_PATH + "/" + real_path = real_path.replace(search_string, "") if real_path is not None and is_writeable(real_path): new_path = Path(real_path) else: diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index 3d76516..e10a049 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -1,7 +1,8 @@ -import psutil -import subprocess -import shutil import logging +import os +import psutil +import shutil +import subprocess from dzgui.const.constants import ( APP_NAME, @@ -26,7 +27,14 @@ def is_steam_running(cmd: str) -> bool: raise TypeError("Not a valid Steam client selection") +def is_sandboxed() -> bool: + logger.debug("Inside Flatpak sandbox") + return bool(os.environ.get("FLATPAK_ID")) + + def is_flatpak_steam_running() -> bool: + if is_sandboxed(): + return True if has_cmd(FLATPAK_CMD) is False: return False proc = subprocess.check_output( diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index 9f499b0..8a9baf4 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -3,6 +3,8 @@ title_intro = "Welcome" blurb_intro = """ This wizard will help you set up some common config options before launching the application. """ +error_sandbox = "Setup cannot be run from inside Flatpak sandbox.\nPlease launch the application directly." +welcome = "Welcome to {0}!" ### SteamPathPage heading_steam_path = "Steam path" diff --git a/dzgui/util/bash.py b/dzgui/util/bash.py index 9c2a73d..d612b94 100644 --- a/dzgui/util/bash.py +++ b/dzgui/util/bash.py @@ -1,5 +1,12 @@ import shlex +from dzgui.const.constants import STEAM_CMD +from dzgui.init.proc import is_sandboxed + def concat_bash_args(command: str) -> list[str]: + # NOTE: NSGs spawned from sandbox cannot access host application commands, + # so fallback to Steam internal URIs + if is_sandboxed(): + return [STEAM_CMD] return shlex.split(command) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index b3e54f4..b1d779b 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -12,13 +12,13 @@ from dzgui.const.constants import ( APP_NAME, APP_NAME_LOWER, HERO_PATH, - FLATPAK_STEAM_PATH, + FLATPAK_ROOT_PATH, LEGACY_CONFIG_PATH, ) 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.proc import is_sandboxed, 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 @@ -45,7 +45,8 @@ class PageNum(Enum): STEAM_API = 4 USER_PREFS = 5 SHORTCUTS = 6 - FINAL = 7 + FLATPAK = 7 + FINAL = 8 class OptionalPageMixin: @@ -144,7 +145,7 @@ class NotificationFrame(Gtk.Frame): margin_start=50, margin_end=50, ) - wrapped = textwrap.fill(label, width=80) + wrapped = textwrap.fill(label, width=60) self.label = Gtk.Label( label=wrapped, justify=Gtk.Justification.CENTER, @@ -158,7 +159,7 @@ class NotificationFrame(Gtk.Frame): add_class(self, "error-frame") def set_text(self, text: str) -> None: - wrapped = textwrap.fill(text, width=80) + wrapped = textwrap.fill(text, width=60) self.label.set_markup(wrapped) @@ -243,12 +244,23 @@ class IntroductionPage(EnumeratedWizardPage): def __init__(self) -> None: super().__init__( enum=PageNum.INTRO, - heading=f"Welcome to {APP_NAME}!", + heading=wizard.welcome.format(APP_NAME), description=wizard.blurb_intro, ) self.page_type = Gtk.AssistantPageType.INTRO self.set_title(wizard.title_intro) + self.error_box = NotificationFrame(wizard.error_sandbox, error=True) + self.add_start(self.error_box) + + self.connect("map", self._on_map) + + def _on_map(self, page: "EnumeratedWizardPage") -> None: + self.error_box.hide() + if is_sandboxed(): + self.error_box.show() + EMITTER.emit("step_pending") + class Heading(Gtk.Label): def __init__(self, label: str): @@ -405,6 +417,43 @@ class PreferencesPage(EnumeratedWizardPage): EMITTER.emit("step_complete") +class FlatpakOverridePage(EnumeratedWizardPage): + def __init__(self, xdg: "Xdg") -> None: + # TODO: strings + super().__init__( + enum=PageNum.FLATPAK, + heading="Flatpak overrides", + description="Enable overrides", + ) + self.xdg = xdg + self.page_type = Gtk.AssistantPageType.INTRO + + # TODO: labels, blurbs + # You chose to add a DZGUI shortcut directly to Steam. + # Your Steam client selection is Flatpak Steam. + # To launch DZGUI from Flatpak's sandbox, you must + # grant permission for the sandbox to read from and write to the + # following locations. + + # Click below to whitelist these paths and enable Steam to register the DZGUI application. + + # XDG.config, XDG.share, XDG.state, /tmp? + + self.label = Gtk.Label(label="Whitelist") + self.authorize_button = Gtk.Button( + label="Grant permissions", halign=Gtk.Align.CENTER + ) + self.authorize_button.connect("clicked", self._on_authorize) + + for el in self.label, self.authorize_button: + self.add_start(el) + + def _on_authorize(self, button: Gtk.Button) -> None: + # TODO: apply flatpak overrides via module + print(self.xdg) + EMITTER.emit("step_complete") + + class CompletionPage(EnumeratedWizardPage): def __init__(self) -> None: super().__init__( @@ -437,6 +486,7 @@ class Assistant(Gtk.Assistant): self.page_api = SteamValidationPage() self.page_prefs = PreferencesPage() self.page_shortcuts = ShortcutCreationPage(XDG.shortcut) + self.page_flatpak = FlatpakOverridePage(XDG) self.page_completion = CompletionPage() self.set_forward_page_func(self._advance_page) @@ -456,6 +506,7 @@ class Assistant(Gtk.Assistant): self.page_api, self.page_prefs, self.page_shortcuts, + self.page_flatpak, self.page_completion, ): # NOTE: skip config migration page if no legacy config file @@ -476,7 +527,10 @@ class Assistant(Gtk.Assistant): def write_config(self, config: dict[str, Any]) -> None: write_json(config, self.config_path) - def get_final_page(self) -> int: + def get_completion_page(self) -> int: + return self.pages[self.page_completion] + + def get_shortcut_page(self) -> int: return self.pages[self.page_shortcuts] def _advance_page(self, index: int) -> int: @@ -489,7 +543,7 @@ class Assistant(Gtk.Assistant): sp = page.migrated_conf["default_steam_path"] self.page_shortcuts.set_steam_path(sp) self.config_values = self.page_migration.migrated_conf - return self.get_final_page() + return self.get_shortcuts_page() case SteamPathPage(): path = page.get_path_from_radio() self.config_values["default_steam_path"] = path @@ -506,10 +560,22 @@ class Assistant(Gtk.Assistant): ) case ShortcutCreationPage(): page.create_shortcuts() + if not self.needs_flatpak_override(): + return self.get_completion_page() + case FlatpakOverridePage(): + pass case _: raise AttributeError("Trying to advance a non-canonical page") return index + 1 + def needs_flatpak_override(self) -> None: + if ( + self.page_shortcuts.is_steam_checkbox_active() + and FLATPAK_ROOT_PATH in self.config_values["default_steam_path"] + ): + return True + return False + def is_setup_complete(self) -> bool: return self.setup_complete @@ -625,12 +691,6 @@ class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ig self.add_start(self.checks_area) self.show_all() self.connect("map", self._on_map) - self.connect("map", self._toggle_checkbox) - - def _toggle_checkbox(self, page: "EnumeratedWizardPage") -> None: - if FLATPAK_STEAM_PATH in str(self.steam_path): - self.steam_checkbox.set_active(False) - self.steam_checkbox.set_sensitive(False) def _on_start_menu_toggled(self, button: Gtk.CheckButton) -> None: state = button.get_active() @@ -641,6 +701,9 @@ class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ig def set_steam_path(self, path: Path) -> None: self.steam_path = path + def is_steam_checkbox_active(self) -> None: + return self.steam_checkbox.get_active() + def create_shortcuts(self) -> None: # NOTE: best-effort, permissive even on failure (page is already marked as complete) if self.steam_checkbox.get_active():