From f0e51fa758483cfbf25ba15810dc1b3834ec2175 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 16 May 2026 21:31:22 +0900 Subject: [PATCH 01/15] chore: bump version --- dzgui/data/CHANGELOG.md | 12 ++++++++---- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index 0b03c65..30c1729 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -32,20 +32,26 @@ - Refresh servers button - Early load alerts button - Preconnect dialog +- Preconnect warnings/failsafes like filesize + ## Changed - Reduce padding on keys button - Boldface breadcrumbs - Bold labels inside frames -- Sidebar buttos do not steal focus +- Sidebar buttons do not steal focus - Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method - Load new model into view without flushing - Cull servers with abnormal queue values (integer overflow: 2147483647) - Packaging structure +- Suppress log messages from imported modules +- Embed Workshop link in Options menu ## Dropped - Debug mode - Branch switching +- Manual mod install mode (describe rationale) +- Force update mods ## Fixed - Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input @@ -57,12 +63,10 @@ - Load offline mods - Choose to jump into splash screen instead of server - Setup wizard -- Move debug mode to developers only - Local documentation +- Raw debug command in context menu ## Developer-facing - Add pyproject.toml file - Show deprecation warnings - Options -> Dev page -- Raw debug command -- Moved debug log to this mode diff --git a/pyproject.toml b/pyproject.toml index 1bf83b6..5b1fa34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux" authors = [ {name = "aclist"} ] -version = "7.0.0" +version = "7.0.0-beta.1" license = "GPL-3.0-or-later" license-files = ["LICENSE"] readme = "README.md" From 07ea73ef2e64c84543a365f99d90c25ba45b44e3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 03:00:12 +0900 Subject: [PATCH 02/15] feat: setup wizard (WIP) --- CHANGELOG.md | 1 + dzgui/api/steam.py | 40 +- dzgui/config/update.py | 4 + dzgui/config/userprefs.py | 1 - dzgui/const/boilerplate.py | 16 + dzgui/const/constants.py | 8 +- dzgui/init/migrate.py | 12 +- dzgui/init/update.py | 23 +- dzgui/main.py | 35 +- dzgui/managers/threading.py | 6 +- dzgui/strings/errors.py | 2 + dzgui/strings/wizard.py | 59 +++ dzgui/util/clip.py | 6 +- dzgui/views/components/buttons.py | 2 +- dzgui/views/components/entry.py | 99 ++++- dzgui/views/dialogs/wizard.py | 584 ++++++++++++++++++++++++++++++ dzgui/views/pages/options.py | 2 +- 17 files changed, 839 insertions(+), 61 deletions(-) create mode 100644 dzgui/const/boilerplate.py create mode 100644 dzgui/strings/wizard.py create mode 100644 dzgui/views/dialogs/wizard.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c1729..8e1898e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Added +- Setup wizard - Changelog text wrapping and formatting - Changelog ships with source - Documentation ships with source diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 4ede7cb..5bb1736 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -1,19 +1,57 @@ import json import logging +import os import requests import subprocess from shlex import shlex from pathlib import Path -from dzgui.const.constants import APP_NAME +from dzgui.init.prereqs import has_steam_client +from dzgui.const.constants import ( + APP_NAME, + DEBIAN_STEAM_PATH, + DEFAULT_STEAM_PATH, + FLATPAK_STEAM_PATH, + UBUNTU_STEAM_PATH, + VDF_PATH, +) from dzgui.const.endpoints import STEAM_PUBLISHED_FILES +from dzgui.strings import wizard from dzgui.util.bash import concat_bash_args logger = logging.getLogger(APP_NAME) +def get_steam_paths() -> list[tuple[Path, str]]: + paths = [] + if has_steam_client(): + HOME = Path.home() + env = os.environ.get("XDG_DATA_HOME") + if env is not None: + XDG_DATA_HOME = env + else: + XDG_DATA_HOME = DEFAULT_STEAM_PATH + + DEFAULT_PATH = HOME.joinpath(XDG_DATA_HOME) + FLATPAK_PATH = HOME.joinpath(FLATPAK_STEAM_PATH) + UBUNTU_PATH = HOME.joinpath(UBUNTU_STEAM_PATH) + DEBIAN_PATH = HOME.joinpath(DEBIAN_STEAM_PATH) + + human = { + DEFAULT_PATH: wizard.desc_default_path, + FLATPAK_PATH: wizard.desc_flatpak_path, + UBUNTU_PATH: wizard.desc_ubuntu_path, + DEBIAN_PATH: wizard.desc_debian_path, + } + + for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH: + if path.joinpath(VDF_PATH).is_file(): + paths.append((path, human[path])) + return paths + + def concat_mods(mods: list[str]) -> str: from dzgui.util.symlink import _hash diff --git a/dzgui/config/update.py b/dzgui/config/update.py index fbb05d9..b5a54ea 100644 --- a/dzgui/config/update.py +++ b/dzgui/config/update.py @@ -4,6 +4,9 @@ from dzgui.const.enum import Preferences from dzgui.config.query import get_config, enum_to_key from dzgui.util._json import write_json +# TODO: drop deprecated + + # TEST: parametrize writing config values and checking config output in test def toggle_config(path: Path, key: Preferences) -> None: real_key = enum_to_key(key) @@ -15,6 +18,7 @@ def toggle_config(path: Path, key: Preferences) -> None: except Exception as e: raise e + def write_config(path: Path, key: Preferences, value: str) -> None: real_key = enum_to_key(key) try: diff --git a/dzgui/config/userprefs.py b/dzgui/config/userprefs.py index a06094c..f99245e 100644 --- a/dzgui/config/userprefs.py +++ b/dzgui/config/userprefs.py @@ -14,6 +14,5 @@ class UserPrefs: is_debug: bool coords: Union["Coords", None] version: str - allow_updates: bool paths: "Xdg" use_miles: bool diff --git a/dzgui/const/boilerplate.py b/dzgui/const/boilerplate.py new file mode 100644 index 0000000..03ac14e --- /dev/null +++ b/dzgui/const/boilerplate.py @@ -0,0 +1,16 @@ +""" +Generic defaults used when initializing a config file from scratch +""" +config_boilerplate = { + "bm_api": "", + "fav_server": "", + "fav_label": "", + "name": "", + "fullscreen": False, + "steam_api": "", + "default_steam_path": "", + "client": "", + "ip_list": [], + "use_miles": False, + "start_tab": 0, +} diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index f536ec4..8304fd4 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -64,8 +64,12 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv" DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG" SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG" CHANGELOG_PATH = "data/CHANGELOG.md" +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" +UBUNTU_STEAM_PATH = ".steam/steam" +DEBIAN_STEAM_PATH = ".steam/debian-installation" LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG") -# TODO: drop -FOREGROUND_CMDS = ("wmctrl", "xdotool") diff --git a/dzgui/init/migrate.py b/dzgui/init/migrate.py index 554e793..140289c 100644 --- a/dzgui/init/migrate.py +++ b/dzgui/init/migrate.py @@ -1,5 +1,4 @@ import shutil -import sys from pathlib import Path from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH @@ -7,16 +6,11 @@ from dzgui.config.convert import rc2json from dzgui.util._json import read_json, write_json -# TODO: move to setup wizard def migrate_legacy_conf(config: Path) -> None: old_conf = Path.home() / LEGACY_CONFIG_PATH - if old_conf.is_file(): - j = rc2json(old_conf) - config.parent.mkdir(parents=True, exist_ok=True) - config.write_text(j) - else: - print("Unimplemented. You must have a working dztuirc.") - sys.exit(1) + j = rc2json(old_conf) + config.parent.mkdir(parents=True, exist_ok=True) + config.write_text(j) def has_new_config(config: Path) -> bool: diff --git a/dzgui/init/update.py b/dzgui/init/update.py index eb6d7db..eac0bee 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -20,6 +20,7 @@ def get_latest_release() -> str | None: try: res = requests.get(url, timeout=REQUEST_TIMEOUT) if res.status_code == 200: + print(res.json()) tag = res.json()["tag_name"] break except Exception as e: @@ -28,31 +29,13 @@ def get_latest_release() -> str | None: return tag -def allow_updates(allow: bool) -> bool: - if allow is False: - return False - if allow is True: - return is_prefix_writeable() - - -def check_updates(version: str) -> None: +def check_updates(version: str) -> str | None: try: latest = get_latest_release() - prefix = sys.prefix if latest is None: return if Version(version) >= Version(latest): return - - # TODO: test update logic - print("UNIMPLEMENTED: fetches in-app updates") - return - - with resources.path(APP_NAME_LOWER, "scripts/update.sh") as path: - proc = subprocess.Popen(["/usr/bin/env", "bash", path, latest, prefix]) - if proc != 0: - # TODO: pop a dialog - pass - sys.exit(proc) + return latest except Exception: return diff --git a/dzgui/main.py b/dzgui/main.py index 516d5d5..a1b557c 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -13,7 +13,6 @@ from dzgui.config.ipdb import get_ipdb from dzgui.config.query import lookup from dzgui.config.userprefs import UserPrefs from dzgui.config.xdg import get_xdg_paths, parse_filepaths -from dzgui.const.update import ALLOW_UPDATES from dzgui.init.coords import get_local_coords from dzgui.init.dayz import is_dayz_installed from dzgui.init.flock import lock_acquire @@ -25,7 +24,7 @@ from dzgui.init.migrate import ( ) from dzgui.init.prefix import get_version from dzgui.init.prereqs import has_steam_client -from dzgui.init.update import allow_updates, check_updates +from dzgui.init.update import check_updates from dzgui.strings import boot @@ -36,6 +35,7 @@ from dzgui.util.symlink import rebuild_symlinks from dzgui.util.strings import init, flags from dzgui.views.base import App +from dzgui.views.dialogs.wizard import SetupWizard from dzgui.views.dialogs.early_alert import EarlyAlertDialog if TYPE_CHECKING: @@ -97,33 +97,31 @@ def main() -> None: if XDG.resolution.parent.is_dir() is False: make_parents(XDG.resolution) + # TODO: test if XDG.debug.is_file() is False: make_parents(XDG.debug) - if has_new_config(XDG.config) is False: - # TODO: handle this in assistant - migrate_legacy_conf(XDG.config) - migrate_cols_file(XDG.columns) - # TODO: copy notes file - copy_state_files(xdg_paths["XDG_STATE_HOME"]) - - setup_logger(XDG.debug) - with open(XDG.debug, "w") as f: - f.truncate(0) - _is_steam_deck = is_steam_deck() _is_game_mode = is_game_mode() if _is_steam_deck else False if _is_game_mode: # NOTE: this may no longer be necessary on newer versions of SteamOS del os.environ["GTK_IM_MODULE"] - # TODO: test spamming timeout - allow = allow_updates(ALLOW_UPDATES) - if allow is True: - check_updates(version) + if has_new_config(XDG.config) is False: + # TODO: add logging inside wizard + # TODO: copy notes file, version file, etc. + # migrate_cols_file(XDG.columns) + # copy_state_files(xdg_paths["XDG_STATE_HOME"]) + SetupWizard(version, _is_steam_deck, XDG.config) + return - # TODO: config wizard: check has_steam_client() prior to VDF exploration + setup_logger(XDG.debug) + with open(XDG.debug, "w") as f: + f.truncate(0) + + # TODO: update area in gutter + # new_version = check_updates(version) if _is_steam_deck is False: # TODO: sudo escalation dialog @@ -148,7 +146,6 @@ def main() -> None: is_debug=args.debug, coords=local_coords, version=version, - allow_updates=allow, paths=XDG, use_miles=use_miles, ) diff --git a/dzgui/managers/threading.py b/dzgui/managers/threading.py index 6c6e20f..b27a409 100644 --- a/dzgui/managers/threading.py +++ b/dzgui/managers/threading.py @@ -1,7 +1,7 @@ import inspect import logging import threading -from typing import Any, Literal, TYPE_CHECKING +from typing import Any, Literal, TYPE_CHECKING, Union from functools import wraps from typing import Callable @@ -52,7 +52,7 @@ class StoredFunc: class ThreadingManager: - def __init__(self, controller: "Controller") -> None: + def __init__(self, controller: Union["Controller", None]) -> None: self.controller = controller self.jobs = 1 self.cleanup_func: StoredFunc | None = None @@ -69,7 +69,7 @@ class ThreadingManager: func.call() GLib.idle_add(self._destroy_on_idle) - if show_dialog: + if show_dialog is True and self.controller is not None: self.wait_dialog = WaitDialog( self.controller, dialog_str, jobs=self.jobs, show_cancel=show_cancel ) diff --git a/dzgui/strings/errors.py b/dzgui/strings/errors.py index bc5c5fd..32d8741 100644 --- a/dzgui/strings/errors.py +++ b/dzgui/strings/errors.py @@ -1,3 +1,5 @@ api_validation_error = ( "API key validation error. Key was typed incorrectly or is defunct." ) + +api_popover = "API key validation failed." diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py new file mode 100644 index 0000000..ffc34fa --- /dev/null +++ b/dzgui/strings/wizard.py @@ -0,0 +1,59 @@ +### IntroductionPage +title_intro = "Welcome" +blurb_intro = """ +This wizard is going to help you set up some common config options before launching the application. +""" + +### SteamPathPage +# TODO +error_steam_path = "ERROR TEXT HERE" +heading_steam_path = "Steam path" +blurb_steam_path = """ +DZGUI needs to find the location to your default Steam installation. +This will be used to determine whether (and where) DayZ is installed. +""" +desc_default_path = "This is the default Steam path on most distributions." +desc_flatpak_path = ( + "This is the default Steam path if you are using Flatpak Steam." +) +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." +) +button_scan = "Scan for Steam" + +### ConfigMigrationPage +heading_config = "Import files" +blurb_config = """ +It looks like you have a DZGUI 6 configuration file on the system.\n +Would you like to import this into DZGUI 7, keeping your existing preferences?\n +In both cases, your DZGUI 6 file will persist separately from DZGUI 7. +""" +config_import_button = "Import DZGUI 6 config to DZGUI 7" +config_import_box = ( + "Configuration data imported successfully. Proceed to the next step." +) +config_new_button = "Create new DZGUI 7 config from scratch" +config_new_box = "A new config file will be created. Proceed to the next step." + +### APIValidationPage +api_success = "API key set successfully. Please proceed to the next step." +heading_steam_api = "Steam Web API key" +button_web_api = "Web API setup link" +blurb_steam_api = """ +You will need to set up a Steam Web API key in order to browse the global server list. +\nIf you don't have one already, it can be set up via the page below. +\nPlease refer to the DZGUI documentation for more instructions. +""" +heading_bm_api = "Battlemetrics Web API key" +blurb_bm_api = """A Battlemetrics key is optional, but allows you to add/search for servers\n +by numeric ID on the web. For example, in the URL https://www.battlemetrics.net/servers/dayz/24819107,\n +the ID would be 24819107. +""" + +entry_placeholder = "Enter API key here" + +### Completion page +heading_completion = "Setup complete" +blurb_completion = "Configuration completed successfully. Please exit and restart DZGUI to apply changes." diff --git a/dzgui/util/clip.py b/dzgui/util/clip.py index 58bf68e..971515c 100644 --- a/dzgui/util/clip.py +++ b/dzgui/util/clip.py @@ -1,10 +1,10 @@ import gi gi.require_version("Gtk", "3.0") -from gi.repository.Gdk import SELECTION_CLIPBOARD # noqa E402 -from gi.repository.Gtk import Clipboard # noqa E402 +gi.require_version("Gdk", "3.0") +from gi.repository import Gtk, Gdk # noqa E402 def copy_clipboard(text: str) -> None: - clipboard = Clipboard.get(SELECTION_CLIPBOARD) + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(text, -1) diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index 6f60814..e15ab5b 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -21,7 +21,7 @@ from dzgui.const.constants import ( import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 +from gi.repository import Gtk, Gdk, GLib # noqa E402 if TYPE_CHECKING: from gi.repository import GLib diff --git a/dzgui/views/components/entry.py b/dzgui/views/components/entry.py index 8dcd3f3..8bdc62c 100644 --- a/dzgui/views/components/entry.py +++ b/dzgui/views/components/entry.py @@ -1,8 +1,10 @@ -from typing import Callable, TYPE_CHECKING +from typing import Any, Callable, TYPE_CHECKING from dzgui.api.servers import validate_ip +from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL from dzgui.util.css import add_class, remove_class from dzgui.util.strings import connect_panel, lan_panel +from dzgui.strings.errors import api_popover import gi @@ -140,3 +142,98 @@ class PortEntry(ValidatedEntry): ) self.set_placeholder_text(lan_panel.placeholder) self.set_tooltip_text(lan_panel.entry_tooltip) + + +# TODO: backport to Options page +class APIEntry(Gtk.Box): + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + + self.func: Callable | None = None + + # TODO: strings + self.entry = Gtk.Entry( + width_chars=60, hexpand=True, placeholder_text="Enter API key" + ) + self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL) + self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True) + self.entry.set_visibility(False) + self.entry.connect("changed", self._on_text_changed) + self.entry.connect("icon-release", self._on_icon_release) + self.entry.connect("activate", self._on_field_activated) + + self.submit = Gtk.Button(label="Submit") + self.submit.set_sensitive(False) + self.submit.connect("clicked", self._on_submit) + + self.pop = Gtk.Popover() + self.pop_label = Gtk.Label( + label=api_popover, + margin_start=10, + margin_end=10, + ) + self.pop.add(self.pop_label) + # NOTE: render once to draw text in bubble + self.pop.show_all() + self.pop.set_margin_start(10) + self.pop.set_relative_to(self.entry) + self.pop.popdown() + + for el in self.entry, self.submit: + self.add(el) + + def get_entry(self) -> None: + return self.entry + + def popup(self) -> None: + self.pop.popup() + + def get_submit(self) -> Gtk.Button: + return self.submit + + def _is_valid_text(self, text: str) -> bool: + if text.isspace(): + return False + if len(text) == 0: + return False + return True + + def _on_text_changed(self, entry: Gtk.Entry) -> None: + text = entry.get_text() + if self._is_valid_text(text): + self.submit.set_sensitive(True) + else: + self.submit.set_sensitive(False) + + def _on_icon_release( + self, + widget: Gtk.Entry, + icon_pos: Gtk.EntryIconPosition, + event: Gdk.Event, + ) -> None: + visible = widget.get_visibility() + if visible: + icon, state = VIEW_REVEAL, False + else: + icon, state = VIEW_CONCEAL, True + widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) + widget.set_visibility(state) + + def _on_field_activated(self, entry: Gtk.Entry) -> None: + self.submit.emit("clicked") + + def _on_submit(self, button: Gtk.Button) -> Any: + if self.func is None: + return + text = self.entry.get_text() + res = self.func(text) + return res + + def set_validation_func(self, func: Callable | None) -> None: + self.func = func + + def disable_button(self) -> None: + self.submit.set_sensitive(False) + + def enable_button(self) -> None: + self.submit.set_sensitive(True) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py new file mode 100644 index 0000000..f990baa --- /dev/null +++ b/dzgui/views/dialogs/wizard.py @@ -0,0 +1,584 @@ +import json +import textwrap + +from enum import Enum +from importlib import resources +from pathlib import Path +from typing import Any, Callable, Self + +from dzgui.api.probe import test_steam_api, test_bm_api +from dzgui.api.steam import get_steam_paths +from dzgui.const.constants import ( + APP_NAME, + APP_NAME_LOWER, + HERO_PATH, + LEGACY_CONFIG_PATH, +) +from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP + +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.open_links import open_link_by_url +from dzgui.util.css import add_class +from dzgui.views.components.buttons import WebButton +from dzgui.views.components.entry import APIEntry + +import gi + +gi.require_version("Gtk", "3.0") +gi.require_version("Gdk", "3.0") +from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402 + + +class PageNum(Enum): + INTRO = 1 + HAS_CONFIG = 2 + STEAM_PATH = 3 + STEAM_API = 4 + BM_API = 5 + USER_PREFS = 6 + FINAL = 7 + + +class DescriptionArea(Gtk.Box): + def __init__(self, text: str): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + wrapped = textwrap.fill(text, width=80) + self.description = Gtk.Label(justify=Gtk.Justification.CENTER) + self.description.set_markup(wrapped) + self.add(self.description) + + +class Progress(Gtk.ProgressBar): + def __init__(self) -> None: + super().__init__(show_text=True) + + +class ScrolledWizardPage(Gtk.ScrolledWindow): + def __init__(self, enum: PageNum, heading: str, description: str): + super().__init__() + + self.enum = enum + self.page_type: Gtk.AssistantPageType + self.title = heading + self.heading = Heading(heading) + self.description = DescriptionArea(description) + + hero = resources.files(APP_NAME_LOWER).joinpath(HERO_PATH) + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + filename=str(hero), + width=600, + height=600, + preserve_aspect_ratio=True, + ) + image = Gtk.Image.new_from_pixbuf(pixbuf) + + self.box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + margin_start=100, + margin_end=100, + margin_top=50, + spacing=20, + ) + self.add(self.box) + self.prog = Progress() + self.box.pack_end(self.prog, expand=False, fill=False, padding=0) + self.box.pack_start(image, expand=False, fill=True, padding=0) + self.box.pack_start(self.heading, expand=False, fill=True, padding=0) + self.box.pack_start(self.description, expand=False, fill=True, padding=0) + + self.connect("map", self._on_map) + + def get_progress_bar(self) -> Progress: + return self.prog + + def get_page_type(self) -> Gtk.AssistantPageType: + return self.page_type + + def set_title(self, title: str) -> None: + self.title = title + + def get_title(self) -> str: + return self.title + + def add_start(self, content: Gtk.Widget) -> None: + self.box.add(content) + + def add_end(self, content: Gtk.Widget) -> None: + self.box.pack_end(content, expand=False, fill=False, padding=50) + + def get_box(self) -> Gtk.Box: + return self.box + + def _on_map(self, page: "ScrolledWizardPage") -> None: + pass + + +class NotificationFrame(Gtk.Frame): + def __init__(self, label: str, error: bool = False) -> None: + super().__init__(halign=Gtk.Align.CENTER) + + self.box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + margin_start=50, + margin_end=50, + ) + wrapped = textwrap.fill(label, width=80) + self.label = Gtk.Label( + label=wrapped, + justify=Gtk.Justification.CENTER, + margin_top=50, + margin_bottom=50, + ) + self.box.add(self.label) + self.add(self.box) + + if error: + # TODO: custom css file only for wizard + add_class(self, "error-frame") + + +class APIValidationPage(ScrolledWizardPage): + def __init__( + self, enum: PageNum, heading: str, description: str, link: str, func: Callable + ) -> None: + super().__init__( + enum=enum, + heading=heading, + description=description, + ) + + self.key = "" + self.link = link + self.thread_man = ThreadingManager(None) + self.page_type = Gtk.AssistantPageType.INTRO + + self.validation_func = func + + self.validation_box = APIEntry() + self.validation_box.set_halign(Gtk.Align.CENTER) + self.validation_box.set_validation_func(self._pre_validate) + + self.link_button = WebButton(label=wizard.button_web_api) + self.link_button.set_halign(Gtk.Align.CENTER) + self.link_button.connect("clicked", self._on_link_clicked) + + self.spinner = Gtk.Spinner() + self.success_box = NotificationFrame(wizard.api_success) + + self.add_start(self.link_button) + self.add_start(self.validation_box) + self.add_start(self.spinner) + self.add_start(self.success_box) + + self.connect("map", lambda _: self.success_box.set_visible(False)) + + def get_api_key(self) -> str: + return self.key + + def _on_link_clicked(self, button: Gtk.Button) -> None: + if self.link == "": + return + open_link_by_url(self.link) + + def _pre_validate(self, key: str) -> None: + self.spinner.start() + self.validation_box.disable_button() + self.validation_func(key) + + def _cleanup(self, state: bool, key: str) -> None: + if state: + self.key = key + self.validation_box.disable_button() + self.success_box.set_visible(True) + EMITTER.emit("step_complete") + else: + self.validation_box.popup() + self.validation_box.enable_button() + self.spinner.stop() + + +class BMValidationPage(APIValidationPage): + def __init__(self) -> None: + super().__init__( + enum=PageNum.BM_API, + heading=wizard.heading_bm_api, + description=wizard.blurb_bm_api, + link=BM_API_SETUP, + func=self._validate, + ) + + @call_on_thread("", show_dialog=False) + def _validate(self, key: str) -> None: + is_valid = test_bm_api(key.strip()) + cleanup = StoredFunc(self._cleanup, is_valid, key) + self.thread_man.set_cleanup_func(cleanup) + + +class SteamValidationPage(APIValidationPage): + def __init__(self) -> None: + super().__init__( + enum=PageNum.STEAM_API, + heading=wizard.heading_steam_api, + description=wizard.blurb_steam_api, + link=STEAM_API_SETUP, + func=self._validate, + ) + + @call_on_thread("", show_dialog=False) + def _validate(self, key: str) -> None: + is_valid = test_steam_api(key) + cleanup = StoredFunc(self._cleanup, is_valid, key) + self.thread_man.set_cleanup_func(cleanup) + + +class IntroductionPage(ScrolledWizardPage): + def __init__(self, version: str): + super().__init__( + enum=PageNum.INTRO, + heading=f"Welcome to {APP_NAME} {version}!", + description=wizard.blurb_intro, + ) + self.page_type = Gtk.AssistantPageType.INTRO + self.set_title(wizard.title_intro) + + +class Heading(Gtk.Label): + def __init__(self, label: str): + super().__init__(label=label) + + # add_class(self, "heading") + # font weight + # TODO: set css em size + # TODO: bold text + + +class ChunkyButton(Gtk.Button): + def __init__(self, text: str) -> None: + super().__init__() + self.set_size_request(80, 80) + + wrapped = textwrap.fill(text, width=40) + label = Gtk.Label(label=wrapped, justify=Gtk.Justification.CENTER) + self.add(label) + + +class RadioFrame(Gtk.Frame): + def __init__( + self, parent: Gtk.RadioButton | None, button_path: tuple[Path, str] + ) -> None: + super().__init__() + + self.vbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + margin_top=15, + margin_start=10, + margin_end=10, + margin_bottom=15, + ) + path, pretty = button_path + if parent is None: + self.button = Gtk.RadioButton.new_with_label(None, str(path)) + else: + self.button = Gtk.RadioButton.new_with_label_from_widget(parent, str(path)) + self.vbox.add(self.button) + label = Gtk.Label(halign=Gtk.Align.START) + label.set_markup(pretty) + self.vbox.add(label) + self.add(self.vbox) + + def get_button(self) -> Gtk.RadioButton: + return self.button + + +class ConfigMigrationPage(ScrolledWizardPage): + def __init__(self, config: Path) -> None: + super().__init__( + enum=PageNum.HAS_CONFIG, + heading=wizard.heading_config, + description=wizard.blurb_config, + ) + + self.migrated = False + self.config = config + self.page_type = Gtk.AssistantPageType.INTRO + + self.import_button = ChunkyButton(wizard.config_import_button) + self.new_button = ChunkyButton(wizard.config_new_button) + + self.grid = Gtk.Grid(column_spacing=30, halign=Gtk.Align.CENTER) + self.grid.set_column_homogeneous(True) + self.grid.attach(self.import_button, 0, 0, 1, 1) + self.grid.attach(self.new_button, 1, 0, 1, 1) + + self.add_start(self.grid) + + self.success_box = NotificationFrame(wizard.config_import_box) + self.from_scratch_box = NotificationFrame(wizard.config_new_box) + self.add_start(self.success_box) + self.add_start(self.from_scratch_box) + + self.connect("map", self._hide_boxes) + self.import_button.connect("clicked", self._on_import_clicked) + self.new_button.connect("clicked", self._on_new_clicked) + + def _hide_boxes(self, page: Self) -> None: + for box in self.success_box, self.from_scratch_box: + box.set_visible(False) + + def _on_new_clicked(self, button: Gtk.Button) -> None: + self.grid.set_sensitive(False) + self.from_scratch_box.set_visible(True) + EMITTER.emit("step_complete") + EMITTER.emit("config", False) + + def get_migrated(self) -> bool: + return self.migrated + + def _on_import_clicked(self, button: Gtk.Button) -> None: + self.grid.set_sensitive(False) + try: + migrate_legacy_conf(self.config) + self.migrated = True + self.success_box.set_visible(True) + except Exception: + pass + EMITTER.emit("step_complete") + EMITTER.emit("config", True) + + +class CompletionPage(ScrolledWizardPage): + def __init__(self) -> None: + super().__init__( + enum=PageNum.FINAL, + heading=wizard.heading_completion, + description=wizard.blurb_completion, + ) + # TODO: show collapsible config file tree + self.page_type = Gtk.AssistantPageType.SUMMARY + + self.connect("map", lambda _: EMITTER.emit("step_complete")) + + +class Assistant(Gtk.Assistant): + def __init__(self, version: str, is_deck: bool, config: Path): + super().__init__() + if is_deck: + self.fullscreen() + else: + self.set_default_size(1500, 900) + + self.config_path = config + # TODO: read in from boilerplate file + from dzgui.const.boilerplate import config_boilerplate + + self.config_values: dict[str, Any] = config_boilerplate + + self.page1 = IntroductionPage(version) + self.page2 = ConfigMigrationPage(config) + self.page3 = SteamPathPage() + self.page4 = SteamValidationPage() + self.page5 = BMValidationPage() + + # self.page6 = PreferencesPage() + # contains name, miles, and steam client choice + # self.name = Gtk.Entry() + # self.miles = Gtk.RadioButton() + # TODO: use dual column model, recycle into options + # TODO: update client_combo in options page + # self.client = Gtk.ComboBox() + # TODO: write to config if not present + + self.page7 = CompletionPage() + + self.set_forward_page_func(self._advance_page) + + EMITTER.connect("step_complete", self._mark_page_complete) + EMITTER.connect("step_pending", self._mark_page_incomplete) + EMITTER.connect("config", self._set_config_state) + + legacy_path = Path.home().joinpath(LEGACY_CONFIG_PATH) + self.has_legacy_config = legacy_path.is_file() + for page in ( + self.page1, + self.page2, + self.page3, + self.page4, + self.page5, + self.page7, + ): + # NOTE: skip config migration page if no legacy config file + if page == self.page2 and self.has_legacy_config is False: + continue + self._add_page(page, page.get_page_type()) + + self.connect("prepare", self._on_page_prepare) + self.connect("cancel", self.destroy_and_quit) + self.connect("close", self.destroy_and_quit) + self.show_all() + + def write_config(self) -> None: + # NOTE: implies that file was already migrated on page 3 + if self.has_legacy_config: + return + write_json(self.config_values, self.config_path) + + def _advance_page(self, index: int) -> int: + page = self.get_nth_page(index) + match page: + case self.page1: + pass + case self.page2: + if self.page2.get_migrated(): + return self.get_n_pages() - 1 + case self.page3: + self.config_values["default_steam_path"] = page.get_path_from_radio() + case self.page4: + self.config_values["steam_api"] = page.get_api_key() + case self.page5: + self.config_values["bm_api"] = page.get_api_key() + # case self.page6: + # self.write_config() + case _: + raise AttributeError("Trying to advance a non-canonical page") + print(self.config_values) + return index + 1 + + def destroy_and_quit(self, widget: Self) -> None: + self.destroy() + Gtk.main_quit() + + def _mark_page_incomplete(self, emitter: "Emitter") -> None: + page_id = self.get_current_page() + page = self.get_nth_page(page_id) + if page is None: + return + self.set_page_complete(page, False) + + def _mark_page_complete(self, emitter: "Emitter") -> None: + page_id = self.get_current_page() + page = self.get_nth_page(page_id) + if page is None: + return + self.set_page_complete(page, True) + + def _add_page(self, page: ScrolledWizardPage, ptype: Gtk.AssistantPageType) -> None: + self.append_page(page) + self.set_page_type(page, ptype) + self.set_page_title(page, page.get_title()) + self.set_page_complete(page, True) + + def _set_config_state(self, emitter: "Emitter", state: bool) -> None: + self.config = state + + def _on_page_prepare(self: Self, wizard: Self, page: ScrolledWizardPage) -> None: + page_num = self.get_current_page() + 1 + total = self.get_n_pages() + fraction = page_num / total + + bar = page.get_progress_bar() + bar.set_fraction(fraction) + bar.set_text(f"{page_num}/{total}") + + # NOTE: disable forward action + if page != self.page1: + EMITTER.emit("step_pending") + + +class SteamPathPage(ScrolledWizardPage): + def __init__(self) -> None: + super().__init__( + enum=PageNum.USER_PREFS, + heading=wizard.heading_steam_path, + description=wizard.blurb_steam_path, + ) + + self.page_type = Gtk.AssistantPageType.INTRO + + # TODO: add custom CSS class to Gtk.Frame so that only this one is styled + err_box = NotificationFrame(wizard.error_steam_path, error=True) + self.err = err_box + + self.scan_button = Gtk.Button(label=wizard.button_scan, halign=Gtk.Align.CENTER) + self.scan_button.connect("clicked", self._on_scan_clicked) + + self.add_start(self.scan_button) + self.connect("map", self._start_incomplete) + + def _on_scan_clicked(self, button: Gtk.Button) -> None: + self.scan_button.set_sensitive(False) + paths = get_steam_paths() + + button_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + halign=Gtk.Align.CENTER, + spacing=10, + ) + total = len(paths) + err_box = NotificationFrame(wizard.no_valid_paths) + if total == 0: + self.add_start(err_box) + else: + button_box.add(Gtk.Label(label=f"Steam paths found: {total} total.")) + for i, button_path in enumerate(paths): + if i == 0: + frame = RadioFrame(None, button_path) + self.first_button = frame.get_button() + button_box.add(frame) + else: + frame = RadioFrame(self.first_button, button_path) + button_box.add(frame) + self.add_start(button_box) + EMITTER.emit("step_complete") + self.show_all() + + def get_path_from_radio(self) -> str: + active = next(r for r in self.first_button.get_group() if r.get_active()) + return active.get_label() + + def _start_incomplete(self, page: Self) -> None: + self.err.set_visible(False) + + def _test_error_func(self, button: Gtk.CheckButton) -> None: + self.err.set_visible(True) + EMITTER.emit("step_incomplete") + + +class SetupWizard(Gtk.Application): + def __init__(self, version: str, is_deck: bool, config: Path) -> None: + super().__init__() + GLib.set_prgname(APP_NAME) + Window(version, is_deck, config) + Gtk.main() + + +class Window(Gtk.Window): + def __init__(self, version: str, is_deck: bool, config: Path) -> None: + super().__init__(title=APP_NAME, icon_name=APP_NAME) + Assistant(version, is_deck, config) + + +class Emitter(GObject.GObject): + def __init__(self) -> None: + super().__init__() + + @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=()) + def step_complete(self) -> None: + pass + + @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=()) + def step_pending(self) -> None: + pass + + @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,)) + def config(self, state: bool) -> None: + pass + + +EMITTER = Emitter() + +# TODO: Ctrl-q +# TODO: change behavior of global emitter diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 0ae777e..83b03cd 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -481,7 +481,7 @@ class Options(Gtk.Box): self.dayz_version_label.set_text(dayz_version) self.dayz_exp_version_label.set_text(dayz_exp_version) - # TODO: not happy with this + # 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) From a77697eec0c0b6862a1d55adb4bbf14df9f143a9 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 03:02:08 +0900 Subject: [PATCH 03/15] chore: clear typehinting errors --- dzgui/init/update.py | 6 +----- dzgui/main.py | 10 +++++----- dzgui/views/components/entry.py | 2 +- dzgui/views/dialogs/wizard.py | 1 - 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/dzgui/init/update.py b/dzgui/init/update.py index eac0bee..2744836 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -1,14 +1,10 @@ import logging import requests -import subprocess -import sys -from importlib import resources from packaging.version import Version -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, REQUEST_TIMEOUT +from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES -from dzgui.init.prefix import is_prefix_writeable logger = logging.getLogger(APP_NAME) diff --git a/dzgui/main.py b/dzgui/main.py index a1b557c..6bbcf41 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -18,17 +18,17 @@ from dzgui.init.dayz import is_dayz_installed from dzgui.init.flock import lock_acquire from dzgui.init.migrate import ( has_new_config, - migrate_cols_file, - migrate_legacy_conf, - copy_state_files, + # migrate_cols_file, + # copy_state_files, ) from dzgui.init.prefix import get_version from dzgui.init.prereqs import has_steam_client -from dzgui.init.update import check_updates + +# from dzgui.init.update import check_updates from dzgui.strings import boot -from dzgui.util.map_count import get_map_count +# from dzgui.util.map_count import get_map_count from dzgui.util.deck import is_steam_deck, is_game_mode from dzgui.util.localize import set_locale from dzgui.util.symlink import rebuild_symlinks diff --git a/dzgui/views/components/entry.py b/dzgui/views/components/entry.py index 8bdc62c..88fd7e2 100644 --- a/dzgui/views/components/entry.py +++ b/dzgui/views/components/entry.py @@ -182,7 +182,7 @@ class APIEntry(Gtk.Box): for el in self.entry, self.submit: self.add(el) - def get_entry(self) -> None: + def get_entry(self) -> Gtk.Entry: return self.entry def popup(self) -> None: diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index f990baa..f49fbe6 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -1,4 +1,3 @@ -import json import textwrap from enum import Enum From ea2d1d7e9f17087e507e93310f5018859eb1928b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 03:56:15 +0900 Subject: [PATCH 04/15] feat: write settings to config file --- dzgui/data/CHANGELOG.md | 1 + dzgui/strings/wizard.py | 16 +++++- dzgui/views/components/misc.py | 35 ++++++++++++ dzgui/views/dialogs/wizard.py | 99 ++++++++++++++++++++++++++-------- dzgui/views/pages/options.py | 28 +--------- 5 files changed, 128 insertions(+), 51 deletions(-) create mode 100644 dzgui/views/components/misc.py diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index 30c1729..8e1898e 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -1,4 +1,5 @@ ## Added +- Setup wizard - Changelog text wrapping and formatting - Changelog ships with source - Documentation ships with source diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index ffc34fa..e2c2e7d 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -38,11 +38,12 @@ config_new_button = "Create new DZGUI 7 config from scratch" config_new_box = "A new config file will be created. Proceed to the next step." ### APIValidationPage +entry_placeholder = "Enter API key here" api_success = "API key set successfully. Please proceed to the next step." heading_steam_api = "Steam Web API key" button_web_api = "Web API setup link" blurb_steam_api = """ -You will need to set up a Steam Web API key in order to browse the global server list. +You must set up a Steam Web API key in order to browse the global server list. \nIf you don't have one already, it can be set up via the page below. \nPlease refer to the DZGUI documentation for more instructions. """ @@ -52,7 +53,18 @@ by numeric ID on the web. For example, in the URL https://www.battlemetrics.net/ the ID would be 24819107. """ -entry_placeholder = "Enter API key here" +### PreferencesPage +heading_prefs = "User preferences" +blurb_prefs = """Here you can set up some basic settings. Additional preferences can be\n +configured via the Options menu once DZGUI launches. +""" +label_player = "Player name" +placeholder_player = "Set an in-game player name" +radio_km = "km (kilometers)" +radio_mi = "mi (miles)" +label_dist = "Distance display" +label_client = "Steam client" + ### Completion page heading_completion = "Setup complete" diff --git a/dzgui/views/components/misc.py b/dzgui/views/components/misc.py new file mode 100644 index 0000000..b959046 --- /dev/null +++ b/dzgui/views/components/misc.py @@ -0,0 +1,35 @@ +from dzgui.strings import options +from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa + + +class ClientCombo(Gtk.ComboBox): + def __init__(self) -> None: + super().__init__() + + self.client_store = Gtk.ListStore(str, str) + clients = ( + ( + options.steam_combo, + STEAM_CMD, + ), + ( + options.flatpak_combo, + FLATPAK_RUN_CMD, + ), + ( + options.flatpak_container_combo, + FLATPAK_SANDBOX, + ), + ) + for client in clients: + self.client_store.append(client) + self.set_model(self.client_store) # Text() + renderer_text = Gtk.CellRendererText() + self.pack_start(renderer_text, True) + self.add_attribute(renderer_text, "text", 0) + self.set_active(0) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index f49fbe6..46fd683 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -13,8 +13,8 @@ from dzgui.const.constants import ( HERO_PATH, LEGACY_CONFIG_PATH, ) +from dzgui.const.boilerplate import config_boilerplate from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP - from dzgui.init.migrate import migrate_legacy_conf from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings import wizard @@ -23,14 +23,16 @@ from dzgui.util.open_links import open_link_by_url from dzgui.util.css import add_class from dzgui.views.components.buttons import WebButton from dzgui.views.components.entry import APIEntry +from dzgui.views.components.misc import ClientCombo import gi gi.require_version("Gtk", "3.0") -gi.require_version("Gdk", "3.0") from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402 +# TODO: currently unused, corresponds to linear index +# TODO: use of pagenum enum and page_type attribute is redundant class PageNum(Enum): INTRO = 1 HAS_CONFIG = 2 @@ -334,7 +336,7 @@ class ConfigMigrationPage(ScrolledWizardPage): EMITTER.emit("step_complete") EMITTER.emit("config", False) - def get_migrated(self) -> bool: + def is_migrated(self) -> bool: return self.migrated def _on_import_clicked(self, button: Gtk.Button) -> None: @@ -349,6 +351,67 @@ class ConfigMigrationPage(ScrolledWizardPage): EMITTER.emit("config", True) +class PreferencesPage(ScrolledWizardPage): + def __init__(self) -> None: + super().__init__( + enum=PageNum.USER_PREFS, + heading=wizard.heading_prefs, + description=wizard.blurb_prefs, + ) + self.page_type = Gtk.AssistantPageType.INTRO + + # TODO: widgets and strings are largely a reimplementation of options page, consolidate + name_label = Gtk.Label(label=wizard.label_player) + self.name_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=10, halign=Gtk.Align.CENTER + ) + self.name_entry = Gtk.Entry(placeholder_text=wizard.placeholder_player) + self.name_entry.connect("changed", self._on_entry_changed) + self.name_box.add(name_label) + self.name_box.add(self.name_entry) + + self.dist_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=10, halign=Gtk.Align.CENTER + ) + self.radio_km = Gtk.RadioButton.new_with_label(None, wizard.radio_km) + self.radio_miles = Gtk.RadioButton.new_with_label_from_widget( + self.radio_km, wizard.radio_mi + ) + dist_label = Gtk.Label(label=wizard.label_dist) + self.dist_box.add(dist_label) + self.dist_box.add(self.radio_km) + self.dist_box.add(self.radio_miles) + + client_label = Gtk.Label(label=wizard.label_client) + self.client_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, spacing=10, halign=Gtk.Align.CENTER + ) + self.client_combo = ClientCombo() + self.client_box.add(client_label) + self.client_box.add(self.client_combo) + + for el in self.name_box, self.dist_box, self.client_box: + self.add_start(el) + + def get_prefs(self) -> None: + 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] + print(name) + print(use_miles) + print(client) + return name, use_miles, client + + def _on_entry_changed(self, entry: Gtk.Entry) -> None: + text = entry.get_text() + if text.isspace(): + EMITTER.emit("step_pending") + return + EMITTER.emit("step_complete") + + class CompletionPage(ScrolledWizardPage): def __init__(self) -> None: super().__init__( @@ -356,7 +419,6 @@ class CompletionPage(ScrolledWizardPage): heading=wizard.heading_completion, description=wizard.blurb_completion, ) - # TODO: show collapsible config file tree self.page_type = Gtk.AssistantPageType.SUMMARY self.connect("map", lambda _: EMITTER.emit("step_complete")) @@ -371,8 +433,6 @@ class Assistant(Gtk.Assistant): self.set_default_size(1500, 900) self.config_path = config - # TODO: read in from boilerplate file - from dzgui.const.boilerplate import config_boilerplate self.config_values: dict[str, Any] = config_boilerplate @@ -381,16 +441,7 @@ class Assistant(Gtk.Assistant): self.page3 = SteamPathPage() self.page4 = SteamValidationPage() self.page5 = BMValidationPage() - - # self.page6 = PreferencesPage() - # contains name, miles, and steam client choice - # self.name = Gtk.Entry() - # self.miles = Gtk.RadioButton() - # TODO: use dual column model, recycle into options - # TODO: update client_combo in options page - # self.client = Gtk.ComboBox() - # TODO: write to config if not present - + self.page6 = PreferencesPage() self.page7 = CompletionPage() self.set_forward_page_func(self._advance_page) @@ -407,6 +458,7 @@ class Assistant(Gtk.Assistant): self.page3, self.page4, self.page5, + self.page6, self.page7, ): # NOTE: skip config migration page if no legacy config file @@ -420,9 +472,6 @@ class Assistant(Gtk.Assistant): self.show_all() def write_config(self) -> None: - # NOTE: implies that file was already migrated on page 3 - if self.has_legacy_config: - return write_json(self.config_values, self.config_path) def _advance_page(self, index: int) -> int: @@ -431,7 +480,7 @@ class Assistant(Gtk.Assistant): case self.page1: pass case self.page2: - if self.page2.get_migrated(): + if self.page2.is_migrated(): return self.get_n_pages() - 1 case self.page3: self.config_values["default_steam_path"] = page.get_path_from_radio() @@ -439,11 +488,15 @@ class Assistant(Gtk.Assistant): self.config_values["steam_api"] = page.get_api_key() case self.page5: self.config_values["bm_api"] = page.get_api_key() - # case self.page6: - # self.write_config() + # NOTE: collects config values before advancing to last page + case self.page6: + name, use_miles, client = self.page6.get_prefs() + self.config_values["name"] = name + self.config_values["use_miles"] = use_miles + self.config_values["client"] = client + self.write_config() case _: raise AttributeError("Trying to advance a non-canonical page") - print(self.config_values) return index + 1 def destroy_and_quit(self, widget: Self) -> None: diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 83b03cd..e38d6ea 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -9,12 +9,9 @@ from dzgui.const.constants import ( APPID_DAYZ_EXP, APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN, - FLATPAK_RUN_CMD, - FLATPAK_SANDBOX, NO_EXPAND, NO_FILL, NO_PADDING, - STEAM_CMD, VIEW_CONCEAL, VIEW_REVEAL, ) @@ -27,6 +24,7 @@ from dzgui.views.components.labels import LeftLabel from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.buttons import WebButton from dzgui.views.components.frame import HeadingFrame +from dzgui.views.components.misc import ClientCombo from dzgui.views.dialogs.generic import ExceptionDialog @@ -101,29 +99,7 @@ class Options(Gtk.Box): Preferences.WINDOW, ) - # TODO: make this an abstract class - client_store = Gtk.ListStore(str, str) - clients = ( - ( - options.steam_combo, - STEAM_CMD, - ), - ( - options.flatpak_combo, - FLATPAK_RUN_CMD, - ), - ( - options.flatpak_container_combo, - FLATPAK_SANDBOX, - ), - ) - for client in clients: - client_store.append(client) - self.client_combo = Gtk.ComboBox.new_with_model(client_store) # Text() - renderer_text = Gtk.CellRendererText() - self.client_combo.pack_start(renderer_text, True) - self.client_combo.add_attribute(renderer_text, "text", 0) - self.client_combo.set_active(0) + self.client_combo = ClientCombo() self.client_combo.connect("changed", self._on_client_changed) client_hbox = ShortHBox(self.client_combo) From 1b4210dcf52fe812f7c56545728ebbc88433dcc4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 03:57:42 +0900 Subject: [PATCH 05/15] chore: clear typehinting errors --- dzgui/views/dialogs/wizard.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 46fd683..907c8a4 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -393,15 +393,12 @@ class PreferencesPage(ScrolledWizardPage): for el in self.name_box, self.dist_box, self.client_box: self.add_start(el) - def get_prefs(self) -> None: + def get_prefs(self) -> tuple[str, bool, str]: 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] - print(name) - print(use_miles) - print(client) return name, use_miles, client def _on_entry_changed(self, entry: Gtk.Entry) -> None: From 5e2d3d1a8886d1bd8ea804f254094847463afe6b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 04:02:56 +0900 Subject: [PATCH 06/15] chore: drop unused files --- dzgui/const/update.py | 15 ---------- dzgui/init/scan.py | 51 ---------------------------------- dzgui/init/update.py | 6 ++-- dzgui/views/components/icon.py | 9 +++--- dzgui/views/dialogs/watcher.py | 18 ------------ 5 files changed, 8 insertions(+), 91 deletions(-) delete mode 100644 dzgui/const/update.py delete mode 100644 dzgui/init/scan.py delete mode 100644 dzgui/views/dialogs/watcher.py diff --git a/dzgui/const/update.py b/dzgui/const/update.py deleted file mode 100644 index 2841083..0000000 --- a/dzgui/const/update.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -This file is intended to be patched by package maintainers -repackaging DZGUI for use in different distributions. - -If DZGUI is going to be installed globally via a package manager -or other means and will reside in an immutable location, or needs to be -explicitly bound to a certain version, set the flag below to False. - -This will disable the following: - -- Version update checks at startup -- Ability to toggle between Stable/Testing branches in the Options menu -""" - -ALLOW_UPDATES = True diff --git a/dzgui/init/scan.py b/dzgui/init/scan.py deleted file mode 100644 index 891f2cc..0000000 --- a/dzgui/init/scan.py +++ /dev/null @@ -1,51 +0,0 @@ -import shutil -import subprocess - -from pathlib import Path - -""" -Find possible locations of default steam path -""" - -def get_steam_dirs() -> list[Path]: - dirs = [] - # pass 1 - if shutil.which("locate") is not None: - # TODO: config or steamapps path? cf. LIBRARYFOLDERS_PATH - proc = subprocess.run( - ["/usr/bin/locate", "Steam/config/libraryfolders.vdf"], - capture_output=True, - text=True - ) - - if proc.returncode == 0: - for d in proc.stdout.splitlines(): - dirs.append(Path(d)) - return dirs - - # pass 2 - for d in Path.home().rglob("Steam/config/libraryfolders.vdf"): - dirs.append(d) - - # pass 3 - # if still nothing, let them select it - # prompt what the dir should look like - if len(dirs) == 0: - print("none found") - return dirs - - -dirs = get_steam_dirs() -print(dirs) - -# TODO: present these in a group of radio buttons -""" -when user toggles radio button, updates text saying -"this is the standard steam location on debian" etc. - -if None or if not satisfied, pop a filepicker and choose -if selection was custom, it has to have a libraryfolders.vdf -keeps next button grayed out until requirements are satisfied - -Cancel Next -""" diff --git a/dzgui/init/update.py b/dzgui/init/update.py index 2744836..e4c08e6 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -29,9 +29,9 @@ def check_updates(version: str) -> str | None: try: latest = get_latest_release() if latest is None: - return + return None if Version(version) >= Version(latest): - return + return None return latest except Exception: - return + return None diff --git a/dzgui/views/components/icon.py b/dzgui/views/components/icon.py index 0ade27e..a8abd11 100644 --- a/dzgui/views/components/icon.py +++ b/dzgui/views/components/icon.py @@ -1,10 +1,11 @@ import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 + class Icon(Gtk.Image): - def __init__(self, name: str, l_margin=0) -> None: - super().__init__(icon_name=name, - icon_size=Gtk.IconSize.BUTTON, - margin_start=l_margin + def __init__(self, name: str, l_margin: int = 0) -> None: + super().__init__( + icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin ) diff --git a/dzgui/views/dialogs/watcher.py b/dzgui/views/dialogs/watcher.py deleted file mode 100644 index 2b56d30..0000000 --- a/dzgui/views/dialogs/watcher.py +++ /dev/null @@ -1,18 +0,0 @@ -import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa - -class Watcher(Gtk.Dialog): - def __init__(): - super().__init__() - pass - - def bump_progress(self): - self.set_secondary_text("FOO") - -#w = Watcher() -#thread = threading() -## compute here -#GLib.idle_add(self._bump_progress) -## raise window: -#Gdk.present_with_time(Gdk.CURRENT_TIME) From 59a84e9e3a50e317dbd624807772434d554ea183 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 04:21:50 +0900 Subject: [PATCH 07/15] chore: additional error handling --- dzgui/data/app.css | 3 +++ dzgui/strings/wizard.py | 2 +- dzgui/views/dialogs/wizard.py | 39 +++++++++++++---------------------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/dzgui/data/app.css b/dzgui/data/app.css index 40eb4e6..9075dec 100644 --- a/dzgui/data/app.css +++ b/dzgui/data/app.css @@ -35,3 +35,6 @@ .masked-tree { background-color: transparent; } +.error-frame border { + border: 2px solid red; +} diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index e2c2e7d..1c44901 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -6,7 +6,6 @@ This wizard is going to help you set up some common config options before launch ### SteamPathPage # TODO -error_steam_path = "ERROR TEXT HERE" heading_steam_path = "Steam path" blurb_steam_path = """ DZGUI needs to find the location to your default Steam installation. @@ -36,6 +35,7 @@ config_import_box = ( ) config_new_button = "Create new DZGUI 7 config from scratch" config_new_box = "A new config file will be created. Proceed to the next step." +config_error_box = "Something went wrong while writing a new config file to the system." ### APIValidationPage entry_placeholder = "Enter API key here" diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 907c8a4..681b717 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -20,7 +20,7 @@ from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManage from dzgui.strings import wizard from dzgui.util._json import write_json from dzgui.util.open_links import open_link_by_url -from dzgui.util.css import add_class +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 @@ -138,7 +138,6 @@ class NotificationFrame(Gtk.Frame): self.add(self.box) if error: - # TODO: custom css file only for wizard add_class(self, "error-frame") @@ -250,11 +249,7 @@ class IntroductionPage(ScrolledWizardPage): class Heading(Gtk.Label): def __init__(self, label: str): super().__init__(label=label) - - # add_class(self, "heading") - # font weight - # TODO: set css em size - # TODO: bold text + add_class(self, "settings-subheading") class ChunkyButton(Gtk.Button): @@ -317,17 +312,18 @@ class ConfigMigrationPage(ScrolledWizardPage): self.add_start(self.grid) + self.err_box = NotificationFrame(wizard.config_error_box, error=True) self.success_box = NotificationFrame(wizard.config_import_box) self.from_scratch_box = NotificationFrame(wizard.config_new_box) - self.add_start(self.success_box) - self.add_start(self.from_scratch_box) + for box in self.err_box, self.success_box, self.from_scratch_box: + self.add_start(box) self.connect("map", self._hide_boxes) self.import_button.connect("clicked", self._on_import_clicked) self.new_button.connect("clicked", self._on_new_clicked) def _hide_boxes(self, page: Self) -> None: - for box in self.success_box, self.from_scratch_box: + for box in self.success_box, self.from_scratch_box, self.err_box: box.set_visible(False) def _on_new_clicked(self, button: Gtk.Button) -> None: @@ -346,7 +342,9 @@ class ConfigMigrationPage(ScrolledWizardPage): self.migrated = True self.success_box.set_visible(True) except Exception: - pass + self.err_box.set_visible(True) + EMITTER.emit("step_pending") + return EMITTER.emit("step_complete") EMITTER.emit("config", True) @@ -467,6 +465,7 @@ class Assistant(Gtk.Assistant): self.connect("cancel", self.destroy_and_quit) self.connect("close", self.destroy_and_quit) self.show_all() + load_css() def write_config(self) -> None: write_json(self.config_values, self.config_path) @@ -546,15 +545,13 @@ class SteamPathPage(ScrolledWizardPage): ) self.page_type = Gtk.AssistantPageType.INTRO - - # TODO: add custom CSS class to Gtk.Frame so that only this one is styled - err_box = NotificationFrame(wizard.error_steam_path, error=True) - self.err = err_box + self.err_box = NotificationFrame(wizard.no_valid_paths, error=True) self.scan_button = Gtk.Button(label=wizard.button_scan, halign=Gtk.Align.CENTER) self.scan_button.connect("clicked", self._on_scan_clicked) self.add_start(self.scan_button) + self.add_start(self.err_box) self.connect("map", self._start_incomplete) def _on_scan_clicked(self, button: Gtk.Button) -> None: @@ -567,9 +564,8 @@ class SteamPathPage(ScrolledWizardPage): spacing=10, ) total = len(paths) - err_box = NotificationFrame(wizard.no_valid_paths) if total == 0: - self.add_start(err_box) + self.err_box.set_visible(True) else: button_box.add(Gtk.Label(label=f"Steam paths found: {total} total.")) for i, button_path in enumerate(paths): @@ -589,11 +585,7 @@ class SteamPathPage(ScrolledWizardPage): return active.get_label() def _start_incomplete(self, page: Self) -> None: - self.err.set_visible(False) - - def _test_error_func(self, button: Gtk.CheckButton) -> None: - self.err.set_visible(True) - EMITTER.emit("step_incomplete") + self.err_box.set_visible(False) class SetupWizard(Gtk.Application): @@ -628,6 +620,3 @@ class Emitter(GObject.GObject): EMITTER = Emitter() - -# TODO: Ctrl-q -# TODO: change behavior of global emitter From 0a6bd831bdd1c23ed1def8836adcebb98850dff9 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 04:39:48 +0900 Subject: [PATCH 08/15] feat: static releases button --- dzgui/config/userprefs.py | 1 + dzgui/const/endpoints.py | 1 + dzgui/init/update.py | 11 ++++----- dzgui/main.py | 8 +++---- dzgui/views/base.py | 24 ------------------- dzgui/views/components/right_panel.py | 34 +++++++++++++++++++++++---- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/dzgui/config/userprefs.py b/dzgui/config/userprefs.py index f99245e..79f13c5 100644 --- a/dzgui/config/userprefs.py +++ b/dzgui/config/userprefs.py @@ -15,4 +15,5 @@ class UserPrefs: coords: Union["Coords", None] version: str paths: "Xdg" + update_available: bool use_miles: bool diff --git a/dzgui/const/endpoints.py b/dzgui/const/endpoints.py index cb1ca1c..621f233 100644 --- a/dzgui/const/endpoints.py +++ b/dzgui/const/endpoints.py @@ -5,6 +5,7 @@ BM_SERVERS = "https://api.battlemetrics.com/servers?" GITHUB = "https://github.com/aclist" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest" +GITHUB_USER_RELEASES = "https://github.com/aclist/dztui/releases" DB_IP = "https://db-ip.com/db/download/ip-to-city-lite" IP_ECHO = "https://ipecho.net/plain" COORDS_API = "http://ip-api.com/json" diff --git a/dzgui/init/update.py b/dzgui/init/update.py index e4c08e6..7ec9b91 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -16,7 +16,6 @@ def get_latest_release() -> str | None: try: res = requests.get(url, timeout=REQUEST_TIMEOUT) if res.status_code == 200: - print(res.json()) tag = res.json()["tag_name"] break except Exception as e: @@ -25,13 +24,13 @@ def get_latest_release() -> str | None: return tag -def check_updates(version: str) -> str | None: +def check_updates(version: str) -> bool: try: latest = get_latest_release() if latest is None: - return None + return False if Version(version) >= Version(latest): - return None - return latest + return False + return True except Exception: - return None + return False diff --git a/dzgui/main.py b/dzgui/main.py index 6bbcf41..9ac6ff3 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -23,10 +23,8 @@ from dzgui.init.migrate import ( ) from dzgui.init.prefix import get_version from dzgui.init.prereqs import has_steam_client - -# from dzgui.init.update import check_updates - from dzgui.strings import boot +from dzgui.init.update import check_updates # from dzgui.util.map_count import get_map_count from dzgui.util.deck import is_steam_deck, is_game_mode @@ -120,8 +118,7 @@ def main() -> None: with open(XDG.debug, "w") as f: f.truncate(0) - # TODO: update area in gutter - # new_version = check_updates(version) + update_available = check_updates(version) if _is_steam_deck is False: # TODO: sudo escalation dialog @@ -147,6 +144,7 @@ def main() -> None: coords=local_coords, version=version, paths=XDG, + update_available=update_available, use_miles=use_miles, ) # TODO: drop allow updates diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 082d218..2778439 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -44,30 +44,6 @@ logger = logging.getLogger(APP_NAME) warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) -## TODO: move to configs/servers -# def query_history() -> list | None: -# history_file = MainController.get_prefs().paths.history -# try: -# with open(history_file, "r") as f: -# rows = [row.rstrip("\n") for row in f] -# except OSError: -# rows = None -# return rows -# -## TODO: belongs in model -# def str_to_record(record: str) -> Record | None: -# r = record.split(":") -# if len(r) != 3: -# return None -# return Record(r[0], int(r[1]), int(r[2])) -# -## TODO: ibid -# def record_to_str(record: Record) -> str: -# return f"{record.ip}:{record.gameport}:{record.qport}" -# -# - - class OuterWindow(Gtk.Window): def __init__(self) -> None: super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER) diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index a0566fc..cea2521 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -1,12 +1,14 @@ from typing import Literal, TYPE_CHECKING +from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING +from dzgui.const.endpoints import GITHUB_USER_RELEASES from dzgui.const.enum import ServerTab from dzgui.util.clip import copy_clipboard +from dzgui.util.open_links import open_link_by_url from dzgui.views.components.buttonbox import ButtonBox from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.mod_panel import ModSelectionPanel -from dzgui.views.components.buttons import RefreshButton, KeysButton -from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING +from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton import gi @@ -40,14 +42,17 @@ class RightPanel(Gtk.Box): self.copying = False - # TODO: strings - version = self.controller.get_prefs().version + prefs = self.controller.get_prefs() + version = prefs.version + update = prefs.update_available + self.version_label = Gtk.Label( label=version, hexpand=True, vexpand=True, halign=Gtk.Align.END, valign=Gtk.Align.END, + # TODO: strings tooltip_text="Click to copy to clipboard", ) eb = Gtk.EventBox(halign=Gtk.Align.END, valign=Gtk.Align.END) @@ -58,7 +63,26 @@ class RightPanel(Gtk.Box): self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) - self.pack_start(eb, NO_EXPAND, FILL, NO_PADDING) + + self.version_button = IconTextButton( + "dialog-information-symbolic", label="Updates available" + ) + + self.gutter_box = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.END, + valign=Gtk.Align.END, + spacing=10, + ) + if update: + self.version_button.set_halign(Gtk.Align.END) + self.gutter_box.add(self.version_button) + self.version_button.connect("clicked", self._on_version_button_clicked) + self.gutter_box.add(eb) + self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING) + + def _on_version_button_clicked(self, button: Gtk.Button) -> None: + open_link_by_url(GITHUB_USER_RELEASES) def _on_server_page_changed( self, emitter: "Emitter", page: "ServerTreeView" From 446d4db290a82ab6c6116591988369e0ebb7a8f6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 04:54:14 +0900 Subject: [PATCH 09/15] chore: update file migration logic --- dzgui/config/xdg.py | 3 ++- dzgui/init/migrate.py | 12 +++++++++++- dzgui/main.py | 11 ++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/dzgui/config/xdg.py b/dzgui/config/xdg.py index 2826a0b..137d143 100644 --- a/dzgui/config/xdg.py +++ b/dzgui/config/xdg.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG + @dataclass class Xdg: config: Path @@ -73,7 +74,7 @@ def parse_filepaths(xdg: dict) -> Xdg: system = state / "logs" / SYSTEM_LOG debug = state / "logs" / DEBUG_LOG - columns = state / "dzg.cols.json" + columns = state / "dzg.columns.json" notes = state / "dzg.notes.json" resolution = state / "dzg.res.json" history = state / "dzg.history" diff --git a/dzgui/init/migrate.py b/dzgui/init/migrate.py index 140289c..55af183 100644 --- a/dzgui/init/migrate.py +++ b/dzgui/init/migrate.py @@ -18,6 +18,7 @@ def has_new_config(config: Path) -> bool: def migrate_cols_file(res: Path) -> None: + # NOTE: dzg.columns.json is API 7 spec old_res = Path.home() / LEGACY_COLS_PATH if old_res.is_file(): j = read_json(old_res) @@ -32,11 +33,20 @@ def migrate_cols_file(res: Path) -> None: def copy_state_files(state_path: Path) -> None: home = Path.home() legacy = home / ".local/state/dzgui" + to_copy = [ + "dzg.res.json", + "dzg.notes.json", + "dzg.history", + "dzg.versions", + "ips.csv", + ".month", + ] if state_path == legacy: # TODO: log this return for file in legacy.iterdir(): - shutil.copy(file, state_path / file.name) + if file.name in to_copy: + shutil.copy(file, state_path / file.name) def copy_ipdb(ips_path: Path) -> None: diff --git a/dzgui/main.py b/dzgui/main.py index 9ac6ff3..87ec2bc 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -18,8 +18,8 @@ from dzgui.init.dayz import is_dayz_installed from dzgui.init.flock import lock_acquire from dzgui.init.migrate import ( has_new_config, - # migrate_cols_file, - # copy_state_files, + copy_state_files, + migrate_cols_file, ) from dzgui.init.prefix import get_version from dzgui.init.prereqs import has_steam_client @@ -96,7 +96,6 @@ def main() -> None: if XDG.resolution.parent.is_dir() is False: make_parents(XDG.resolution) - # TODO: test if XDG.debug.is_file() is False: make_parents(XDG.debug) @@ -107,10 +106,9 @@ def main() -> None: del os.environ["GTK_IM_MODULE"] if has_new_config(XDG.config) is False: + migrate_cols_file(XDG.columns) + copy_state_files(xdg_paths["XDG_STATE_HOME"]) # TODO: add logging inside wizard - # TODO: copy notes file, version file, etc. - # migrate_cols_file(XDG.columns) - # copy_state_files(xdg_paths["XDG_STATE_HOME"]) SetupWizard(version, _is_steam_deck, XDG.config) return @@ -147,6 +145,5 @@ def main() -> None: update_available=update_available, use_miles=use_miles, ) - # TODO: drop allow updates print(boot.all_ok) App(prefs) From 867c47a4eb91f54356fa5678fb98933e5c0e9d83 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 05:53:28 +0900 Subject: [PATCH 10/15] chore: remove typo --- dzgui/views/components/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/views/components/misc.py b/dzgui/views/components/misc.py index b959046..7b9fc0f 100644 --- a/dzgui/views/components/misc.py +++ b/dzgui/views/components/misc.py @@ -28,7 +28,7 @@ class ClientCombo(Gtk.ComboBox): ) for client in clients: self.client_store.append(client) - self.set_model(self.client_store) # Text() + self.set_model(self.client_store) renderer_text = Gtk.CellRendererText() self.pack_start(renderer_text, True) self.add_attribute(renderer_text, "text", 0) From 5ef40859226f2084c8cadd2d16ccf78b87ed01c4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 15:51:27 +0900 Subject: [PATCH 11/15] chore: conform to PEP 440 versioning --- dzgui/scripts/update.sh | 28 ---------------------------- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 dzgui/scripts/update.sh diff --git a/dzgui/scripts/update.sh b/dzgui/scripts/update.sh deleted file mode 100644 index 7146dcc..0000000 --- a/dzgui/scripts/update.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# TODO: move this into package data dir -prefix="dzgui" -tag="$1" -prefix="$2" -dir="/tmp/dzgui-${tag}" - -mkdir -p "$dir" - -git clone -C "$dir" git@github.com:aclist/dzgui.git --branch "${tag}" -git -C "$dir" submodule update --recursive --init - -if [[ ! $(which python3.13) ]]; then - echo "Missing python3.13" - exit 1 -fi - -python3.13 -m venv "${prefix}" -source "$prefix/bin/activate" - -rm -rf "$dir" -if [[ $(which uv) ]]; then - uv --directory "$dir" pip install . -else - cd "$dir" - pip install -r requirements.txt - echo "Setup complete. Re-launch DZGUI via the command 'dzgui'" -fi diff --git a/pyproject.toml b/pyproject.toml index 5b1fa34..d750a00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux" authors = [ {name = "aclist"} ] -version = "7.0.0-beta.1" +version = "7.0.0b1" license = "GPL-3.0-or-later" license-files = ["LICENSE"] readme = "README.md" From 4841c9e04b91ba7c0abd8a97679535115679fa6d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 15:54:42 +0900 Subject: [PATCH 12/15] chore: add mypy exclusion --- CHANGELOG.md | 1 + dzgui/data/CHANGELOG.md | 1 + pyproject.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1898e..6cddc42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ ## Changed +- Conform to PEP 440 versioning for beta versions - Reduce padding on keys button - Boldface breadcrumbs - Bold labels inside frames diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index 8e1898e..6cddc42 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -37,6 +37,7 @@ ## Changed +- Conform to PEP 440 versioning for beta versions - Reduce padding on keys button - Boldface breadcrumbs - Bold labels inside frames diff --git a/pyproject.toml b/pyproject.toml index d750a00..6d8a58b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ line-length = 88 python_version = "3.13" disallow_untyped_defs = true warn_return_any = true +exclude = ["lib/*"] [[tool.mypy.overrides]] module = [ From 25e2e7e20e0ba27324dbc18dab2f3d09a8bbb45e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 19:18:39 +0900 Subject: [PATCH 13/15] chore: update build setup --- .gitignore | 2 ++ pyproject.toml | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 456bad6..73744d0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ build/ notes.otl uv.lock .coverage +pyapp-latest/ +dist/ diff --git a/pyproject.toml b/pyproject.toml index 6d8a58b..cc2d4c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,8 +54,12 @@ lint = [ "types-requests>=2.32.4", "types-psutil==7.2.2.20260508", ] +build = [ + "build==1.5.0", +] + dev = [ - "dzgui[docs,test,lint]" + "dzgui[docs,test,lint,build]" ] [build-system] From 13bd36cec6dd15a1df6230d88654ad79635c4978 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 22:54:03 +0900 Subject: [PATCH 14/15] fix: rearrange preferences box elements --- dzgui/lib/dayzquery | 2 +- dzgui/main.py | 2 +- dzgui/strings/wizard.py | 2 +- dzgui/views/dialogs/wizard.py | 51 +++++++++++++++++++---------------- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/dzgui/lib/dayzquery b/dzgui/lib/dayzquery index a22a9f4..07483b8 160000 --- a/dzgui/lib/dayzquery +++ b/dzgui/lib/dayzquery @@ -1 +1 @@ -Subproject commit a22a9f428cbe075d7dda62f78000296955eea92a +Subproject commit 07483b88ed096327ebca752f5e177011b604d7fe diff --git a/dzgui/main.py b/dzgui/main.py index 87ec2bc..94e1236 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -109,7 +109,7 @@ def main() -> None: migrate_cols_file(XDG.columns) copy_state_files(xdg_paths["XDG_STATE_HOME"]) # TODO: add logging inside wizard - SetupWizard(version, _is_steam_deck, XDG.config) + SetupWizard(_is_steam_deck, XDG.config) return setup_logger(XDG.debug) diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index 1c44901..345c3e7 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -1,7 +1,7 @@ ### IntroductionPage title_intro = "Welcome" blurb_intro = """ -This wizard is going to help you set up some common config options before launching the application. +This wizard will help you set up some common config options before launching the application. """ ### SteamPathPage diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 681b717..1c118cb 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -236,10 +236,10 @@ class SteamValidationPage(APIValidationPage): class IntroductionPage(ScrolledWizardPage): - def __init__(self, version: str): + def __init__(self): super().__init__( enum=PageNum.INTRO, - heading=f"Welcome to {APP_NAME} {version}!", + heading=f"Welcome to {APP_NAME}!", description=wizard.blurb_intro, ) self.page_type = Gtk.AssistantPageType.INTRO @@ -359,37 +359,39 @@ class PreferencesPage(ScrolledWizardPage): self.page_type = Gtk.AssistantPageType.INTRO # TODO: widgets and strings are largely a reimplementation of options page, consolidate - name_label = Gtk.Label(label=wizard.label_player) - self.name_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, spacing=10, halign=Gtk.Align.CENTER + name_label = Gtk.Label(label=wizard.label_player, halign=Gtk.Align.START) + self.name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + self.name_entry = Gtk.Entry( + placeholder_text=wizard.placeholder_player, + halign=Gtk.Align.END, + width_chars=40, ) - self.name_entry = Gtk.Entry(placeholder_text=wizard.placeholder_player) self.name_entry.connect("changed", self._on_entry_changed) self.name_box.add(name_label) self.name_box.add(self.name_entry) - self.dist_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, spacing=10, halign=Gtk.Align.CENTER - ) + self.dist_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) self.radio_km = Gtk.RadioButton.new_with_label(None, wizard.radio_km) self.radio_miles = Gtk.RadioButton.new_with_label_from_widget( self.radio_km, wizard.radio_mi ) - dist_label = Gtk.Label(label=wizard.label_dist) + dist_label = Gtk.Label(label=wizard.label_dist, halign=Gtk.Align.START) self.dist_box.add(dist_label) self.dist_box.add(self.radio_km) self.dist_box.add(self.radio_miles) - client_label = Gtk.Label(label=wizard.label_client) - self.client_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, spacing=10, halign=Gtk.Align.CENTER - ) + 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) - for el in self.name_box, self.dist_box, self.client_box: - self.add_start(el) + 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: + outer_box.add(el) + self.add_start(outer_box) def get_prefs(self) -> tuple[str, bool, str]: name = self.name_entry.get_text().strip() @@ -420,7 +422,7 @@ class CompletionPage(ScrolledWizardPage): class Assistant(Gtk.Assistant): - def __init__(self, version: str, is_deck: bool, config: Path): + def __init__(self, is_deck: bool, config: Path): super().__init__() if is_deck: self.fullscreen() @@ -431,7 +433,7 @@ class Assistant(Gtk.Assistant): self.config_values: dict[str, Any] = config_boilerplate - self.page1 = IntroductionPage(version) + self.page1 = IntroductionPage() self.page2 = ConfigMigrationPage(config) self.page3 = SteamPathPage() self.page4 = SteamValidationPage() @@ -565,7 +567,7 @@ class SteamPathPage(ScrolledWizardPage): ) total = len(paths) if total == 0: - self.err_box.set_visible(True) + show_errors = True else: button_box.add(Gtk.Label(label=f"Steam paths found: {total} total.")) for i, button_path in enumerate(paths): @@ -577,8 +579,11 @@ class SteamPathPage(ScrolledWizardPage): frame = RadioFrame(self.first_button, button_path) button_box.add(frame) self.add_start(button_box) + show_errors = False EMITTER.emit("step_complete") self.show_all() + # TODO: more robust approach + self.err_box.set_visible(show_errors) def get_path_from_radio(self) -> str: active = next(r for r in self.first_button.get_group() if r.get_active()) @@ -589,17 +594,17 @@ class SteamPathPage(ScrolledWizardPage): class SetupWizard(Gtk.Application): - def __init__(self, version: str, is_deck: bool, config: Path) -> None: + def __init__(self, is_deck: bool, config: Path) -> None: super().__init__() GLib.set_prgname(APP_NAME) - Window(version, is_deck, config) + Window(is_deck, config) Gtk.main() class Window(Gtk.Window): - def __init__(self, version: str, is_deck: bool, config: Path) -> None: + def __init__(self, is_deck: bool, config: Path) -> None: super().__init__(title=APP_NAME, icon_name=APP_NAME) - Assistant(version, is_deck, config) + Assistant(is_deck, config) class Emitter(GObject.GObject): From 9b2b26433e4ecc0f7325cc692268683a072ea3ec Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 17 May 2026 22:56:13 +0900 Subject: [PATCH 15/15] chore: drop comment --- dzgui/strings/wizard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index 345c3e7..56c9053 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -5,7 +5,6 @@ This wizard will help you set up some common config options before launching the """ ### SteamPathPage -# TODO heading_steam_path = "Steam path" blurb_steam_path = """ DZGUI needs to find the location to your default Steam installation.