From 9f3c39fdd67790a6287795808dab5c3db4544312 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:35:56 +0900 Subject: [PATCH 01/18] chore: drop unused print statement --- dzgui/views/pages/options.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 64c5d51..ebce129 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -266,7 +266,6 @@ class FlatpakToggleField(ToggleField): def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None: cmd = FLATPAK_RUN_CMD if self.radio1.get_active() else FLATPAK_SANDBOX - print(context) self.controller.update_config(context, cmd) From e286f02342b22dc5a8349a60599d428a99eb34d3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:36:23 +0900 Subject: [PATCH 02/18] chore: add flatpak toggle test --- tests/test_flatpak_toggle.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_flatpak_toggle.py diff --git a/tests/test_flatpak_toggle.py b/tests/test_flatpak_toggle.py new file mode 100644 index 0000000..eec56e7 --- /dev/null +++ b/tests/test_flatpak_toggle.py @@ -0,0 +1,30 @@ +import pytest + +from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX +from dzgui.const.enum import Preferences +from dzgui.views.pages.options import FlatpakToggleField + + +class MockController: + def __init__(self) -> None: + self.command: str + + def update_config(self, context: Preferences, cmd: str) -> None: + self.command = cmd + + def get_command(self) -> str: + return self.command + + +@pytest.mark.parametrize("index, expect", [(0, FLATPAK_RUN_CMD), (1, FLATPAK_SANDBOX)]) +def test_toggle(index: int, expect: str) -> None: + controller = MockController() + toggle = FlatpakToggleField(controller) + match index: + case 0: + toggle.radio2.set_active(True) + toggle.radio1.set_active(True) + case 1: + toggle.radio1.set_active(True) + toggle.radio2.set_active(True) + assert controller.get_command() == expect From 01b3babdf5ea78e0bf244248856b844de4d131d5 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:23:28 +0900 Subject: [PATCH 03/18] feat: FlatpakOverridePage WIP --- dzgui/api/steam.py | 1 + dzgui/config/xdg.py | 12 ++++- dzgui/init/proc.py | 14 ++++-- dzgui/strings/wizard.py | 2 + dzgui/util/bash.py | 7 +++ dzgui/views/dialogs/wizard.py | 91 +++++++++++++++++++++++++++++------ 6 files changed, 108 insertions(+), 19 deletions(-) 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(): From 519e6c354d08a8c440a087478af7ad7c4a953297 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:42:08 +0900 Subject: [PATCH 04/18] chore: patch VDF changes from main --- dzgui/api/shortcuts.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 594d1a1..144bb44 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -10,8 +10,8 @@ from pathlib import Path from typing import Any from dzgui.api.steam import find_user_id_32 -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH -from dzgui.util.dirs import copy_dzgui_to_xdg_data +from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, GTK_APP_ID, IMAGES_PATH +from dzgui.util.dirs import copy_dzgui_to_xdg_data, make_parents from dzgui.util.strings import unknown logger = logging.getLogger(APP_NAME) @@ -75,6 +75,8 @@ class Shortcuts: shortcuts = vdf.binary_load(f) self.shortcuts = shortcuts logger.debug("Loaded shortcuts file") + except FileNotFoundError: + self.shortcuts = {"shortcuts": {}} except Exception as e: logger.critical(e) raise e @@ -109,6 +111,7 @@ class Shortcuts: for img in ("_hero", "p", "_logo"): filename = str(self.bpid) + img dest = grid_path.joinpath(filename).with_suffix(".png") + make_parents(dest) b = images.joinpath(img).with_suffix(".png").read_bytes() dest.write_bytes(b) @@ -155,7 +158,8 @@ class Shortcuts: NEW_ENTRY["DevkitGameID"] = "" NEW_ENTRY["DevkitOverrideAppID"] = 0 NEW_ENTRY["LastPlayTime"] = 0 - NEW_ENTRY["FlatpakAppID"] = "" + NEW_ENTRY["FlatpakAppID"] = GTK_APP_ID + NEW_ENTRY["sortas"] = "" NEW_ENTRY["tags"] = {} return NEW_ENTRY @@ -172,6 +176,9 @@ class Shortcuts: try: backup = self.shortcuts_path.with_suffix(".vdf.bak") shutil.copy(self.shortcuts_path, backup) + except Exception as e: + logger.critical(e) + try: with open(self.shortcuts_path, "wb") as f: vdf.binary_dump(self.shortcuts, f) except Exception as e: From edb87893f9be6cf00175f60c5a0ed4b78b62afd5 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:46:05 +0900 Subject: [PATCH 05/18] chore: clear typehinting errors --- dzgui/config/xdg.py | 2 +- dzgui/views/dialogs/wizard.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dzgui/config/xdg.py b/dzgui/config/xdg.py index 77919f0..1d7f5b3 100644 --- a/dzgui/config/xdg.py +++ b/dzgui/config/xdg.py @@ -62,7 +62,7 @@ def get_xdg_paths() -> dict: resolved_paths = {} for path in xdg_paths: real_path = os.environ.get(path) - if is_sandboxed(): + if real_path is not None and 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): diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index b1d779b..2873eb9 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -543,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_shortcuts_page() + return self.get_shortcut_page() case SteamPathPage(): path = page.get_path_from_radio() self.config_values["default_steam_path"] = path @@ -568,7 +568,7 @@ class Assistant(Gtk.Assistant): raise AttributeError("Trying to advance a non-canonical page") return index + 1 - def needs_flatpak_override(self) -> None: + def needs_flatpak_override(self) -> bool: if ( self.page_shortcuts.is_steam_checkbox_active() and FLATPAK_ROOT_PATH in self.config_values["default_steam_path"] @@ -701,7 +701,7 @@ 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: + def is_steam_checkbox_active(self) -> bool: return self.steam_checkbox.get_active() def create_shortcuts(self) -> None: From c2993ad289d2543bf13e9859fffd5661e692d864 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:11:19 +0900 Subject: [PATCH 06/18] change: revert prior VDF change, no observable effect on D-Bus --- dzgui/api/shortcuts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 144bb44..2ecb076 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any from dzgui.api.steam import find_user_id_32 -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, GTK_APP_ID, IMAGES_PATH +from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH from dzgui.util.dirs import copy_dzgui_to_xdg_data, make_parents from dzgui.util.strings import unknown @@ -158,7 +158,7 @@ class Shortcuts: NEW_ENTRY["DevkitGameID"] = "" NEW_ENTRY["DevkitOverrideAppID"] = 0 NEW_ENTRY["LastPlayTime"] = 0 - NEW_ENTRY["FlatpakAppID"] = GTK_APP_ID + NEW_ENTRY["FlatpakAppID"] = "" NEW_ENTRY["sortas"] = "" NEW_ENTRY["tags"] = {} From d577ba09707eedc268b2ac90a8f1b7c4dd8cdf8f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:20:10 +0900 Subject: [PATCH 07/18] change: remove Flatpak option if binary is not installed --- dzgui/api/steam.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index ab87025..7efff23 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -13,6 +13,7 @@ from warnings import deprecated from dzgui.api.acf import ACF from dzgui.init.prereqs import has_any_client +from dzgui.init.proc import has_cmd from dzgui.const.constants import ( APPID_DAYZ, APPID_DAYZ_EXP, @@ -22,6 +23,7 @@ from dzgui.const.constants import ( DEBIAN_STEAM_PATH, DEFAULT_STEAM_PATH, DOWNLOADS_PATH, + FLATPAK_CMD, FLATPAK_STEAM_PATH, LIBRARYFOLDERS_PATH, UBUNTU_STEAM_PATH, @@ -82,6 +84,8 @@ def get_steam_paths() -> list[tuple[Path, str]]: } for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH: + if path is FLATPAK_PATH and has_cmd(FLATPAK_CMD) is False: + continue if path.joinpath(VDF_PATH).is_file(): paths.append((path, human[path])) return paths From 682532a867e5262ed97fbbe2c7c1f22afc073407 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:34:04 +0900 Subject: [PATCH 08/18] chore: remove erroneous debug string --- dzgui/init/proc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index e10a049..0c1a2a0 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -28,7 +28,6 @@ def is_steam_running(cmd: str) -> bool: def is_sandboxed() -> bool: - logger.debug("Inside Flatpak sandbox") return bool(os.environ.get("FLATPAK_ID")) From 761988d8c2df913e0dad30072cd500556f3e60a1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:14:30 +0900 Subject: [PATCH 09/18] feat: add override module --- dzgui/app_init.py | 2 +- dzgui/views/dialogs/wizard.py | 43 ++++++++++++++++------------------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/dzgui/app_init.py b/dzgui/app_init.py index 95da728..728a495 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -148,7 +148,7 @@ def load_gui(version: str, is_debug: bool) -> None: migrate_cols_file(XDG.columns) copy_state_files(xdg_paths["XDG_STATE_HOME"]) # TODO: add logging inside wizard - wizard = SetupWizard(_is_steam_deck, XDG) + wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths) if not wizard.is_setup_complete(): return diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 2873eb9..2639093 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -23,11 +23,13 @@ from dzgui.init.migrate import migrate_legacy_conf from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings import wizard from dzgui.util._json import write_json +from dzgui.util.flatpak import apply_overrides 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.mixins.colorscheme import ColorAwareApp +from dzgui.views.trees.tree_simple import SimpleTree import gi @@ -418,39 +420,32 @@ class PreferencesPage(EnumeratedWizardPage): class FlatpakOverridePage(EnumeratedWizardPage): - def __init__(self, xdg: "Xdg") -> None: - # TODO: strings + def __init__(self, xdg_paths: dict[str, str]) -> None: super().__init__( enum=PageNum.FLATPAK, - heading="Flatpak overrides", - description="Enable overrides", + heading=wizard.heading_flatpak, + description=wizard.blurb_flatpak, ) - self.xdg = xdg + self.xdg_paths = xdg_paths 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. + store = Gtk.ListStore(str) + for path in xdg_paths.values(): + store.append((str(path),)) + tree = SimpleTree(store, wizard.tree_column) + tree.set_halign(Gtk.Align.CENTER) - # 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: + for el in tree, self.authorize_button: self.add_start(el) def _on_authorize(self, button: Gtk.Button) -> None: - # TODO: apply flatpak overrides via module - print(self.xdg) + apply_overrides(self.xdg_paths) EMITTER.emit("step_complete") @@ -467,7 +462,7 @@ class CompletionPage(EnumeratedWizardPage): class Assistant(Gtk.Assistant): - def __init__(self, is_deck: bool, XDG: "Xdg"): + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]): super().__init__() if is_deck: # NOTE: deemed to be "safe" dimensions that exclude taskbar size @@ -486,7 +481,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_flatpak = FlatpakOverridePage(xdg_paths) self.page_completion = CompletionPage() self.set_forward_page_func(self._advance_page) @@ -772,10 +767,10 @@ class SteamPathPage(EnumeratedWizardPage): class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore - def __init__(self, is_deck: bool, XDG: "Xdg") -> None: + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None: super().__init__() GLib.set_prgname(APP_NAME) - self.win = Window(is_deck, XDG) + self.win = Window(is_deck, XDG, xdg_paths) Gtk.main() def is_setup_complete(self) -> int: @@ -783,9 +778,9 @@ class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore class Window(Gtk.Window): - def __init__(self, is_deck: bool, XDG: "Xdg") -> None: + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None: super().__init__(title=APP_NAME, icon_name=APP_NAME) - self.assistant = Assistant(is_deck, XDG) + self.assistant = Assistant(is_deck, XDG, xdg_paths) class Emitter(GObject.GObject): From 1440871819fd17ca5f76810d432c75b6fe3f850f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:14:44 +0900 Subject: [PATCH 10/18] chore: update strings --- dzgui/strings/wizard.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index 8a9baf4..9662256 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -82,6 +82,15 @@ checkbox_start_menu = ( ) checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.") +### Flatpak page +heading_flatpak = "Flatpak sandbox overrides" +blurb_flatpak = ( + "You chose to add a shortcut directly to Flatpak Steam. " + "To launch DZGUI from Flatpak's sandbox, you must grant " + "permission to read from and write to the following locations." +) +tree_column = "Paths" + ### Completion page heading_completion = "Setup complete" blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI." From d9d3f747bbe6bc72ff23557757a4de1e4d18b828 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:10:24 +0900 Subject: [PATCH 11/18] feat: add override module --- dzgui/util/flatpak.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 dzgui/util/flatpak.py diff --git a/dzgui/util/flatpak.py b/dzgui/util/flatpak.py new file mode 100644 index 0000000..21cd062 --- /dev/null +++ b/dzgui/util/flatpak.py @@ -0,0 +1,29 @@ +import logging +import subprocess + +from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID + +logger = logging.getLogger(APP_NAME) + + +def apply_overrides(xdg: dict[str, str]) -> None: + try: + # NOTE: D-Bus registration required for sandbox + args = [ + "flatpak", + "--user", + "override" f"--own-name={GTK_APP_ID}", + FLATPAK_APPID, + ] + subprocess.run([*args], capture_output=True) + + for path in xdg.values(): + args = [ + "flatpak", + "--user", + "override" f"--filesystem={path}", + FLATPAK_APPID, + ] + subprocess.run([*args], capture_output=True) + except Exception as e: + logger.critical(e) From 67c19fb90dee8757e76847198d65e62df19297ee Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:11:05 +0900 Subject: [PATCH 12/18] feat: add SimpleTree --- dzgui/views/trees/tree_simple.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 dzgui/views/trees/tree_simple.py diff --git a/dzgui/views/trees/tree_simple.py b/dzgui/views/trees/tree_simple.py new file mode 100644 index 0000000..be96668 --- /dev/null +++ b/dzgui/views/trees/tree_simple.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from dzgui.model.model_factory import FastInsertListStore + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa + + +class SimpleTree(Gtk.ScrolledWindow): + """Simple scrollable 1-column tree""" + + def __init__( + self, model: Union[Gtk.ListStore, "FastInsertListStore"], title: str + ) -> None: + super().__init__( + overlay_scrolling=False, + propagate_natural_width=True, + propagate_natural_height=True, + ) + + self.tree = Gtk.TreeView(model=model, enable_search=False) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(title, renderer, text=0) + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + column.set_resizable(False) + column.set_sort_column_id(0) + self.tree.append_column(column) + + self.add(self.tree) + + def get_tree(self) -> Gtk.TreeView: + return self.tree From 97b5c0e6ee40d6c0fe6a3b2f1eaac65e43b50cbb Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:19:37 +0900 Subject: [PATCH 13/18] fix: split arguments --- dzgui/util/flatpak.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dzgui/util/flatpak.py b/dzgui/util/flatpak.py index 21cd062..a432970 100644 --- a/dzgui/util/flatpak.py +++ b/dzgui/util/flatpak.py @@ -12,7 +12,8 @@ def apply_overrides(xdg: dict[str, str]) -> None: args = [ "flatpak", "--user", - "override" f"--own-name={GTK_APP_ID}", + "override", + f"--own-name={GTK_APP_ID}", FLATPAK_APPID, ] subprocess.run([*args], capture_output=True) @@ -21,7 +22,8 @@ def apply_overrides(xdg: dict[str, str]) -> None: args = [ "flatpak", "--user", - "override" f"--filesystem={path}", + "override", + f"--filesystem={path}", FLATPAK_APPID, ] subprocess.run([*args], capture_output=True) From cdec81071d4fcb55bd7852cafa57651b49a5b2d0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:39:56 +0900 Subject: [PATCH 14/18] feat: show notice on override --- dzgui/strings/wizard.py | 2 ++ dzgui/views/dialogs/wizard.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index 9662256..a495de8 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -90,6 +90,8 @@ blurb_flatpak = ( "permission to read from and write to the following locations." ) tree_column = "Paths" +overrides_applied = "Overrides applied." +button_flatpak = "Grant permissions" ### Completion page heading_completion = "Setup complete" diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 2639093..1c12d40 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -429,7 +429,6 @@ class FlatpakOverridePage(EnumeratedWizardPage): self.xdg_paths = xdg_paths self.page_type = Gtk.AssistantPageType.INTRO - store = Gtk.ListStore(str) for path in xdg_paths.values(): store.append((str(path),)) @@ -437,15 +436,22 @@ class FlatpakOverridePage(EnumeratedWizardPage): tree.set_halign(Gtk.Align.CENTER) self.authorize_button = Gtk.Button( - label="Grant permissions", halign=Gtk.Align.CENTER + label=wizard.button_flatpak, halign=Gtk.Align.CENTER ) self.authorize_button.connect("clicked", self._on_authorize) - for el in tree, self.authorize_button: + self.notice = NotificationFrame(wizard.overrides_applied) + self.connect("map", self._on_map) + + for el in tree, self.authorize_button, self.notice: self.add_start(el) + def _on_map(self, page: "EnumeratedWizardPage") -> None: + self.notice.hide() + def _on_authorize(self, button: Gtk.Button) -> None: apply_overrides(self.xdg_paths) + self.notice.show() EMITTER.emit("step_complete") From a9ae5f59b519ddc5a0a7c6c7420156420f4aaf46 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:21:50 +0900 Subject: [PATCH 15/18] chore: add flatpak tests --- pyproject.toml | 3 ++- tests/test_flatpak_override.py | 43 +++++++++++++++++++++++++++++++++ tests/test_process_detection.py | 3 +++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/test_flatpak_override.py diff --git a/pyproject.toml b/pyproject.toml index 1d52faa..b7a9729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,10 +110,11 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } } markers = [ "apitest: checks remote endpoints", "config: config file keys/values", - "realconfig: depends on actual generated local config file", + "flatpak: depends on local flatpak installation", "mods: tests mod metadata/link creation", "pefile: validate PE files", "post_install: requires a completed installation", + "realconfig: depends on actual generated local config file", "redact: log redaction mechanisms", "slow: long-running tests", "webtest: depends on remote endpoint", diff --git a/tests/test_flatpak_override.py b/tests/test_flatpak_override.py new file mode 100644 index 0000000..d34307d --- /dev/null +++ b/tests/test_flatpak_override.py @@ -0,0 +1,43 @@ +import configparser +import pytest + +from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID +from dzgui.config.xdg import get_xdg_paths +from dzgui.util.flatpak import apply_overrides + +PATHS_KEY = "Context" +PATHS_SUBKEY = "filesystems" +BUS_KEY = "Session Bus Policy" + +pytestmark = pytest.mark.flatpak + + +@pytest.fixture +def xdg_paths() -> dict[str, str]: + return get_xdg_paths() + + +@pytest.fixture +def override(xdg_paths) -> None: + apply_overrides(xdg_paths) + + +@pytest.fixture +def config(xdg_paths) -> configparser.ConfigParser: + conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath( + f"flatpak/overrides/{FLATPAK_APPID}" + ) + config = configparser.ConfigParser() + config.read(conf_path) + return config + + +def test_session_bus(override, config) -> None: + bus = config[BUS_KEY][GTK_APP_ID] + assert bus == "own" + + +def test_paths(override, xdg_paths, config) -> None: + expect = xdg_paths + paths = config[PATHS_KEY][PATHS_SUBKEY].split(";") + assert all(str(path) in paths for path in expect.values()) diff --git a/tests/test_process_detection.py b/tests/test_process_detection.py index 23f0956..8b25c9b 100644 --- a/tests/test_process_detection.py +++ b/tests/test_process_detection.py @@ -1,3 +1,4 @@ +import pytest import unittest from unittest.mock import patch @@ -9,6 +10,8 @@ from dzgui.const.constants import ( ) from dzgui.init import proc +pytestmark = pytest.mark.flatpak + class TestSteamDetection(unittest.TestCase): def test_flatpak_detection_with_multiple_running_apps(self): From a6282e4401a5cac339fa5a04107a679478d14856 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:58:21 +0900 Subject: [PATCH 16/18] chore: clean up test blockage --- tests/test_html_escaping.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_html_escaping.py b/tests/test_html_escaping.py index 471bb1a..904e5b2 100644 --- a/tests/test_html_escaping.py +++ b/tests/test_html_escaping.py @@ -1,3 +1,5 @@ +import pytest + from dzgui.api.servers import Details from dzgui.views.dialogs.servers import ServerDetailsDialog @@ -21,6 +23,7 @@ class MockController: return None +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_html_escaping() -> None: name = "Test server" desc = " Game & mods" @@ -31,5 +34,7 @@ def test_html_escaping() -> None: controller = MockController() dialog = ServerDetailsDialog(controller, details) dialog.destroy() + while Gtk.events_pending(): + Gtk.main_iteration() assert dialog.description.get_text() == desc From f4be9a31e90c8545e8b5facc94a9377cc548742f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:56:06 +0900 Subject: [PATCH 17/18] change: check actual client preference --- dzgui/app_init.py | 15 ++++++++++++--- dzgui/util/strings.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dzgui/app_init.py b/dzgui/app_init.py index 728a495..77d076f 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -7,7 +7,12 @@ from typing import TYPE_CHECKING from warnings import deprecated -from dzgui.const.constants import APP_NAME, STEAM_ICON, STEAM_ICONS_PATH +from dzgui.const.constants import ( + APP_NAME, + FLATPAK_ROOT_PATH, + STEAM_ICON, + STEAM_ICONS_PATH, +) from dzgui.const.enum import Preferences from dzgui.config.query import lookup from dzgui.config.userprefs import UserPrefs @@ -18,7 +23,7 @@ from dzgui.init.migrate import ( migrate_cols_file, ) from dzgui.init.flock import lock_acquire -from dzgui.init.prereqs import has_any_client +from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.strings import boot from dzgui.util.deck import is_steam_deck, is_game_mode @@ -161,7 +166,11 @@ def load_gui(version: str, is_debug: bool) -> None: # TODO: sudo escalation dialog # count = get_map_count() # TODO: move into module - if has_any_client() is False: + if FLATPAK_ROOT_PATH in lookup(XDG.config, Preferences.DEFAULT): + has_client = has_flatpak_client() + else: + has_client = has_steam_client() + if has_client is False: EarlyAlertDialog(init.requires_steam) bootwin = BootWindow(XDG, version) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 30a7fc6..91c41ea 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -297,7 +297,7 @@ class Options: init = Init( is_steam_running="Is Steam running? DZGUI must be run on top of Steam.", is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.", - requires_steam="DZGUI requires that Steam or Flatpak Steam be installed on the system.", + requires_steam="Your chosen client (Steam/Flatpak Steam) must be installed on the system.", ) mod_panel = ModPanelStrings( From 3e6bd95285beaab7bc47ddc3abe854e91f14938b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:47:45 +0900 Subject: [PATCH 18/18] change: require explicit Steam launch command --- dzgui/api/steam.py | 5 ++++- dzgui/strings/wizard.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 7efff23..2580bd0 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -26,8 +26,9 @@ from dzgui.const.constants import ( FLATPAK_CMD, FLATPAK_STEAM_PATH, LIBRARYFOLDERS_PATH, - UBUNTU_STEAM_PATH, REQUEST_TIMEOUT, + STEAM_CMD, + UBUNTU_STEAM_PATH, VDF_PATH, ) from dzgui.const.endpoints import ( @@ -86,6 +87,8 @@ def get_steam_paths() -> list[tuple[Path, str]]: for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH: if path is FLATPAK_PATH and has_cmd(FLATPAK_CMD) is False: continue + if path is not FLATPAK_PATH and has_cmd(STEAM_CMD) is False: + continue if path.joinpath(VDF_PATH).is_file(): paths.append((path, human[path])) return paths diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index a495de8..de83762 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -19,7 +19,7 @@ desc_flatpak_path = ( desc_ubuntu_path = "This is the default Steam path on Ubuntu-based systems." desc_debian_path = "This is the default Steam path on Debian-based systems." no_valid_paths = ( - "No valid Steam paths found on system. Please install Steam to continue." + "No valid Steam path or Steam command found on system. Please install Steam to continue." ) button_scan = "Scan for Steam"