From 7d651e965301f2781b45df857844c9af5ed0f41c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:21:52 +0900 Subject: [PATCH 01/10] feat: add GIO property constants --- dzgui/const/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index d75fc31..0ce8702 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -30,6 +30,7 @@ BETA_REPO = "testing" APP_NAME = "DZGUI" APP_NAME_LOWER = "dzgui" APP_NAME_ABBR = "dzg" +GTK_APP_ID = "io.github.aclist.dzgui" HEX_GREEN = "#32CD32" HEX_RED = "#FF0000" @@ -91,3 +92,8 @@ TMP_TARBALL = "/tmp/dzgui.tar.gz" TMP_EXE = "/tmp/dzgui/dzgui" DAYZ_COMMUNITY_ROOT = "Missions" + +GIO_SETTINGS_INTERFACE = "org.gnome.desktop.interface" +GIO_SETTINGS_PROP = "color-scheme" +GIO_SETTINGS_KEY = "gtk-application-prefer-dark-theme" +GIO_SETTINGS_VAL = "prefer-dark" From 1254044ef8ad2fc3706a5f0392f5cff82459d422 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:22:57 +0900 Subject: [PATCH 02/10] chore: drop Gtk.main_quit call --- dzgui/managers/config.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 576f809..5783b5e 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -19,11 +19,6 @@ from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.strings import dialogs from dzgui.util._json import read_json, write_json -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository.Gtk import main_quit # noqa E402 - if TYPE_CHECKING: from dzgui.config.userprefs import UserPrefs from dzgui.controllers.mc import Controller @@ -206,7 +201,10 @@ class ConfigManager: logger.info("Normal user exit") if window.props.is_maximized: - main_quit() + app = window.get_application() + if app is None: + return + app.quit() return w, h = window.get_size() @@ -218,7 +216,10 @@ class ConfigManager: except Exception as e: logger.critical(e) - main_quit() + app = window.get_application() + if app is None: + return + app.quit() def set_resolution(self, window: "OuterWindow") -> None: if self.prefs.is_game_mode: From 2e2d3484c4ac90116a7419aadc68bcf5cc13d7e4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:24:06 +0900 Subject: [PATCH 03/10] chore: drop unused imports --- dzgui/managers/contextmenu.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 37e6b81..8b362eb 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -1,4 +1,3 @@ -import gi import logging from typing import TYPE_CHECKING @@ -14,10 +13,6 @@ from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402 - - logger = logging.getLogger(APP_NAME) if TYPE_CHECKING: From ea2ca684d636e85b19b34d4f88201a78b85e9288 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:25:55 +0900 Subject: [PATCH 04/10] chore: restructure as Gtk.Application and drop Gtk.main loop --- dzgui/views/base.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 7e0faea..d0b75af 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -2,9 +2,14 @@ import logging import signal import warnings -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, Self -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, STEAM_ICON +from dzgui.const.constants import ( + APP_NAME, + APP_NAME_LOWER, + GTK_APP_ID, + STEAM_ICON, +) from dzgui.const.enum import NotebookPage from dzgui.controllers.emitter import Emitter from dzgui.controllers.mc import Controller @@ -14,6 +19,7 @@ from dzgui.views.components.connect_panel import ConnectPanel from dzgui.views.components.crumbs import Breadcrumbs from dzgui.views.components.right_panel import RightPanel from dzgui.views.components.statusbar import Statusbar +from dzgui.views.mixins.colorscheme import ColorAwareApp from dzgui.views.mixins.scrollable_mixin import ScrollableMixin # TODO: import notebook only and add components there? @@ -34,7 +40,7 @@ import gi gi.require_version("Gtk", "3.0") gi.require_version("GLibUnix", "2.0") -from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore # noqa E402 +from gi.repository import Gtk, GLib, GLibUnix, Gdk # noqa E402 if TYPE_CHECKING: from dzgui.config.userprefs import UserPrefs @@ -46,9 +52,11 @@ logger = logging.getLogger(APP_NAME) warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) -class OuterWindow(Gtk.Window): - def __init__(self) -> None: - super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER) +class OuterWindow(Gtk.ApplicationWindow): + def __init__(self, app: Gtk.Application) -> None: + super().__init__( + application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER + ) self.hb = AppHeaderBar() MainController.register_widget("window", self) @@ -279,13 +287,18 @@ class Grid(Gtk.Grid): self.right_panel.refresh_button.set_visible(state) -class App(Gtk.Application): +class App(ColorAwareApp, Gtk.Application): # type: ignore def __init__(self, prefs: "UserPrefs") -> None: + super().__init__(application_id=GTK_APP_ID) GLib.set_prgname(APP_NAME) MainController.set_prefs(prefs) - self.win = OuterWindow() + self._setup_signals() + self.connect("activate", self._on_activate) + + def _on_activate(self, app: Self) -> None: + self.win = OuterWindow(app) accel = Gtk.AccelGroup() accel.connect( @@ -296,9 +309,6 @@ class App(Gtk.Application): ) self.win.add_accel_group(accel) - self._setup_signals() - Gtk.main() - def _setup_signals(self) -> None: SIGNAL_ADD = "signal_add" SIGNAL_ADD_FULL = "signal_add_full" @@ -330,6 +340,7 @@ class App(Gtk.Application): MainController = Controller() + try: theme = Gtk.IconTheme.get_default() icons = theme.list_icons(None) From e1e034fffd6fbeef1461bb117014693d9f77dea1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:26:41 +0900 Subject: [PATCH 05/10] chore: initialize and run app --- dzgui/app_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dzgui/app_init.py b/dzgui/app_init.py index 8a3999f..643d32c 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -154,4 +154,5 @@ def load_gui(version: str, is_debug: bool) -> None: use_miles=use_miles, ) print(boot.all_ok) - App(prefs) + app = App(prefs) + app.run(None) From fad8d2f43bc7f17ba2b7117c1185441ab859ce5d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:28:42 +0900 Subject: [PATCH 06/10] feat: add ColorAwareApp mixin (resolves #465) --- dzgui/views/dialogs/boot.py | 5 ++- dzgui/views/dialogs/early_alert.py | 3 +- dzgui/views/dialogs/uninstall.py | 3 +- dzgui/views/dialogs/wizard.py | 3 +- dzgui/views/mixins/colorscheme.py | 37 +++++++++++++++++++ tests/test_colorscheme.py | 59 ++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 dzgui/views/mixins/colorscheme.py create mode 100644 tests/test_colorscheme.py diff --git a/dzgui/views/dialogs/boot.py b/dzgui/views/dialogs/boot.py index 8873b7e..34fb8fe 100644 --- a/dzgui/views/dialogs/boot.py +++ b/dzgui/views/dialogs/boot.py @@ -4,6 +4,9 @@ from typing import Any, Literal, Self, TYPE_CHECKING # import time from enum import Enum + +from dzgui.views.mixins.colorscheme import ColorAwareApp + # TODO: import dialog titles from dzgui.api.mods import remove_stale_signatures as remove_stale from dzgui.config.ipdb import get_ipdb @@ -37,7 +40,7 @@ class Success(Enum): # TODO: strings for "Running", "Failed", etc. # TODO: add margins to tree -class BootDialog(Gtk.Dialog): +class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None: super().__init__( title=dialog_header, diff --git a/dzgui/views/dialogs/early_alert.py b/dzgui/views/dialogs/early_alert.py index e55c76b..64042d0 100644 --- a/dzgui/views/dialogs/early_alert.py +++ b/dzgui/views/dialogs/early_alert.py @@ -3,6 +3,7 @@ import sys from typing import Self from dzgui.util.strings import dialog_error, dialog_header +from dzgui.views.mixins.colorscheme import ColorAwareApp import gi @@ -10,7 +11,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 -class AbortDialog(Gtk.MessageDialog): +class AbortDialog(ColorAwareApp, Gtk.MessageDialog): def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None: super().__init__( title=dialog_header, diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py index 9098692..3e7d2e7 100644 --- a/dzgui/views/dialogs/uninstall.py +++ b/dzgui/views/dialogs/uninstall.py @@ -13,6 +13,7 @@ from dzgui.util.css import load_css from dzgui.util.format import format_exception from dzgui.views.components.box import HBox from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel +from dzgui.views.mixins.colorscheme import ColorAwareApp import gi @@ -217,7 +218,7 @@ class UninstallPage(ScrolledWizardPage): pass -class UninstallWizard(Gtk.Application): +class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: super().__init__() config = Path(paths["XDG_CONFIG_HOME"]) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index d0f0aa0..033f7a1 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -29,6 +29,7 @@ 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 @@ -706,7 +707,7 @@ class SteamPathPage(EnumeratedWizardPage): self.err_box.set_visible(False) -class SetupWizard(Gtk.Application): +class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore def __init__(self, is_deck: bool, XDG: "Xdg") -> None: super().__init__() GLib.set_prgname(APP_NAME) diff --git a/dzgui/views/mixins/colorscheme.py b/dzgui/views/mixins/colorscheme.py new file mode 100644 index 0000000..2e0be9c --- /dev/null +++ b/dzgui/views/mixins/colorscheme.py @@ -0,0 +1,37 @@ +from typing import Any + +from dzgui.const.constants import ( + GIO_SETTINGS_KEY, + GIO_SETTINGS_INTERFACE, + GIO_SETTINGS_PROP, + GIO_SETTINGS_VAL, +) + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gio # noqa + + +class ColorAwareApp: + """ + Polls the local theme's color scheme settings. + Intended to be consumed by Gtk.Applications (or ephemeral widgets like EarlyAlertDialogs) + on initialization, prior to being realized. Applying scheme changes too late in the chain + may cause a blinking effect. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.interface_settings = Gio.Settings.new(GIO_SETTINGS_INTERFACE) + self.interface_settings.connect( + "changed::color-scheme", self._on_color_scheme_changed + ) + self.default_settings = Gtk.Settings.get_default() + self._on_color_scheme_changed(self.interface_settings, GIO_SETTINGS_PROP) + + def _on_color_scheme_changed(self, settings: Gio.Settings, prop: str) -> None: + state = self.interface_settings.get_string(prop) == GIO_SETTINGS_VAL + if self.default_settings is None: + return + self.default_settings.set_property(GIO_SETTINGS_KEY, state) diff --git a/tests/test_colorscheme.py b/tests/test_colorscheme.py new file mode 100644 index 0000000..fd02526 --- /dev/null +++ b/tests/test_colorscheme.py @@ -0,0 +1,59 @@ +import pytest +import subprocess + +from dzgui.const.constants import GIO_SETTINGS_KEY +from dzgui.views.mixins.colorscheme import ColorAwareApp + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gio, GLib # noqa + + +class TestWidget(ColorAwareApp, Gtk.Button): + def __init__(self) -> None: + super().__init__() + + self.state: bool + + def check_theme(self) -> None: + self.state = self.get_settings().get_property(GIO_SETTINGS_KEY) + Gtk.main_quit() + + +@pytest.fixture +def widget() -> TestWidget: + return TestWidget() + + +@pytest.mark.integration +@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)]) +def test_light_mode(widget, key: str, expect: bool) -> None: + local_pref = subprocess.run( + ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"], + capture_output=True, + text=True, + ).stdout + subprocess.run( + [ + "gsettings", + "set", + "org.gnome.desktop.interface", + "color-scheme", + key, + ], + ) + + # NOTE: allow changes to propagate + GLib.idle_add(widget.check_theme) + Gtk.main() + subprocess.run( + [ + "gsettings", + "set", + "org.gnome.desktop.interface", + "color-scheme", + local_pref, + ] + ) + assert widget.state is expect From f24b3574b803d06b0acd8024fa269ad5203894c7 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:29:51 +0900 Subject: [PATCH 07/10] chore: clear typehinting errors --- dzgui/views/base.py | 2 +- dzgui/views/dialogs/early_alert.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index d0b75af..e9a0fb9 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -40,7 +40,7 @@ import gi gi.require_version("Gtk", "3.0") gi.require_version("GLibUnix", "2.0") -from gi.repository import Gtk, GLib, GLibUnix, Gdk # noqa E402 +from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore # noqa E402 if TYPE_CHECKING: from dzgui.config.userprefs import UserPrefs diff --git a/dzgui/views/dialogs/early_alert.py b/dzgui/views/dialogs/early_alert.py index 64042d0..b7032a0 100644 --- a/dzgui/views/dialogs/early_alert.py +++ b/dzgui/views/dialogs/early_alert.py @@ -11,7 +11,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 -class AbortDialog(ColorAwareApp, Gtk.MessageDialog): +class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None: super().__init__( title=dialog_header, From b00523661f1713a909fb1f5b838b9b111964e047 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:30:10 +0900 Subject: [PATCH 08/10] chore: update exclusions --- scripts/unused_imports.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/unused_imports.py b/scripts/unused_imports.py index ad6d8c7..83498ad 100644 --- a/scripts/unused_imports.py +++ b/scripts/unused_imports.py @@ -1,12 +1,23 @@ from pathlib import Path + def iterate(search_str: str) -> None: # TODO: exclude files from .gitignore print() print(f"Searching for '{search_str}'") print() files = Path(".").rglob("*") - ignore = [".git", ".mypy", "build/", "test.py"] + ignore = [ + "scripts/", + ".git", + "egg", + "CHANGELOG", + ".mypy", + "build/", + "test.py", + "bugs", + "pyproject.toml", + ] for file in files: if file.is_dir(): continue From 3f587f64a0a18467a9f37ed096e8fc71bac4e066 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:33:33 +0900 Subject: [PATCH 09/10] chore: drop extraneous newline --- dzgui/views/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index e9a0fb9..9a728bc 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -340,7 +340,6 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore MainController = Controller() - try: theme = Gtk.IconTheme.get_default() icons = theme.list_icons(None) From 4451eee8492ca63a1339cf17ad2e0457bb73e9b1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:22:10 +0900 Subject: [PATCH 10/10] docs: update changelog --- CHANGELOG.md | 1 + dzgui/data/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b5ca0..c2c6eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Update map count (-u flag) - Filter button tooltips - Expand/collapse changelog versions +- Support local light/dark mode changes on system theme ### Changed - Conform to PEP 440 versioning for beta versions diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index d1b5ca0..c2c6eb2 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -51,6 +51,7 @@ - Update map count (-u flag) - Filter button tooltips - Expand/collapse changelog versions +- Support local light/dark mode changes on system theme ### Changed - Conform to PEP 440 versioning for beta versions