From c063059c196d8e2d2c5e82456b9ca74c69556356 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:11:51 +0900 Subject: [PATCH 01/10] feat: auto update --- dzgui/config/userprefs.py | 2 +- dzgui/const/constants.py | 4 ++ dzgui/init/update.py | 20 ++++--- dzgui/main.py | 4 +- dzgui/managers/threading.py | 2 +- dzgui/strings/dialogs.py | 4 ++ dzgui/util/strings.py | 1 + dzgui/views/components/right_panel.py | 81 +++++++++++++++++++++++---- dzgui/views/dialogs/generic.py | 4 +- scripts/release.py | 3 +- 10 files changed, 98 insertions(+), 27 deletions(-) diff --git a/dzgui/config/userprefs.py b/dzgui/config/userprefs.py index 79f13c5..a392235 100644 --- a/dzgui/config/userprefs.py +++ b/dzgui/config/userprefs.py @@ -15,5 +15,5 @@ class UserPrefs: coords: Union["Coords", None] version: str paths: "Xdg" - update_available: bool + latest_release: str | None use_miles: bool diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index 8304fd4..6818fef 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -73,3 +73,7 @@ UBUNTU_STEAM_PATH = ".steam/steam" DEBIAN_STEAM_PATH = ".steam/debian-installation" LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG") + +TMP_PATH = "/tmp" +TMP_TARBALL = "/tmp/dzgui.tar.gz" +TMP_EXE = "/tmp/dzgui" diff --git a/dzgui/init/update.py b/dzgui/init/update.py index 7ec9b91..ef10b07 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -9,28 +9,30 @@ from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES logger = logging.getLogger(APP_NAME) -def get_latest_release() -> str | None: +def get_latest_release() -> tuple[str, str] | tuple[None, None]: tag = None + url = None # TODO: check order; github often has gateway errors for url in [GITHUB_RELEASES, CODEBERG_RELEASES]: try: res = requests.get(url, timeout=REQUEST_TIMEOUT) if res.status_code == 200: - tag = res.json()["tag_name"] + tag = str(res.json()["tag_name"]) + url = str(res.json()["assets"][0]["browser_download_url"]) break except Exception as e: logger.critical(e) continue - return tag + return tag, url -def check_updates(version: str) -> bool: +def check_updates(version: str) -> str | None: try: - latest = get_latest_release() + latest, url = get_latest_release() if latest is None: - return False + return None if Version(version) >= Version(latest): - return False - return True + return None + return url except Exception: - return False + return None diff --git a/dzgui/main.py b/dzgui/main.py index 94e1236..8e4783f 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -116,7 +116,7 @@ def main() -> None: with open(XDG.debug, "w") as f: f.truncate(0) - update_available = check_updates(version) + latest_release = check_updates(version) if _is_steam_deck is False: # TODO: sudo escalation dialog @@ -142,7 +142,7 @@ def main() -> None: coords=local_coords, version=version, paths=XDG, - update_available=update_available, + latest_release=latest_release, use_miles=use_miles, ) print(boot.all_ok) diff --git a/dzgui/managers/threading.py b/dzgui/managers/threading.py index b27a409..91db2e9 100644 --- a/dzgui/managers/threading.py +++ b/dzgui/managers/threading.py @@ -29,7 +29,7 @@ def call_on_thread( self = args[0] stored = StoredFunc(func, *args, **kwargs) if not hasattr(self, "thread_man"): - raise AttributeError + raise AttributeError(f"Object '{self}' has no attribute 'thread_man'") if type(self.thread_man) is not ThreadingManager: raise TypeError( "Attribute 'thread_man' must be of type 'ThreadingManager'" diff --git a/dzgui/strings/dialogs.py b/dzgui/strings/dialogs.py index c791a47..d7587a1 100644 --- a/dzgui/strings/dialogs.py +++ b/dzgui/strings/dialogs.py @@ -1,2 +1,6 @@ waiting_for_launch = "Waiting for DayZ to launch" waiting_for_mods = "Waiting for Steam to update mods" + +fetching_update = "Fetching update" +failed_to_update = "Failed to update DZGUI executable" +update_success = "Updated successfully. Please exit and relaunch." diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 176d259..45ab37d 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -38,6 +38,7 @@ input_required = "User input required" confirm = "Confirmation" notice = "Notice" wait = "Please wait" +restart_required = "App relaunch required" # Wait dialogs ping = "Ping" diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index cea2521..660b959 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -1,14 +1,31 @@ +import logging +import os +import requests +import shutil +import subprocess +import tarfile + 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.constants import ( + APP_NAME, + NO_EXPAND, + NO_FILL, + FILL, + NO_PADDING, + TMP_EXE, + TMP_PATH, + TMP_TARBALL, +) from dzgui.const.enum import ServerTab +from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager +from dzgui.strings import dialogs 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 IconTextButton, RefreshButton, KeysButton +from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog import gi @@ -20,11 +37,14 @@ if TYPE_CHECKING: from dzgui.controllers.emitter import Emitter from dzgui.views.trees.tree_servers import ServerTreeView +logger = logging.getLogger(APP_NAME) + class RightPanel(Gtk.Box): def __init__(self, controller: "Controller"): super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL) + self.thread_man = ThreadingManager(controller) self.controller = controller self.controller.register_widget("right_panel", self) @@ -44,7 +64,7 @@ class RightPanel(Gtk.Box): prefs = self.controller.get_prefs() version = prefs.version - update = prefs.update_available + update = prefs.latest_release self.version_label = Gtk.Label( label=version, @@ -64,7 +84,7 @@ class RightPanel(Gtk.Box): self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) - self.version_button = IconTextButton( + self.update_button = IconTextButton( "dialog-information-symbolic", label="Updates available" ) @@ -74,15 +94,54 @@ class RightPanel(Gtk.Box): 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) + if update is not None: + self.update_button.set_halign(Gtk.Align.END) + self.gutter_box.add(self.update_button) + self.update_button.connect( + "clicked", self._on_update_button_clicked, update + ) 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_update_success(self) -> None: + msg = dialogs.update_success + dialog = QuitDialog(self.controller, msg) + dialog.run() + + def _on_update_failure(self, msg: str) -> None: + dialog = ExceptionDialog(self.controller, msg) + dialog.run() + + @call_on_thread(dialogs.fetching_update) + def _on_update_button_clicked(self, button: Gtk.Button, url: str) -> None: + try: + res = requests.get(url) + if res.status_code == 200: + with open(TMP_TARBALL, "wb") as file: + file.write(res.content) + with tarfile.open(TMP_TARBALL) as tar: + tar.extractall(TMP_PATH) + + exe_path = os.getenv("PYAPP") + if exe_path is None: + msg = "Failed to find DZGUI launch executable" + func = StoredFunc(self._on_update_failure, msg) + self.thread_man.set_cleanup_func(func, destroy_first=True) + return + shutil.move(TMP_EXE, exe_path) + + proc = subprocess.run([exe_path, "self", "restore"]) + if proc.returncode == 0: + func = StoredFunc(self._on_update_success) + self.thread_man.set_cleanup_func(func, destroy_first=True) + else: + msg = dialogs.failed_to_update + func = StoredFunc(self._on_update_failure, msg) + self.thread_man.set_cleanup_func(func, destroy_first=True) + except Exception as e: + func = StoredFunc(self._on_update_failure, e) + self.thread_man.set_cleanup_func(func, destroy_first=True) + logger.warning(e) def _on_server_page_changed( self, emitter: "Emitter", page: "ServerTreeView" diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index eb10d58..b0324f3 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -81,7 +81,7 @@ class NotifyDialog(GenericDialog): controller=controller, text=strings.notice, mtype=Gtk.MessageType.INFO, - buttons=Gtk.ButtonsType.OK_CANCEL, + buttons=Gtk.ButtonsType.OK, secondary=secondary, ) @@ -143,7 +143,7 @@ class QuitDialog(GenericDialog): def __init__(self, controller: "Controller", secondary: str): super().__init__( controller=controller, - text=strings.wait, + text=strings.restart_required, mtype=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.NONE, secondary=secondary, diff --git a/scripts/release.py b/scripts/release.py index c2e97ef..7faf0ca 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -30,8 +30,9 @@ env["PYAPP_PROJECT_VERSION"] = version env["PYAPP_PROJECT_NAME"] = APP_NAME_LOWER env["PYAPP_EXEC_SPEC"] = entrypoint env["PYAPP_PROJECT_PATH"] = wheel -env["PYAPP_DISTRIBUTION_EMBED"] = "1" +env["PYAPP_DISTRIBUTION_EMBED"] = "true" env["PYAPP_PYTHON_VERSION"] = "3.13" +env["PYAPP_PASS_LOCATION"] = "true" proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir) if proc.returncode == 0: From 7f1125432ee3cc86fd7ae23fb2b3c7abf80e89c2 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:12:20 +0900 Subject: [PATCH 02/10] chore: bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 44ace5b..c055619 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.0b1" +version = "7.0.0b2" license = "GPL-3.0-or-later" license-files = ["LICENSE"] readme = "README.md" From 1b20969c303d8551e362b2f434b535bc45263aaf Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:24:59 +0900 Subject: [PATCH 03/10] chore: clear typehinting errors --- dzgui/init/update.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dzgui/init/update.py b/dzgui/init/update.py index ef10b07..55d96de 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -2,6 +2,7 @@ import logging import requests from packaging.version import Version +from typing import Any from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES @@ -9,7 +10,7 @@ from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES logger = logging.getLogger(APP_NAME) -def get_latest_release() -> tuple[str, str] | tuple[None, None]: +def get_latest_release() -> tuple[Any, Any] | tuple[None, None]: tag = None url = None # TODO: check order; github often has gateway errors @@ -17,8 +18,8 @@ def get_latest_release() -> tuple[str, str] | tuple[None, None]: try: res = requests.get(url, timeout=REQUEST_TIMEOUT) if res.status_code == 200: - tag = str(res.json()["tag_name"]) - url = str(res.json()["assets"][0]["browser_download_url"]) + tag = res.json()["tag_name"] + url = res.json()["assets"][0]["browser_download_url"] break except Exception as e: logger.critical(e) From af3a9cfa775de06cbfda07305138522790f6fe0d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:25:31 +0900 Subject: [PATCH 04/10] docs: update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca32a3c..90af44b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,7 +65,6 @@ ## Unreleased - Load offline mods - Choose to jump into splash screen instead of server -- Setup wizard - Local documentation - Raw debug command in context menu From 418c0f026d05a8b2a260a2ba3c22cdaccaec5690 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:30:02 +0900 Subject: [PATCH 05/10] chore: restore ALLOW_UPDATES flag --- dzgui/const/update.py | 13 +++++++++++++ dzgui/data/CHANGELOG.md | 1 - dzgui/views/components/right_panel.py | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 dzgui/const/update.py diff --git a/dzgui/const/update.py b/dzgui/const/update.py new file mode 100644 index 0000000..0487e2c --- /dev/null +++ b/dzgui/const/update.py @@ -0,0 +1,13 @@ +""" +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 ability for the application to self-manage and +will suppress the "Updates available" button in the gutter. +""" + +ALLOW_UPDATES = True diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index ca32a3c..90af44b 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -65,7 +65,6 @@ ## Unreleased - Load offline mods - Choose to jump into splash screen instead of server -- Setup wizard - Local documentation - Raw debug command in context menu diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index 660b959..aad0d09 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -17,6 +17,7 @@ from dzgui.const.constants import ( TMP_PATH, TMP_TARBALL, ) +from dzgui.const.update import ALLOW_UPDATES from dzgui.const.enum import ServerTab from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings import dialogs @@ -94,7 +95,7 @@ class RightPanel(Gtk.Box): valign=Gtk.Align.END, spacing=10, ) - if update is not None: + if ALLOW_UPDATES and update is not None: self.update_button.set_halign(Gtk.Align.END) self.gutter_box.add(self.update_button) self.update_button.connect( From d5309c1ddff297419022cb409c031a27ec538c03 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:37:42 +0900 Subject: [PATCH 06/10] chore: include version number in tarball --- scripts/release.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 7faf0ca..b606ad5 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -20,9 +20,10 @@ interpreter = "py3" arch = "none" target = "any" version = metadata.version(APP_NAME_LOWER) -filename = f"{APP_NAME_LOWER}-{version}-{interpreter}-{arch}-{target}.whl" +wheelname = f"{APP_NAME_LOWER}-{version}-{interpreter}-{arch}-{target}.whl" +tarname = f"{APP_NAME_LOWER}-{version}.tar.gz" -wheel = str(output.joinpath(filename)) +wheel = str(output.joinpath(wheelname)) entrypoint = "dzgui.main:main" env = os.environ @@ -39,7 +40,6 @@ if proc.returncode == 0: output_exe = root.joinpath("pyapp-latest/target/release/pyapp") release_exe = output.joinpath(APP_NAME_LOWER) output_exe.rename(release_exe) - tarname = APP_NAME_LOWER + ".tar.gz" tarpath = output.joinpath(tarname) with tarfile.open(tarpath, "w:gz") as tar: tar.add(release_exe) From 0a84c20d1ddba587d16a5a10a641969501c27cfb Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 20:48:23 +0900 Subject: [PATCH 07/10] fix: explicitly set arcname in tarball --- scripts/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index b606ad5..b518270 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -42,5 +42,5 @@ if proc.returncode == 0: output_exe.rename(release_exe) tarpath = output.joinpath(tarname) with tarfile.open(tarpath, "w:gz") as tar: - tar.add(release_exe) + tar.add(release_exe, arcname=APP_NAME_LOWER) print(f"Wrote tarfile to '{tarpath}'") From 7b5d99c34b7856a78c1d75b527055188f628d594 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 21:18:54 +0900 Subject: [PATCH 08/10] feat: print version number when loading --- dzgui/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dzgui/main.py b/dzgui/main.py index 8e4783f..cec454f 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -89,6 +89,8 @@ def main() -> None: version = get_version() set_locale() + print(f"{APP_NAME} {version}") + # NOTE: consider aborting this check if steam deck xdg_paths = get_xdg_paths() XDG = parse_filepaths(xdg_paths) From fe9f488e1080b4e90ccb607977aa8d515f75d88b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 21:19:18 +0900 Subject: [PATCH 09/10] fix: source metadata directly from wheel --- scripts/release.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index b518270..e4768b2 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -3,32 +3,27 @@ import os import subprocess import tarfile -from importlib import metadata from pathlib import Path -from dzgui.const.constants import APP_NAME_LOWER - root = Path(__file__).resolve().parents[1] output = root.joinpath("dist") pyapp_dir = root.joinpath("pyapp-latest") builder = build.ProjectBuilder(root) -builder.build("wheel", output_directory=output) +wheel = builder.build("wheel", output_directory=output) +stem = Path(wheel).stem +tarname = f"{stem}.tar.gz" -interpreter = "py3" -arch = "none" -target = "any" -version = metadata.version(APP_NAME_LOWER) -wheelname = f"{APP_NAME_LOWER}-{version}-{interpreter}-{arch}-{target}.whl" -tarname = f"{APP_NAME_LOWER}-{version}.tar.gz" +metadata = stem.split("-") +appname = metadata[0] +version = metadata[1] -wheel = str(output.joinpath(wheelname)) entrypoint = "dzgui.main:main" env = os.environ env["PYAPP_PROJECT_VERSION"] = version -env["PYAPP_PROJECT_NAME"] = APP_NAME_LOWER +env["PYAPP_PROJECT_NAME"] = appname env["PYAPP_EXEC_SPEC"] = entrypoint env["PYAPP_PROJECT_PATH"] = wheel env["PYAPP_DISTRIBUTION_EMBED"] = "true" @@ -38,9 +33,9 @@ env["PYAPP_PASS_LOCATION"] = "true" proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir) if proc.returncode == 0: output_exe = root.joinpath("pyapp-latest/target/release/pyapp") - release_exe = output.joinpath(APP_NAME_LOWER) + release_exe = output.joinpath(appname) output_exe.rename(release_exe) tarpath = output.joinpath(tarname) with tarfile.open(tarpath, "w:gz") as tar: - tar.add(release_exe, arcname=APP_NAME_LOWER) + tar.add(release_exe, arcname=appname) print(f"Wrote tarfile to '{tarpath}'") From de4521a3662afaeb645ffcf5ec52cd822c2cd4d9 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 18 May 2026 21:45:08 +0900 Subject: [PATCH 10/10] chore: use string file --- dzgui/views/components/right_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index aad0d09..efea8e6 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -125,7 +125,7 @@ class RightPanel(Gtk.Box): exe_path = os.getenv("PYAPP") if exe_path is None: - msg = "Failed to find DZGUI launch executable" + msg = dialogs.failed_to_update func = StoredFunc(self._on_update_failure, msg) self.thread_man.set_cleanup_func(func, destroy_first=True) return