From 88ad8cf7c23570f09b9fc28701e1d26883e97731 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:29:38 +0900 Subject: [PATCH 01/58] feat: expand all changelog nodes --- dzgui/views/pages/changelog.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 4d36637..9548b11 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -1,5 +1,6 @@ import logging import re +import textwrap from typing import TYPE_CHECKING from importlib import resources @@ -7,6 +8,7 @@ from importlib import resources from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH from dzgui.util.strings import missing_changelog from dzgui.util.format import format_pango +from dzgui.views.components.box import HBox, VBox from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin @@ -31,20 +33,40 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig logger.critical(e) changelog = missing_changelog - # TODO: should long text be wrapped? + # FIXME: wrap long text self.controller = controller self.box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10 ) self.add(self.box) + expand_all = Gtk.Button( + label="Expand all", halign=Gtk.Align.START, margin_start=20 + ) + expand_all.connect("clicked", self._on_expand_all_clicked) + self.box.add(expand_all) + + self.expanded = False + self.expanders: list[Gtk.Expander] = [] self.connect("key-press-event", self._on_keypress) self.connect("key-press-event", self._on_esc_keypress) changes = self._parse(changelog) self._generate_nodes(changes) + self.show_all() + def _on_expand_all_clicked(self, button: Gtk.Button) -> None: + self.expanded = not self.expanded + for expander in self.expanders: + # NOTE: simply setting set_expanded() does not trigger activate() signal, + # so margins are not applied + if expander.get_expanded() == self.expanded: + continue + expander.activate() + label = "Collapse all" if self.expanded else "Expand all" + button.set_label(label) + def grab_content_area(self) -> None: self.grab_focus() @@ -65,9 +87,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig text = "\n".join(changes) formatted = format_pango(text) - container = Gtk.Box( - valign=Gtk.Align.START, halign=Gtk.Align.START - ) + container = Gtk.Box(valign=Gtk.Align.START, halign=Gtk.Align.START) label = Gtk.Label() label.set_markup(formatted) container.add(label) @@ -81,6 +101,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig expander.add(container) expander.connect("activate", self._on_expand, container) self.box.add(expander) + self.expanders.append(expander) def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None: """ @@ -94,6 +115,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig container.set_margin_bottom(15) def _parse(self, changelog: str) -> list[tuple[str, list[str]]]: + release = "" releases: list[tuple[str, list[str]]] = [] release_notes: list[str] = [] @@ -107,5 +129,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig release = "" release = line continue - release_notes.append(line.rstrip()) + release_notes.append(textwrap.fill(line.rstrip(), width=120)) return releases From eeffdb57f36f7aa6eea51e1cc5501957363b7b7c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:30:10 +0900 Subject: [PATCH 02/58] chore: remove unused imports --- dzgui/views/pages/changelog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 9548b11..35d6e48 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -8,7 +8,6 @@ from importlib import resources from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH from dzgui.util.strings import missing_changelog from dzgui.util.format import format_pango -from dzgui.views.components.box import HBox, VBox from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin From 456c6e62e8b9be7f6d5aa356b24186b4b7e4333b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:59:13 +0900 Subject: [PATCH 03/58] chore: add test for changelog markdown headers --- tests/test_changelog.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 3eae186..acc6019 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,20 +1,34 @@ +import re import pytest -from importlib import resources -from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH +from pathlib import Path @pytest.fixture -def changelog(): - path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH) - return path +def changelog(request) -> None: + root = request.config.rootpath + changelog = Path(root).joinpath("CHANGELOG.md").read_text() + return changelog -def test_headings(changelog): - with open(changelog, "r") as f: - lines = f.readlines() - for line in lines: - if line.startswith("#"): - pass - # TODO: use regex - pass +def count_hash(line: str) -> int: + cnt = 0 + for c in line: + if c == "#": + cnt += 1 + return cnt + + +@pytest.mark.FOO +def test_changelog_prefix(changelog) -> None: + r = r".*(\[.*\]).*" + lines = changelog.splitlines() + sort = sorted(lines) + match = [line for line in sort if line.startswith("#")] + for m in match: + if "Changelog" in m: + assert count_hash(m) == 1 + elif re.match(r, m) is not None: + assert count_hash(m) == 2 + else: + assert count_hash(m) == 3 From 79bbc25714e5fe72b3936d5f68d1b6951a6b40d6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:59:13 +0900 Subject: [PATCH 04/58] chore: add test for changelog markdown headers --- tests/test_changelog.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 3eae186..acc6019 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,20 +1,34 @@ +import re import pytest -from importlib import resources -from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH +from pathlib import Path @pytest.fixture -def changelog(): - path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH) - return path +def changelog(request) -> None: + root = request.config.rootpath + changelog = Path(root).joinpath("CHANGELOG.md").read_text() + return changelog -def test_headings(changelog): - with open(changelog, "r") as f: - lines = f.readlines() - for line in lines: - if line.startswith("#"): - pass - # TODO: use regex - pass +def count_hash(line: str) -> int: + cnt = 0 + for c in line: + if c == "#": + cnt += 1 + return cnt + + +@pytest.mark.FOO +def test_changelog_prefix(changelog) -> None: + r = r".*(\[.*\]).*" + lines = changelog.splitlines() + sort = sorted(lines) + match = [line for line in sort if line.startswith("#")] + for m in match: + if "Changelog" in m: + assert count_hash(m) == 1 + elif re.match(r, m) is not None: + assert count_hash(m) == 2 + else: + assert count_hash(m) == 3 From 5536180fecdd5341c09bf781ef90bf645616d8f7 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:03:27 +0900 Subject: [PATCH 05/58] chore: drop placeholder mark --- tests/test_changelog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index acc6019..e897571 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -19,7 +19,6 @@ def count_hash(line: str) -> int: return cnt -@pytest.mark.FOO def test_changelog_prefix(changelog) -> None: r = r".*(\[.*\]).*" lines = changelog.splitlines() From d0ccc3bf4cacd073d507154ccf57f6c15d83dda3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:22:14 +0900 Subject: [PATCH 06/58] feat: add notebook to ExceptionDialog --- dzgui/views/dialogs/generic.py | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index c9b890c..19e6b44 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -196,17 +196,49 @@ class ExceptionDialog(GenericDialog): propagate_natural_height=True, max_content_height=500 ) box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) - # TODO: wrap/truncate long messages textview = Gtk.TextView( - wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + wrap_mode=Gtk.WrapMode.WORD, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) - box.pack_start(textview, EXPAND, FILL, 10) - scrollable.add(box) + box.pack_start(textview, EXPAND, FILL, 0) + + self.error_details = Gtk.ScrolledWindow( + overlay_scrolling=False, + max_content_height=150, + propagate_natural_height=False, + ) + details_box = Gtk.Box( + hexpand=True, + vexpand=True, + margin_right=5, + orientation=Gtk.Orientation.VERTICAL, + ) + + self.details_buffer = Gtk.TextBuffer() + details_textview = Gtk.TextView( + wrap_mode=Gtk.WrapMode.WORD_CHAR, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, + ) + + details_textview.set_buffer(self.details_buffer) + details_box.pack_start(details_textview, EXPAND, FILL, 10) + self.error_details.add(details_box) + + self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15) + self.error_notebook.append_page(box, Gtk.Label(label="Error")) + self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details")) + self.error_notebook.connect("switch-page", self._on_page_changed) + scrollable.add(self.error_notebook) content = self.get_content_area() content.set_spacing(0) - # TODO: padding around top of content area when traceback is long content.add(scrollable) copy_button = ClipboardButton(controller, self.get_trace) @@ -219,6 +251,18 @@ class ExceptionDialog(GenericDialog): self.ok.grab_focus() self.connect("response", self._on_response) + def _on_page_changed( + self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + ) -> None: + if child == self.error_details: + child.set_propagate_natural_height(True) + else: + self.error_details.set_propagate_natural_height(False) + + def set_secondary_text(self, text: str) -> None: + self.details_buffer.set_text(text) + self.error_notebook.set_show_tabs(True) + def get_trace(self) -> str: return self.trace From 067b77bf92d639c3b2f280f873983b8111c29103 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:26:42 +0900 Subject: [PATCH 07/58] fix: clear typehinting errors --- dzgui/views/dialogs/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 19e6b44..16d0e8e 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -252,10 +252,10 @@ class ExceptionDialog(GenericDialog): self.connect("response", self._on_response) def _on_page_changed( - self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: if child == self.error_details: - child.set_propagate_natural_height(True) + self.error_details.set_propagate_natural_height(True) else: self.error_details.set_propagate_natural_height(False) From 1e8a423ec346a1d5c7e9dd167959458bb59292be Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:38:45 +0900 Subject: [PATCH 08/58] fix: add bottom padding --- dzgui/views/dialogs/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 16d0e8e..f3174eb 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -202,6 +202,7 @@ class ExceptionDialog(GenericDialog): left_margin=10, right_margin=10, top_margin=15, + bottom_margin=10, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) box.pack_start(textview, EXPAND, FILL, 0) From 8098c4b68c7833a9074ec02621b1eae32dfd9073 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:02 +0900 Subject: [PATCH 09/58] feat: internal knowledeat: add knowledge base text to specific exception dialogs --- dzgui/managers/connection.py | 6 +++++- dzgui/managers/mods.py | 3 ++- dzgui/model/servers.py | 3 ++- dzgui/strings/kb.py | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 dzgui/strings/kb.py diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index c96abc2..9baf109 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -45,6 +45,7 @@ from dzgui.strings.dialogs import ( waiting_for_mods, waiting_for_directories, ) +from dzgui.strings import kb from dzgui.strings.server_mods import checkmark, resync from dzgui.util.format import format_mib from dzgui.util.strings import dialog, server_timeout @@ -193,7 +194,9 @@ class ConnectionManager: is_last = self.is_last_server() # TODO: strings - invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"] + invalid_mods = [ + (mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod" + ] prereqs = Prerequisites( name=info.server_name, @@ -298,6 +301,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _connect_steam(self, menu_only: bool) -> None: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index da31385..522c340 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -20,7 +20,7 @@ from dzgui.const.constants import ( from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.format import format_mods from dzgui.util.strings import server_timeout from dzgui.util.symlink import rebuild_symlinks @@ -215,6 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_seconary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index bc5d616..cfb2c0a 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -14,7 +14,7 @@ from dzgui.const.constants import ( ) from dzgui.const.enum import FilterMode, Preferences, ServerTab from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.strings import server_timeout, dialog from dzgui.views.dialogs.generic import ExceptionDialog @@ -420,6 +420,7 @@ class ServerModelManager: # customize statusbar and dialog accordingly if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _push_data(self, data: list[Any]) -> None: diff --git a/dzgui/strings/kb.py b/dzgui/strings/kb.py new file mode 100644 index 0000000..42f300b --- /dev/null +++ b/dzgui/strings/kb.py @@ -0,0 +1,15 @@ +DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration. + +Many third-party DayZ servers use a server rental/hosting provider with DDoS protection. + +This can cause responses from servers to originate from a server other than the one originally queried. + +Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation). + +By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue. + +If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. + +In addition, packets received from server responses are expected to be a standard size: MTU (maximum transmission unit) of 1500. + +Deviation from this may cause your router to discard incoming responses from the server.""" From 250da7a635e77a2bde2d8bd094d673208487c5b8 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:34 +0900 Subject: [PATCH 10/58] fix: typo --- dzgui/managers/mods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 522c340..742873a 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -215,7 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_seconary_text(kb.DZG_006) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: From 1e073af27ab548ee8dd2e91e596d71873808dfdb Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:22:14 +0900 Subject: [PATCH 11/58] feat: add notebook to ExceptionDialog --- dzgui/views/dialogs/generic.py | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index c9b890c..19e6b44 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -196,17 +196,49 @@ class ExceptionDialog(GenericDialog): propagate_natural_height=True, max_content_height=500 ) box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) - # TODO: wrap/truncate long messages textview = Gtk.TextView( - wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + wrap_mode=Gtk.WrapMode.WORD, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) - box.pack_start(textview, EXPAND, FILL, 10) - scrollable.add(box) + box.pack_start(textview, EXPAND, FILL, 0) + + self.error_details = Gtk.ScrolledWindow( + overlay_scrolling=False, + max_content_height=150, + propagate_natural_height=False, + ) + details_box = Gtk.Box( + hexpand=True, + vexpand=True, + margin_right=5, + orientation=Gtk.Orientation.VERTICAL, + ) + + self.details_buffer = Gtk.TextBuffer() + details_textview = Gtk.TextView( + wrap_mode=Gtk.WrapMode.WORD_CHAR, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, + ) + + details_textview.set_buffer(self.details_buffer) + details_box.pack_start(details_textview, EXPAND, FILL, 10) + self.error_details.add(details_box) + + self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15) + self.error_notebook.append_page(box, Gtk.Label(label="Error")) + self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details")) + self.error_notebook.connect("switch-page", self._on_page_changed) + scrollable.add(self.error_notebook) content = self.get_content_area() content.set_spacing(0) - # TODO: padding around top of content area when traceback is long content.add(scrollable) copy_button = ClipboardButton(controller, self.get_trace) @@ -219,6 +251,18 @@ class ExceptionDialog(GenericDialog): self.ok.grab_focus() self.connect("response", self._on_response) + def _on_page_changed( + self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + ) -> None: + if child == self.error_details: + child.set_propagate_natural_height(True) + else: + self.error_details.set_propagate_natural_height(False) + + def set_secondary_text(self, text: str) -> None: + self.details_buffer.set_text(text) + self.error_notebook.set_show_tabs(True) + def get_trace(self) -> str: return self.trace From 0ebc25b3b4b0669be63cf92acaf6e9f9153b550d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:26:42 +0900 Subject: [PATCH 12/58] fix: clear typehinting errors --- dzgui/views/dialogs/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 19e6b44..16d0e8e 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -252,10 +252,10 @@ class ExceptionDialog(GenericDialog): self.connect("response", self._on_response) def _on_page_changed( - self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: if child == self.error_details: - child.set_propagate_natural_height(True) + self.error_details.set_propagate_natural_height(True) else: self.error_details.set_propagate_natural_height(False) From 8087f3a707b120da95a797908e2e0e7974276ab5 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:38:45 +0900 Subject: [PATCH 13/58] fix: add bottom padding --- dzgui/views/dialogs/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 16d0e8e..f3174eb 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -202,6 +202,7 @@ class ExceptionDialog(GenericDialog): left_margin=10, right_margin=10, top_margin=15, + bottom_margin=10, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) box.pack_start(textview, EXPAND, FILL, 0) From 0bb8f3206f4048375da2a78e21189c20896eb90d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:02 +0900 Subject: [PATCH 14/58] feat: internal knowledeat: add knowledge base text to specific exception dialogs --- dzgui/managers/connection.py | 6 +++++- dzgui/managers/mods.py | 3 ++- dzgui/model/servers.py | 3 ++- dzgui/strings/kb.py | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 dzgui/strings/kb.py diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index c96abc2..9baf109 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -45,6 +45,7 @@ from dzgui.strings.dialogs import ( waiting_for_mods, waiting_for_directories, ) +from dzgui.strings import kb from dzgui.strings.server_mods import checkmark, resync from dzgui.util.format import format_mib from dzgui.util.strings import dialog, server_timeout @@ -193,7 +194,9 @@ class ConnectionManager: is_last = self.is_last_server() # TODO: strings - invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"] + invalid_mods = [ + (mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod" + ] prereqs = Prerequisites( name=info.server_name, @@ -298,6 +301,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _connect_steam(self, menu_only: bool) -> None: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index da31385..522c340 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -20,7 +20,7 @@ from dzgui.const.constants import ( from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.format import format_mods from dzgui.util.strings import server_timeout from dzgui.util.symlink import rebuild_symlinks @@ -215,6 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_seconary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index bc5d616..cfb2c0a 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -14,7 +14,7 @@ from dzgui.const.constants import ( ) from dzgui.const.enum import FilterMode, Preferences, ServerTab from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.strings import server_timeout, dialog from dzgui.views.dialogs.generic import ExceptionDialog @@ -420,6 +420,7 @@ class ServerModelManager: # customize statusbar and dialog accordingly if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _push_data(self, data: list[Any]) -> None: diff --git a/dzgui/strings/kb.py b/dzgui/strings/kb.py new file mode 100644 index 0000000..42f300b --- /dev/null +++ b/dzgui/strings/kb.py @@ -0,0 +1,15 @@ +DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration. + +Many third-party DayZ servers use a server rental/hosting provider with DDoS protection. + +This can cause responses from servers to originate from a server other than the one originally queried. + +Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation). + +By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue. + +If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. + +In addition, packets received from server responses are expected to be a standard size: MTU (maximum transmission unit) of 1500. + +Deviation from this may cause your router to discard incoming responses from the server.""" From b300b614a09ed7eccd06247b957fbdd96575bdc2 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:34 +0900 Subject: [PATCH 15/58] fix: typo --- dzgui/managers/mods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 522c340..742873a 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -215,7 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_seconary_text(kb.DZG_006) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: From 7cc8076df1ae989a6328d951b511ecdd87f54bef Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:36:56 +0900 Subject: [PATCH 16/58] chore: backport DebugDialog --- dzgui/views/dialogs/generic.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index f3174eb..e6c98c4 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -169,6 +169,66 @@ class QuitDialog(GenericDialog): self.controller.save_res_and_quit() +class TextBufferDialog(GenericDialog): + def __init__( + self, controller: "Controller", heading: str, secondary: str, text: str + ): + super().__init__( + controller=controller, + text=heading, + mtype=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.NONE, + secondary=secondary, + ) + self.text = text + copy_button = ClipboardButton(controller, self.get_text) + self.add_action_widget(copy_button, Gtk.ResponseType.NONE) + self.add_button("OK", Gtk.ResponseType.OK) + self.connect("response", self._on_response) + + def get_text(self) -> str: + return self.text + + def _on_response( + self, dialog: Self, response: Gtk.ResponseType + ) -> None | Literal[True]: + match response: + case Gtk.ResponseType.OK: + self.destroy() + return None + case Gtk.ResponseType.NONE: + return True + case Gtk.ResponseType.DELETE_EVENT: + self.destroy() + return None + case _: + return None + + +class DebugDialog(TextBufferDialog): + def __init__(self, controller: "Controller", debug: str): + super().__init__( + controller=controller, + heading="Debug", + secondary="Debug args", + text=debug, + ) + scrollable = Gtk.ScrolledWindow( + propagate_natural_height=True, max_content_height=500 + ) + box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) + textview = Gtk.TextView( + wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + ) + textview.set_buffer(Gtk.TextBuffer(text=debug)) + box.pack_start(textview, EXPAND, FILL, 10) + scrollable.add(box) + content = self.get_content_area() + content.set_spacing(0) + content.add(scrollable) + self.show_all() + + class ExceptionDialog(GenericDialog): """ Error dialog with rich traceback. From 5eb89a172d01573d9476cb7688f576b879eb7b52 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:46:02 +0900 Subject: [PATCH 17/58] feat: add DebugDialog to preconnect --- dzgui/api/steam.py | 16 ++++++++++++++++ dzgui/controllers/mc.py | 3 +++ dzgui/managers/connection.py | 8 ++++++++ dzgui/views/pages/preconnect.py | 12 ++++++++++++ 4 files changed, 39 insertions(+) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index c5b0074..6139e9a 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -150,6 +150,22 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]: return hashes +def connect_debug(client: str, addr: str, appid: int, name: str, mods: list[str]) -> str: + concat = concat_mods(mods) + client_args = concat_bash_args(client) + params = [ + "-applaunch", + str(appid), + f"-connect={addr}", + "-nolauncher", + "-nosplash", + "-skipintro", + f"-name={name}", + f"-mod={concat}", + ] + client_args.extend(params) + return " ".join(client_args) + # TODO: set config to name=user, use official server and no mods, # ensure that formatted string is identical to fixture with same hash def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int: diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index b8b69fc..e6af0d6 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -526,6 +526,9 @@ class Controller(GObject.GObject): ind = self.config_man.get_start_tab() self.get_servers().notebook.set_current_page(ind) + def get_debug_args(self) -> str: + return self.connection_man.get_debug_args() + def update_and_load_to_menu(self) -> None: self.connection_man.update_and_connect(menu_only=True) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 9baf109..44cde10 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -13,6 +13,7 @@ from dzgui.api.shortcuts import Shortcuts from dzgui.api.steam import ( connect, + connect_debug, get_app_allows_downloads, get_app_name, get_needs_update, @@ -304,6 +305,13 @@ class ConnectionManager: dialog.set_secondary_text(kb.DZG_006) dialog.run() + def get_debug_args(self) -> str: + addr = f"{self.record.ip}:{self.record.gameport}" + playername = self.controller.query_config(Preferences.NAME) + return connect_debug( + self.client, addr, self.appid, playername, self.remote_mod_ids + ) + def _connect_steam(self, menu_only: bool) -> None: addr = f"{self.record.ip}:{self.record.gameport}" playername = self.controller.query_config(Preferences.NAME) diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 2e07de0..b9f7bd9 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask from dzgui.util.localize import number from dzgui.strings.server_mods import checkmark from dzgui.strings import preconnect +from dzgui.views.dialogs.generic import DebugDialog from dzgui.views.components.frame import HeadingFrame from dzgui.views.trees.tree_server_mods import ServerModTreeView @@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box): spacing=5, ) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + # TODO: + if self.controller.get_prefs().is_debug: + # TODO: strings + debug = Gtk.Button(label="Debug", halign=Gtk.Align.START) + debug.connect("clicked", self._on_debug_clicked) + box.add(debug) for button in self.back, self.ok, self.connect_last: box.add(button) self.button_box.add(box) @@ -221,6 +228,11 @@ class PreConnectionAssistant(Gtk.Box): def _on_connect_last_clicked(self, button: Gtk.Button) -> None: self.controller.update_and_load_to_menu() + def _on_debug_clicked(self, button: Gtk.Button) -> None: + args = self.controller.get_debug_args() + d = DebugDialog(self.controller, args) + d.run() + def _on_ok_clicked(self, button: Gtk.Button) -> None: self.controller.update_and_connect() From 48b0660ebd874a8cc8bce8c33b8d30631c1fc4d8 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:00:28 +0900 Subject: [PATCH 18/58] chore: update strings --- dzgui/strings/dialogs.py | 3 +++ dzgui/strings/preconnect.py | 1 + 2 files changed, 4 insertions(+) diff --git a/dzgui/strings/dialogs.py b/dzgui/strings/dialogs.py index 460250d..3ab9a04 100644 --- a/dzgui/strings/dialogs.py +++ b/dzgui/strings/dialogs.py @@ -20,3 +20,6 @@ ok = "OK" mission_dialog = "Set a mission folder location" custom_mod_dialog = "Set location to a custom mods folder" + +debug_heading = "Debug" +debug_secondary = "Launch arguments" diff --git a/dzgui/strings/preconnect.py b/dzgui/strings/preconnect.py index 9e0f764..274c4b0 100644 --- a/dzgui/strings/preconnect.py +++ b/dzgui/strings/preconnect.py @@ -2,6 +2,7 @@ update_mods = "Update mods and connect" connect = "Connect" connect_last = "Load to menu screen" connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting." +debug = "Debug" back = "Back" cancel = "Cancel" warnings = "Warnings" From a2938c17f501ec78460625e54ed6d124ef8a8a07 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:00:53 +0900 Subject: [PATCH 19/58] chore: subclass ExceptionDialog from TextBufferDialog --- dzgui/views/dialogs/generic.py | 52 +++++++++++----------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index e6c98c4..bf020f2 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -1,6 +1,7 @@ from typing import Literal, Self, TYPE_CHECKING from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL +from dzgui.strings import dialogs from dzgui.util import strings from dzgui.views.components.buttons import ClipboardButton @@ -171,12 +172,17 @@ class QuitDialog(GenericDialog): class TextBufferDialog(GenericDialog): def __init__( - self, controller: "Controller", heading: str, secondary: str, text: str + self, + controller: "Controller", + mtype: Gtk.MessageType, + heading: str, + secondary: str, + text: str, ): super().__init__( controller=controller, text=heading, - mtype=Gtk.MessageType.INFO, + mtype=mtype, buttons=Gtk.ButtonsType.NONE, secondary=secondary, ) @@ -209,8 +215,9 @@ class DebugDialog(TextBufferDialog): def __init__(self, controller: "Controller", debug: str): super().__init__( controller=controller, - heading="Debug", - secondary="Debug args", + mtype=Gtk.MessageType.INFO, + heading=dialogs.debug_heading, + secondary=dialogs.debug_secondary, text=debug, ) scrollable = Gtk.ScrolledWindow( @@ -229,7 +236,7 @@ class DebugDialog(TextBufferDialog): self.show_all() -class ExceptionDialog(GenericDialog): +class ExceptionDialog(TextBufferDialog): """ Error dialog with rich traceback. Usage: @@ -244,13 +251,12 @@ class ExceptionDialog(GenericDialog): def __init__(self, controller: "Controller", trace: str): super().__init__( controller=controller, - text=strings.error_heading, - mtype=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.NONE, + heading=strings.error_heading, secondary=strings.something_wrong, + text=trace, + mtype=Gtk.MessageType.ERROR, ) - self.trace = trace # NOTE: box expands to end of content area scrollable = Gtk.ScrolledWindow( propagate_natural_height=True, max_content_height=500 @@ -264,7 +270,7 @@ class ExceptionDialog(GenericDialog): top_margin=15, bottom_margin=10, ) - textview.set_buffer(Gtk.TextBuffer(text=self.trace)) + textview.set_buffer(Gtk.TextBuffer(text=trace)) box.pack_start(textview, EXPAND, FILL, 0) self.error_details = Gtk.ScrolledWindow( @@ -302,15 +308,7 @@ class ExceptionDialog(GenericDialog): content.set_spacing(0) content.add(scrollable) - copy_button = ClipboardButton(controller, self.get_trace) - self.add_action_widget(copy_button, Gtk.ResponseType.NONE) - self.add_button("OK", Gtk.ResponseType.OK) - self.show_all() - self.ok = self.get_widget_for_response(Gtk.ResponseType.OK) - if self.ok is not None: - self.ok.grab_focus() - self.connect("response", self._on_response) def _on_page_changed( self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int @@ -323,21 +321,3 @@ class ExceptionDialog(GenericDialog): def set_secondary_text(self, text: str) -> None: self.details_buffer.set_text(text) self.error_notebook.set_show_tabs(True) - - def get_trace(self) -> str: - return self.trace - - def _on_response( - self, dialog: Self, response: Gtk.ResponseType - ) -> None | Literal[True]: - match response: - case Gtk.ResponseType.OK: - self.destroy() - return None - case Gtk.ResponseType.NONE: - return True - case Gtk.ResponseType.DELETE_EVENT: - self.destroy() - return None - case _: - return None From 22c9be36a6a97ea184d2a657293d83e2896df0c1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:01:06 +0900 Subject: [PATCH 20/58] chore: add strings to preconnect page --- dzgui/views/pages/preconnect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index b9f7bd9..4d6e6eb 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -121,10 +121,10 @@ class PreConnectionAssistant(Gtk.Box): spacing=5, ) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - # TODO: + + # NOTE: -d flag if self.controller.get_prefs().is_debug: - # TODO: strings - debug = Gtk.Button(label="Debug", halign=Gtk.Align.START) + debug = Gtk.Button(label=preconnect.debug, halign=Gtk.Align.START) debug.connect("clicked", self._on_debug_clicked) box.add(debug) for button in self.back, self.ok, self.connect_last: From 3932a3aed4515e7a6cb117c41e863d39179c02af Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:01:40 +0900 Subject: [PATCH 21/58] chore: drop debug key from prefs enum --- dzgui/const/enum.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 28a4cdf..4e77740 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -76,9 +76,6 @@ class Preferences(EnumWithAttrs): INSTALL = { "key": "auto_install", } - DEBUG = { - "key": "debug", - } DEFAULT = { "key": "default_steam_path", } From 027e6c0781aaec4259cd30ff11a1698ec6b24173 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:02:59 +0900 Subject: [PATCH 22/58] chore: drop unused enums --- dzgui/const/enum.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 4e77740..05a9763 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -73,9 +73,6 @@ class Preferences(EnumWithAttrs): NAME = { "key": "name", } - INSTALL = { - "key": "auto_install", - } DEFAULT = { "key": "default_steam_path", } @@ -88,9 +85,6 @@ class Preferences(EnumWithAttrs): IP_LIST = { "key": "ip_list", } - BRANCH = { - "key": "branch", - } START_TAB = { "key": "start_tab", } From cca1006efbd5ac84a544fcdb3297a1654f18bd05 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:03:08 +0900 Subject: [PATCH 23/58] chore: drop unused method --- dzgui/controllers/mc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index e6af0d6..d8ca3d6 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -114,9 +114,6 @@ class Controller(GObject.GObject): def query_config(self, key: Preferences) -> Any: return self.config_man.lookup(key) - def is_auto_install(self) -> bool: - return bool(self.query_config(Preferences.INSTALL)) - def suppress_signal( self, owner: Any, child: Any, func_name: str, state: bool ) -> None: From 0f59d92bc302dbdca4434b387558fa897693109a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:33:05 +0900 Subject: [PATCH 24/58] chore: drop unused method --- dzgui/managers/mods.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 742873a..8689072 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -213,11 +213,6 @@ class ModManager: func = StoredFunc(self._on_stale_mods_found, stale) self.thread_man.set_cleanup_func(func) - def _server_timeout(self) -> None: - dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_secondary_text(kb.DZG_006) - dialog.run() - def select_colorized(self) -> None: model = self.treeview.get_model() if model is None: From 9f8b760c79616231c54dcf2585e4c04d9c26eb1b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:33:42 +0900 Subject: [PATCH 25/58] fix: defer propagation until size allocation --- dzgui/views/dialogs/generic.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index bf020f2..2257873 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -221,19 +221,19 @@ class DebugDialog(TextBufferDialog): text=debug, ) scrollable = Gtk.ScrolledWindow( - propagate_natural_height=True, max_content_height=500 + propagate_natural_height=False, + min_content_height=200, + max_content_height=200, + margin_bottom=10, ) - box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) textview = Gtk.TextView( - wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10 ) textview.set_buffer(Gtk.TextBuffer(text=debug)) - box.pack_start(textview, EXPAND, FILL, 10) - scrollable.add(box) + scrollable.add(textview) content = self.get_content_area() content.set_spacing(0) content.add(scrollable) - self.show_all() class ExceptionDialog(TextBufferDialog): @@ -308,15 +308,13 @@ class ExceptionDialog(TextBufferDialog): content.set_spacing(0) content.add(scrollable) - self.show_all() - def _on_page_changed( self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: if child == self.error_details: - self.error_details.set_propagate_natural_height(True) + GLib.idle_add(self.error_details.set_propagate_natural_height, True) else: - self.error_details.set_propagate_natural_height(False) + GLib.idle_add(self.error_details.set_propagate_natural_height, False) def set_secondary_text(self, text: str) -> None: self.details_buffer.set_text(text) From 1a3154712f57cd313bf06716375988be7b51fe48 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:36:35 +0900 Subject: [PATCH 26/58] fix: draw dialog outside of init --- dzgui/controllers/mc.py | 2 ++ dzgui/managers/config.py | 1 + dzgui/managers/connection.py | 1 + dzgui/managers/mods.py | 4 +--- dzgui/managers/update.py | 1 + dzgui/model/servers.py | 2 ++ dzgui/views/pages/options.py | 1 + dzgui/views/pages/preconnect.py | 1 + 8 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index d8ca3d6..9effd3c 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -263,6 +263,7 @@ class Controller(GObject.GObject): self.open_page(NotebookPage.LOG) except Exception as e: dialog = ExceptionDialog(self, str(e)) + dialog.show_all() dialog.run() def select_colorized(self) -> None: @@ -296,6 +297,7 @@ class Controller(GObject.GObject): write_diagnostic(self.prefs.paths.config, file) except Exception as e: dialog = ExceptionDialog(self, str(e)) + dialog.show_all() dialog.run() def update_steam_api_key(self, text: str) -> None: diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index ac559b2..9128579 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -178,6 +178,7 @@ class ConfigManager: logger.critical(e) trace = traceback.format_exc() dialog = ExceptionDialog(self.controller, trace) + dialog.show_all() dialog.run() raise e diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 44cde10..fc5b65a 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -303,6 +303,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) dialog.set_secondary_text(kb.DZG_006) + dialog.show_all() dialog.run() def get_debug_args(self) -> str: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 8689072..c04d231 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -20,11 +20,9 @@ from dzgui.const.constants import ( from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory -from dzgui.strings import dialogs, kb +from dzgui.strings import dialogs from dzgui.util.format import format_mods -from dzgui.util.strings import server_timeout from dzgui.util.symlink import rebuild_symlinks -from dzgui.views.dialogs.generic import ExceptionDialog import dzgui.api.pefile as PeFile diff --git a/dzgui/managers/update.py b/dzgui/managers/update.py index 3c0f928..ff64fb4 100644 --- a/dzgui/managers/update.py +++ b/dzgui/managers/update.py @@ -64,4 +64,5 @@ class UpdateManager: def _on_update_failure(self, msg: str) -> None: dialog = ExceptionDialog(self.controller, msg) + dialog.show_all() dialog.run() diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index cfb2c0a..e2c9430 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -405,6 +405,7 @@ class ServerModelManager: if show_dialog: dialog = ExceptionDialog(self.controller, dialogs.load_error_lan) + dialog.show_all() dialog.run() def _cleanup_on_failure(self, show_dialog: bool = True) -> None: @@ -421,6 +422,7 @@ class ServerModelManager: if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) dialog.set_secondary_text(kb.DZG_006) + dialog.show_all() dialog.run() def _push_data(self, data: list[Any]) -> None: diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 277c13e..93341ec 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -430,6 +430,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore # NOTE: re-check in case file was removed by user between runs if prefs.paths.config.is_file() is False: dialog = ExceptionDialog(self.controller, strings.config_not_found) + dialog.show_all() dialog.run() raise OSError(f"Config file '{prefs.paths.config}' not found") diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 4d6e6eb..93b0fb6 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -231,6 +231,7 @@ class PreConnectionAssistant(Gtk.Box): def _on_debug_clicked(self, button: Gtk.Button) -> None: args = self.controller.get_debug_args() d = DebugDialog(self.controller, args) + d.show_all() d.run() def _on_ok_clicked(self, button: Gtk.Button) -> None: From 6ded9a3eec1b3424ca2d90b96bfd8ce2b2e836f1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:59:00 +0900 Subject: [PATCH 27/58] chore: disambiguate buffer setter method --- dzgui/managers/connection.py | 2 +- dzgui/model/servers.py | 2 +- dzgui/views/dialogs/generic.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index fc5b65a..7d2047e 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -302,7 +302,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_secondary_text(kb.DZG_006) + dialog.set_details_buffer(kb.DZG_006) dialog.show_all() dialog.run() diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index e2c9430..a429ffb 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -421,7 +421,7 @@ class ServerModelManager: # customize statusbar and dialog accordingly if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_secondary_text(kb.DZG_006) + dialog.set_details_buffer(kb.DZG_006) dialog.show_all() dialog.run() diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 2257873..4e871ff 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -316,6 +316,6 @@ class ExceptionDialog(TextBufferDialog): else: GLib.idle_add(self.error_details.set_propagate_natural_height, False) - def set_secondary_text(self, text: str) -> None: + def set_details_buffer(self, text: str) -> None: self.details_buffer.set_text(text) self.error_notebook.set_show_tabs(True) From 5b83a26f6a7959bb27137b1e56bbef35e8b9c7aa Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:44:55 +0900 Subject: [PATCH 28/58] chore: reword remarks on MTU --- docs/source/kb.rst | 4 ++-- dzgui/strings/kb.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/source/kb.rst b/docs/source/kb.rst index afaa68f..7ab8bb2 100644 --- a/docs/source/kb.rst +++ b/docs/source/kb.rst @@ -16,8 +16,8 @@ By contrast, wireless routers and enterprise-grade routers may be less likely to If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. -In addition, packets sent from server responses are expected to be a standard size (see warning below). -Deviation from this may cause your router to discard incoming responses from the server. +In addition, a uniform packet size is expected. If your router's MTU setting differs from the default of 1,500, +it could lead to incoming responses being discarded (see warning below). .. important:: diff --git a/dzgui/strings/kb.py b/dzgui/strings/kb.py index 42f300b..68ad2e5 100644 --- a/dzgui/strings/kb.py +++ b/dzgui/strings/kb.py @@ -10,6 +10,4 @@ By contrast, wireless routers and enterprise-grade routers may be less likely to If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. -In addition, packets received from server responses are expected to be a standard size: MTU (maximum transmission unit) of 1500. - -Deviation from this may cause your router to discard incoming responses from the server.""" +In addition, a uniform packet size is expected. If your router's MTU (maximum transmission unit) setting differs from the default of 1500, it could lead to incoming responses being discarded.""" From 2b394553b31927a610716f19ba777ff7ecbd0124 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:55:43 +0900 Subject: [PATCH 29/58] chore: normalize docstring --- dzgui/managers/config.py | 1 - dzgui/views/dialogs/generic.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 9128579..576f809 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -180,7 +180,6 @@ class ConfigManager: dialog = ExceptionDialog(self.controller, trace) dialog.show_all() dialog.run() - raise e def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None: columns = tv.get_columns() diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 4e871ff..b504955 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -245,6 +245,7 @@ class ExceptionDialog(TextBufferDialog): except Exception: trace = traceback.format_exc() dialog = ExceptionDialog(Controller, trace) + dialog.show_all() dialog.run() """ From 203cf0da7fc9cba869c8a9966b5541a0c90617fc Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:53:06 +0900 Subject: [PATCH 30/58] chore: abstract strings --- dzgui/strings/changelog.py | 2 ++ dzgui/views/pages/changelog.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 dzgui/strings/changelog.py diff --git a/dzgui/strings/changelog.py b/dzgui/strings/changelog.py new file mode 100644 index 0000000..cc637ea --- /dev/null +++ b/dzgui/strings/changelog.py @@ -0,0 +1,2 @@ +collapse_all = "Collapse all" +expand_all = "Expand all" diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 35d6e48..108912c 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from importlib import resources from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH +from dzgui.strings import changelog as clog from dzgui.util.strings import missing_changelog from dzgui.util.format import format_pango from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin @@ -32,7 +33,6 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig logger.critical(e) changelog = missing_changelog - # FIXME: wrap long text self.controller = controller self.box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10 @@ -40,7 +40,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig self.add(self.box) expand_all = Gtk.Button( - label="Expand all", halign=Gtk.Align.START, margin_start=20 + label=clog.expand_all, halign=Gtk.Align.START, margin_start=20 ) expand_all.connect("clicked", self._on_expand_all_clicked) self.box.add(expand_all) @@ -63,7 +63,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig if expander.get_expanded() == self.expanded: continue expander.activate() - label = "Collapse all" if self.expanded else "Expand all" + label = clog.collapse_all if self.expanded else clog.expand_all button.set_label(label) def grab_content_area(self) -> None: From 05fab5774eba64d033d3240d2eff828e31accea2 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:59:13 +0900 Subject: [PATCH 31/58] chore: add test for changelog markdown headers --- tests/test_changelog.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 3eae186..acc6019 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,20 +1,34 @@ +import re import pytest -from importlib import resources -from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH +from pathlib import Path @pytest.fixture -def changelog(): - path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH) - return path +def changelog(request) -> None: + root = request.config.rootpath + changelog = Path(root).joinpath("CHANGELOG.md").read_text() + return changelog -def test_headings(changelog): - with open(changelog, "r") as f: - lines = f.readlines() - for line in lines: - if line.startswith("#"): - pass - # TODO: use regex - pass +def count_hash(line: str) -> int: + cnt = 0 + for c in line: + if c == "#": + cnt += 1 + return cnt + + +@pytest.mark.FOO +def test_changelog_prefix(changelog) -> None: + r = r".*(\[.*\]).*" + lines = changelog.splitlines() + sort = sorted(lines) + match = [line for line in sort if line.startswith("#")] + for m in match: + if "Changelog" in m: + assert count_hash(m) == 1 + elif re.match(r, m) is not None: + assert count_hash(m) == 2 + else: + assert count_hash(m) == 3 From b5105ca9f9c00e6f61bb5382262100e80bc25f20 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:03:27 +0900 Subject: [PATCH 32/58] chore: drop placeholder mark --- tests/test_changelog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index acc6019..e897571 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -19,7 +19,6 @@ def count_hash(line: str) -> int: return cnt -@pytest.mark.FOO def test_changelog_prefix(changelog) -> None: r = r".*(\[.*\]).*" lines = changelog.splitlines() From ddc18e5705708afdf0d42bd688136f727edb7228 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:22:14 +0900 Subject: [PATCH 33/58] feat: add notebook to ExceptionDialog --- dzgui/views/dialogs/generic.py | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index c9b890c..19e6b44 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -196,17 +196,49 @@ class ExceptionDialog(GenericDialog): propagate_natural_height=True, max_content_height=500 ) box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) - # TODO: wrap/truncate long messages textview = Gtk.TextView( - wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + wrap_mode=Gtk.WrapMode.WORD, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) - box.pack_start(textview, EXPAND, FILL, 10) - scrollable.add(box) + box.pack_start(textview, EXPAND, FILL, 0) + + self.error_details = Gtk.ScrolledWindow( + overlay_scrolling=False, + max_content_height=150, + propagate_natural_height=False, + ) + details_box = Gtk.Box( + hexpand=True, + vexpand=True, + margin_right=5, + orientation=Gtk.Orientation.VERTICAL, + ) + + self.details_buffer = Gtk.TextBuffer() + details_textview = Gtk.TextView( + wrap_mode=Gtk.WrapMode.WORD_CHAR, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, + ) + + details_textview.set_buffer(self.details_buffer) + details_box.pack_start(details_textview, EXPAND, FILL, 10) + self.error_details.add(details_box) + + self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15) + self.error_notebook.append_page(box, Gtk.Label(label="Error")) + self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details")) + self.error_notebook.connect("switch-page", self._on_page_changed) + scrollable.add(self.error_notebook) content = self.get_content_area() content.set_spacing(0) - # TODO: padding around top of content area when traceback is long content.add(scrollable) copy_button = ClipboardButton(controller, self.get_trace) @@ -219,6 +251,18 @@ class ExceptionDialog(GenericDialog): self.ok.grab_focus() self.connect("response", self._on_response) + def _on_page_changed( + self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + ) -> None: + if child == self.error_details: + child.set_propagate_natural_height(True) + else: + self.error_details.set_propagate_natural_height(False) + + def set_secondary_text(self, text: str) -> None: + self.details_buffer.set_text(text) + self.error_notebook.set_show_tabs(True) + def get_trace(self) -> str: return self.trace From 97ddbcdf0a12a0f6d59dd13ed8170f973ec8b942 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:26:42 +0900 Subject: [PATCH 34/58] fix: clear typehinting errors --- dzgui/views/dialogs/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 19e6b44..16d0e8e 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -252,10 +252,10 @@ class ExceptionDialog(GenericDialog): self.connect("response", self._on_response) def _on_page_changed( - self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: if child == self.error_details: - child.set_propagate_natural_height(True) + self.error_details.set_propagate_natural_height(True) else: self.error_details.set_propagate_natural_height(False) From c64631cdcab2fa475d4d660e7a0b45c2ffa69ba7 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:38:45 +0900 Subject: [PATCH 35/58] fix: add bottom padding --- dzgui/views/dialogs/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 16d0e8e..f3174eb 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -202,6 +202,7 @@ class ExceptionDialog(GenericDialog): left_margin=10, right_margin=10, top_margin=15, + bottom_margin=10, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) box.pack_start(textview, EXPAND, FILL, 0) From 0dfcc5a38711bc1c8e0e129eaab44120be0ceb4a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:02 +0900 Subject: [PATCH 36/58] feat: internal knowledeat: add knowledge base text to specific exception dialogs --- dzgui/managers/connection.py | 6 +++++- dzgui/managers/mods.py | 3 ++- dzgui/model/servers.py | 3 ++- dzgui/strings/kb.py | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 dzgui/strings/kb.py diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index c96abc2..9baf109 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -45,6 +45,7 @@ from dzgui.strings.dialogs import ( waiting_for_mods, waiting_for_directories, ) +from dzgui.strings import kb from dzgui.strings.server_mods import checkmark, resync from dzgui.util.format import format_mib from dzgui.util.strings import dialog, server_timeout @@ -193,7 +194,9 @@ class ConnectionManager: is_last = self.is_last_server() # TODO: strings - invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"] + invalid_mods = [ + (mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod" + ] prereqs = Prerequisites( name=info.server_name, @@ -298,6 +301,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _connect_steam(self, menu_only: bool) -> None: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index da31385..522c340 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -20,7 +20,7 @@ from dzgui.const.constants import ( from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.format import format_mods from dzgui.util.strings import server_timeout from dzgui.util.symlink import rebuild_symlinks @@ -215,6 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_seconary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index bc5d616..cfb2c0a 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -14,7 +14,7 @@ from dzgui.const.constants import ( ) from dzgui.const.enum import FilterMode, Preferences, ServerTab from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.strings import server_timeout, dialog from dzgui.views.dialogs.generic import ExceptionDialog @@ -420,6 +420,7 @@ class ServerModelManager: # customize statusbar and dialog accordingly if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _push_data(self, data: list[Any]) -> None: diff --git a/dzgui/strings/kb.py b/dzgui/strings/kb.py new file mode 100644 index 0000000..42f300b --- /dev/null +++ b/dzgui/strings/kb.py @@ -0,0 +1,15 @@ +DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration. + +Many third-party DayZ servers use a server rental/hosting provider with DDoS protection. + +This can cause responses from servers to originate from a server other than the one originally queried. + +Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation). + +By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue. + +If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. + +In addition, packets received from server responses are expected to be a standard size: MTU (maximum transmission unit) of 1500. + +Deviation from this may cause your router to discard incoming responses from the server.""" From de4e74daa15db55d06cce44c4f09d8719acc264f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:34 +0900 Subject: [PATCH 37/58] fix: typo --- dzgui/managers/mods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 522c340..742873a 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -215,7 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_seconary_text(kb.DZG_006) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: From b153ab875ef4a3d7b8148347c7a057f56a423d6e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:36:56 +0900 Subject: [PATCH 38/58] chore: backport DebugDialog --- dzgui/views/dialogs/generic.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index f3174eb..e6c98c4 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -169,6 +169,66 @@ class QuitDialog(GenericDialog): self.controller.save_res_and_quit() +class TextBufferDialog(GenericDialog): + def __init__( + self, controller: "Controller", heading: str, secondary: str, text: str + ): + super().__init__( + controller=controller, + text=heading, + mtype=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.NONE, + secondary=secondary, + ) + self.text = text + copy_button = ClipboardButton(controller, self.get_text) + self.add_action_widget(copy_button, Gtk.ResponseType.NONE) + self.add_button("OK", Gtk.ResponseType.OK) + self.connect("response", self._on_response) + + def get_text(self) -> str: + return self.text + + def _on_response( + self, dialog: Self, response: Gtk.ResponseType + ) -> None | Literal[True]: + match response: + case Gtk.ResponseType.OK: + self.destroy() + return None + case Gtk.ResponseType.NONE: + return True + case Gtk.ResponseType.DELETE_EVENT: + self.destroy() + return None + case _: + return None + + +class DebugDialog(TextBufferDialog): + def __init__(self, controller: "Controller", debug: str): + super().__init__( + controller=controller, + heading="Debug", + secondary="Debug args", + text=debug, + ) + scrollable = Gtk.ScrolledWindow( + propagate_natural_height=True, max_content_height=500 + ) + box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) + textview = Gtk.TextView( + wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + ) + textview.set_buffer(Gtk.TextBuffer(text=debug)) + box.pack_start(textview, EXPAND, FILL, 10) + scrollable.add(box) + content = self.get_content_area() + content.set_spacing(0) + content.add(scrollable) + self.show_all() + + class ExceptionDialog(GenericDialog): """ Error dialog with rich traceback. From b1f189bff8e59286922069aaa0f012a2bbd2fede Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:22:14 +0900 Subject: [PATCH 39/58] feat: add notebook to ExceptionDialog --- dzgui/views/dialogs/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index e6c98c4..3a78f5a 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -316,9 +316,9 @@ class ExceptionDialog(GenericDialog): self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: if child == self.error_details: - self.error_details.set_propagate_natural_height(True) + GLib.idle_add(self.error_details.set_propagate_natural_height, True) else: - self.error_details.set_propagate_natural_height(False) + GLib.idle_add(self.error_details.set_propagate_natural_height, False) def set_secondary_text(self, text: str) -> None: self.details_buffer.set_text(text) From 4eaedd0cddd901c311ed121a6f73b30e088f64ef Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:46:02 +0900 Subject: [PATCH 40/58] feat: add DebugDialog to preconnect --- dzgui/api/steam.py | 16 ++++++++++++++++ dzgui/controllers/mc.py | 3 +++ dzgui/managers/connection.py | 8 ++++++++ dzgui/views/pages/preconnect.py | 12 ++++++++++++ 4 files changed, 39 insertions(+) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index c5b0074..6139e9a 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -150,6 +150,22 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]: return hashes +def connect_debug(client: str, addr: str, appid: int, name: str, mods: list[str]) -> str: + concat = concat_mods(mods) + client_args = concat_bash_args(client) + params = [ + "-applaunch", + str(appid), + f"-connect={addr}", + "-nolauncher", + "-nosplash", + "-skipintro", + f"-name={name}", + f"-mod={concat}", + ] + client_args.extend(params) + return " ".join(client_args) + # TODO: set config to name=user, use official server and no mods, # ensure that formatted string is identical to fixture with same hash def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int: diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index b8b69fc..e6af0d6 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -526,6 +526,9 @@ class Controller(GObject.GObject): ind = self.config_man.get_start_tab() self.get_servers().notebook.set_current_page(ind) + def get_debug_args(self) -> str: + return self.connection_man.get_debug_args() + def update_and_load_to_menu(self) -> None: self.connection_man.update_and_connect(menu_only=True) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 9baf109..44cde10 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -13,6 +13,7 @@ from dzgui.api.shortcuts import Shortcuts from dzgui.api.steam import ( connect, + connect_debug, get_app_allows_downloads, get_app_name, get_needs_update, @@ -304,6 +305,13 @@ class ConnectionManager: dialog.set_secondary_text(kb.DZG_006) dialog.run() + def get_debug_args(self) -> str: + addr = f"{self.record.ip}:{self.record.gameport}" + playername = self.controller.query_config(Preferences.NAME) + return connect_debug( + self.client, addr, self.appid, playername, self.remote_mod_ids + ) + def _connect_steam(self, menu_only: bool) -> None: addr = f"{self.record.ip}:{self.record.gameport}" playername = self.controller.query_config(Preferences.NAME) diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 2e07de0..b9f7bd9 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask from dzgui.util.localize import number from dzgui.strings.server_mods import checkmark from dzgui.strings import preconnect +from dzgui.views.dialogs.generic import DebugDialog from dzgui.views.components.frame import HeadingFrame from dzgui.views.trees.tree_server_mods import ServerModTreeView @@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box): spacing=5, ) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + # TODO: + if self.controller.get_prefs().is_debug: + # TODO: strings + debug = Gtk.Button(label="Debug", halign=Gtk.Align.START) + debug.connect("clicked", self._on_debug_clicked) + box.add(debug) for button in self.back, self.ok, self.connect_last: box.add(button) self.button_box.add(box) @@ -221,6 +228,11 @@ class PreConnectionAssistant(Gtk.Box): def _on_connect_last_clicked(self, button: Gtk.Button) -> None: self.controller.update_and_load_to_menu() + def _on_debug_clicked(self, button: Gtk.Button) -> None: + args = self.controller.get_debug_args() + d = DebugDialog(self.controller, args) + d.run() + def _on_ok_clicked(self, button: Gtk.Button) -> None: self.controller.update_and_connect() From 1332026c8cc326ec9d8d5f432dd4a57b1598edf3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:00:28 +0900 Subject: [PATCH 41/58] chore: update strings --- dzgui/strings/dialogs.py | 3 +++ dzgui/strings/preconnect.py | 1 + 2 files changed, 4 insertions(+) diff --git a/dzgui/strings/dialogs.py b/dzgui/strings/dialogs.py index 460250d..3ab9a04 100644 --- a/dzgui/strings/dialogs.py +++ b/dzgui/strings/dialogs.py @@ -20,3 +20,6 @@ ok = "OK" mission_dialog = "Set a mission folder location" custom_mod_dialog = "Set location to a custom mods folder" + +debug_heading = "Debug" +debug_secondary = "Launch arguments" diff --git a/dzgui/strings/preconnect.py b/dzgui/strings/preconnect.py index 9e0f764..274c4b0 100644 --- a/dzgui/strings/preconnect.py +++ b/dzgui/strings/preconnect.py @@ -2,6 +2,7 @@ update_mods = "Update mods and connect" connect = "Connect" connect_last = "Load to menu screen" connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting." +debug = "Debug" back = "Back" cancel = "Cancel" warnings = "Warnings" From cf850c8f34c01652ddf67af8670a34a8b8446380 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:00:53 +0900 Subject: [PATCH 42/58] chore: subclass ExceptionDialog from TextBufferDialog --- dzgui/views/dialogs/generic.py | 52 +++++++++++----------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 3a78f5a..df71bc6 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -1,6 +1,7 @@ from typing import Literal, Self, TYPE_CHECKING from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL +from dzgui.strings import dialogs from dzgui.util import strings from dzgui.views.components.buttons import ClipboardButton @@ -171,12 +172,17 @@ class QuitDialog(GenericDialog): class TextBufferDialog(GenericDialog): def __init__( - self, controller: "Controller", heading: str, secondary: str, text: str + self, + controller: "Controller", + mtype: Gtk.MessageType, + heading: str, + secondary: str, + text: str, ): super().__init__( controller=controller, text=heading, - mtype=Gtk.MessageType.INFO, + mtype=mtype, buttons=Gtk.ButtonsType.NONE, secondary=secondary, ) @@ -209,8 +215,9 @@ class DebugDialog(TextBufferDialog): def __init__(self, controller: "Controller", debug: str): super().__init__( controller=controller, - heading="Debug", - secondary="Debug args", + mtype=Gtk.MessageType.INFO, + heading=dialogs.debug_heading, + secondary=dialogs.debug_secondary, text=debug, ) scrollable = Gtk.ScrolledWindow( @@ -229,7 +236,7 @@ class DebugDialog(TextBufferDialog): self.show_all() -class ExceptionDialog(GenericDialog): +class ExceptionDialog(TextBufferDialog): """ Error dialog with rich traceback. Usage: @@ -244,13 +251,12 @@ class ExceptionDialog(GenericDialog): def __init__(self, controller: "Controller", trace: str): super().__init__( controller=controller, - text=strings.error_heading, - mtype=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.NONE, + heading=strings.error_heading, secondary=strings.something_wrong, + text=trace, + mtype=Gtk.MessageType.ERROR, ) - self.trace = trace # NOTE: box expands to end of content area scrollable = Gtk.ScrolledWindow( propagate_natural_height=True, max_content_height=500 @@ -264,7 +270,7 @@ class ExceptionDialog(GenericDialog): top_margin=15, bottom_margin=10, ) - textview.set_buffer(Gtk.TextBuffer(text=self.trace)) + textview.set_buffer(Gtk.TextBuffer(text=trace)) box.pack_start(textview, EXPAND, FILL, 0) self.error_details = Gtk.ScrolledWindow( @@ -302,15 +308,7 @@ class ExceptionDialog(GenericDialog): content.set_spacing(0) content.add(scrollable) - copy_button = ClipboardButton(controller, self.get_trace) - self.add_action_widget(copy_button, Gtk.ResponseType.NONE) - self.add_button("OK", Gtk.ResponseType.OK) - self.show_all() - self.ok = self.get_widget_for_response(Gtk.ResponseType.OK) - if self.ok is not None: - self.ok.grab_focus() - self.connect("response", self._on_response) def _on_page_changed( self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int @@ -323,21 +321,3 @@ class ExceptionDialog(GenericDialog): def set_secondary_text(self, text: str) -> None: self.details_buffer.set_text(text) self.error_notebook.set_show_tabs(True) - - def get_trace(self) -> str: - return self.trace - - def _on_response( - self, dialog: Self, response: Gtk.ResponseType - ) -> None | Literal[True]: - match response: - case Gtk.ResponseType.OK: - self.destroy() - return None - case Gtk.ResponseType.NONE: - return True - case Gtk.ResponseType.DELETE_EVENT: - self.destroy() - return None - case _: - return None From 37c3937aee406b32f5b48921fb6fa55f8fbe2afa Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:01:06 +0900 Subject: [PATCH 43/58] chore: add strings to preconnect page --- dzgui/views/pages/preconnect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index b9f7bd9..4d6e6eb 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -121,10 +121,10 @@ class PreConnectionAssistant(Gtk.Box): spacing=5, ) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) - # TODO: + + # NOTE: -d flag if self.controller.get_prefs().is_debug: - # TODO: strings - debug = Gtk.Button(label="Debug", halign=Gtk.Align.START) + debug = Gtk.Button(label=preconnect.debug, halign=Gtk.Align.START) debug.connect("clicked", self._on_debug_clicked) box.add(debug) for button in self.back, self.ok, self.connect_last: From cb9ffb5b996cf355b6b49742f844116053a1790a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:01:40 +0900 Subject: [PATCH 44/58] chore: drop debug key from prefs enum --- dzgui/const/enum.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 28a4cdf..4e77740 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -76,9 +76,6 @@ class Preferences(EnumWithAttrs): INSTALL = { "key": "auto_install", } - DEBUG = { - "key": "debug", - } DEFAULT = { "key": "default_steam_path", } From 9ec3fa09e3aad09dadc1c3f19b8bf179161cf70e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:02:59 +0900 Subject: [PATCH 45/58] chore: drop unused enums --- dzgui/const/enum.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 4e77740..05a9763 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -73,9 +73,6 @@ class Preferences(EnumWithAttrs): NAME = { "key": "name", } - INSTALL = { - "key": "auto_install", - } DEFAULT = { "key": "default_steam_path", } @@ -88,9 +85,6 @@ class Preferences(EnumWithAttrs): IP_LIST = { "key": "ip_list", } - BRANCH = { - "key": "branch", - } START_TAB = { "key": "start_tab", } From 52b18f710f21955090a8480c8e837ef202c3f3c8 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:03:08 +0900 Subject: [PATCH 46/58] chore: drop unused method --- dzgui/controllers/mc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index e6af0d6..d8ca3d6 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -114,9 +114,6 @@ class Controller(GObject.GObject): def query_config(self, key: Preferences) -> Any: return self.config_man.lookup(key) - def is_auto_install(self) -> bool: - return bool(self.query_config(Preferences.INSTALL)) - def suppress_signal( self, owner: Any, child: Any, func_name: str, state: bool ) -> None: From bd022ea1d8063084c893159a79e6ac5d3204bb59 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:33:05 +0900 Subject: [PATCH 47/58] chore: drop unused method --- dzgui/managers/mods.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 742873a..8689072 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -213,11 +213,6 @@ class ModManager: func = StoredFunc(self._on_stale_mods_found, stale) self.thread_man.set_cleanup_func(func) - def _server_timeout(self) -> None: - dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_secondary_text(kb.DZG_006) - dialog.run() - def select_colorized(self) -> None: model = self.treeview.get_model() if model is None: From ec004aad5dc7415a978a1018862191ac45c9ce4d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:33:42 +0900 Subject: [PATCH 48/58] fix: defer propagation until size allocation --- dzgui/views/dialogs/generic.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index df71bc6..2257873 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -221,19 +221,19 @@ class DebugDialog(TextBufferDialog): text=debug, ) scrollable = Gtk.ScrolledWindow( - propagate_natural_height=True, max_content_height=500 + propagate_natural_height=False, + min_content_height=200, + max_content_height=200, + margin_bottom=10, ) - box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) textview = Gtk.TextView( - wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10 + wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10 ) textview.set_buffer(Gtk.TextBuffer(text=debug)) - box.pack_start(textview, EXPAND, FILL, 10) - scrollable.add(box) + scrollable.add(textview) content = self.get_content_area() content.set_spacing(0) content.add(scrollable) - self.show_all() class ExceptionDialog(TextBufferDialog): @@ -308,8 +308,6 @@ class ExceptionDialog(TextBufferDialog): content.set_spacing(0) content.add(scrollable) - self.show_all() - def _on_page_changed( self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: From e7ff025605daad6c761fb9e841680ea45c968931 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:36:35 +0900 Subject: [PATCH 49/58] fix: draw dialog outside of init --- dzgui/controllers/mc.py | 2 ++ dzgui/managers/config.py | 1 + dzgui/managers/connection.py | 1 + dzgui/managers/mods.py | 4 +--- dzgui/managers/update.py | 1 + dzgui/model/servers.py | 2 ++ dzgui/views/pages/options.py | 1 + dzgui/views/pages/preconnect.py | 1 + 8 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index d8ca3d6..9effd3c 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -263,6 +263,7 @@ class Controller(GObject.GObject): self.open_page(NotebookPage.LOG) except Exception as e: dialog = ExceptionDialog(self, str(e)) + dialog.show_all() dialog.run() def select_colorized(self) -> None: @@ -296,6 +297,7 @@ class Controller(GObject.GObject): write_diagnostic(self.prefs.paths.config, file) except Exception as e: dialog = ExceptionDialog(self, str(e)) + dialog.show_all() dialog.run() def update_steam_api_key(self, text: str) -> None: diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index ac559b2..9128579 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -178,6 +178,7 @@ class ConfigManager: logger.critical(e) trace = traceback.format_exc() dialog = ExceptionDialog(self.controller, trace) + dialog.show_all() dialog.run() raise e diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 44cde10..fc5b65a 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -303,6 +303,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) dialog.set_secondary_text(kb.DZG_006) + dialog.show_all() dialog.run() def get_debug_args(self) -> str: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 8689072..c04d231 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -20,11 +20,9 @@ from dzgui.const.constants import ( from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory -from dzgui.strings import dialogs, kb +from dzgui.strings import dialogs from dzgui.util.format import format_mods -from dzgui.util.strings import server_timeout from dzgui.util.symlink import rebuild_symlinks -from dzgui.views.dialogs.generic import ExceptionDialog import dzgui.api.pefile as PeFile diff --git a/dzgui/managers/update.py b/dzgui/managers/update.py index 3c0f928..ff64fb4 100644 --- a/dzgui/managers/update.py +++ b/dzgui/managers/update.py @@ -64,4 +64,5 @@ class UpdateManager: def _on_update_failure(self, msg: str) -> None: dialog = ExceptionDialog(self.controller, msg) + dialog.show_all() dialog.run() diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index cfb2c0a..e2c9430 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -405,6 +405,7 @@ class ServerModelManager: if show_dialog: dialog = ExceptionDialog(self.controller, dialogs.load_error_lan) + dialog.show_all() dialog.run() def _cleanup_on_failure(self, show_dialog: bool = True) -> None: @@ -421,6 +422,7 @@ class ServerModelManager: if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) dialog.set_secondary_text(kb.DZG_006) + dialog.show_all() dialog.run() def _push_data(self, data: list[Any]) -> None: diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 277c13e..93341ec 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -430,6 +430,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore # NOTE: re-check in case file was removed by user between runs if prefs.paths.config.is_file() is False: dialog = ExceptionDialog(self.controller, strings.config_not_found) + dialog.show_all() dialog.run() raise OSError(f"Config file '{prefs.paths.config}' not found") diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 4d6e6eb..93b0fb6 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -231,6 +231,7 @@ class PreConnectionAssistant(Gtk.Box): def _on_debug_clicked(self, button: Gtk.Button) -> None: args = self.controller.get_debug_args() d = DebugDialog(self.controller, args) + d.show_all() d.run() def _on_ok_clicked(self, button: Gtk.Button) -> None: From 17b1a757450f48f5a28d8c556897de55ddd6cc5b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:59:00 +0900 Subject: [PATCH 50/58] chore: disambiguate buffer setter method --- dzgui/managers/connection.py | 2 +- dzgui/model/servers.py | 2 +- dzgui/views/dialogs/generic.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index fc5b65a..7d2047e 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -302,7 +302,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_secondary_text(kb.DZG_006) + dialog.set_details_buffer(kb.DZG_006) dialog.show_all() dialog.run() diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index e2c9430..a429ffb 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -421,7 +421,7 @@ class ServerModelManager: # customize statusbar and dialog accordingly if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_secondary_text(kb.DZG_006) + dialog.set_details_buffer(kb.DZG_006) dialog.show_all() dialog.run() diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 2257873..4e871ff 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -316,6 +316,6 @@ class ExceptionDialog(TextBufferDialog): else: GLib.idle_add(self.error_details.set_propagate_natural_height, False) - def set_secondary_text(self, text: str) -> None: + def set_details_buffer(self, text: str) -> None: self.details_buffer.set_text(text) self.error_notebook.set_show_tabs(True) From e9d064820ea5d89de781eea95a433f12045c719b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:44:55 +0900 Subject: [PATCH 51/58] chore: reword remarks on MTU --- docs/source/kb.rst | 4 ++-- dzgui/strings/kb.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/source/kb.rst b/docs/source/kb.rst index afaa68f..7ab8bb2 100644 --- a/docs/source/kb.rst +++ b/docs/source/kb.rst @@ -16,8 +16,8 @@ By contrast, wireless routers and enterprise-grade routers may be less likely to If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. -In addition, packets sent from server responses are expected to be a standard size (see warning below). -Deviation from this may cause your router to discard incoming responses from the server. +In addition, a uniform packet size is expected. If your router's MTU setting differs from the default of 1,500, +it could lead to incoming responses being discarded (see warning below). .. important:: diff --git a/dzgui/strings/kb.py b/dzgui/strings/kb.py index 42f300b..68ad2e5 100644 --- a/dzgui/strings/kb.py +++ b/dzgui/strings/kb.py @@ -10,6 +10,4 @@ By contrast, wireless routers and enterprise-grade routers may be less likely to If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port. -In addition, packets received from server responses are expected to be a standard size: MTU (maximum transmission unit) of 1500. - -Deviation from this may cause your router to discard incoming responses from the server.""" +In addition, a uniform packet size is expected. If your router's MTU (maximum transmission unit) setting differs from the default of 1500, it could lead to incoming responses being discarded.""" From 0d5e190b75f924af44273e4952baf6c5675950f1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:55:43 +0900 Subject: [PATCH 52/58] chore: normalize docstring --- dzgui/managers/config.py | 1 - dzgui/views/dialogs/generic.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 9128579..576f809 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -180,7 +180,6 @@ class ConfigManager: dialog = ExceptionDialog(self.controller, trace) dialog.show_all() dialog.run() - raise e def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None: columns = tv.get_columns() diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 4e871ff..b504955 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -245,6 +245,7 @@ class ExceptionDialog(TextBufferDialog): except Exception: trace = traceback.format_exc() dialog = ExceptionDialog(Controller, trace) + dialog.show_all() dialog.run() """ From 93d870b5bf0a58aae4dab2a2a4fb3a57e914eb7c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:13:37 +0900 Subject: [PATCH 53/58] feat: uninstall wizard WIP --- dzgui/api/shortcuts.py | 7 + dzgui/main.py | 24 ++-- dzgui/strings/uninstall.py | 38 ++++++ dzgui/views/dialogs/uninstall.py | 226 +++++++++++++++++++++++++++++++ dzgui/views/dialogs/wizard.py | 17 ++- 5 files changed, 293 insertions(+), 19 deletions(-) create mode 100644 dzgui/strings/uninstall.py create mode 100644 dzgui/views/dialogs/uninstall.py diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 154645e..50c1d33 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -160,6 +160,13 @@ class Shortcuts: NEW_ENTRY["tags"] = {} return NEW_ENTRY + def delete_shortcut(self, start_path: Path) -> None: + for s in self.shortcuts["shortcuts"]: + if self.shortcuts[s]["StartDir"] == str(start_path): + del self.shortcuts[s] + break + self.save_shortcuts() + def save_shortcuts(self) -> None: try: backup = self.shortcuts_path.with_suffix(".vdf.bak") diff --git a/dzgui/main.py b/dzgui/main.py index 731f58e..ff59ba8 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -8,30 +8,26 @@ from dzgui.init.prefix import get_version from dzgui.util.map_count import set_map_count from dzgui.util.strings import flags +from dzgui.views.dialogs.uninstall import UninstallWizard +from dzgui.config.xdg import get_xdg_paths + parser = argparse.ArgumentParser(description=flags.description) -parser.add_argument("-d", "--debug", action="store_true", help=flags.debug) -parser.add_argument("-m", "--map", action="store_true", help=flags.map_count) -parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall) -parser.add_argument("-v", "--version", action="store_true", help=flags.version) +group = parser.add_mutually_exclusive_group() +group.add_argument("-d", "--debug", action="store_true", help=flags.debug) +group.add_argument("-m", "--map", action="store_true", help=flags.map_count) +group.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall) +group.add_argument("-v", "--version", action="store_true", help=flags.version) args = parser.parse_args() -def uninstall() -> None: - # TODO: uninstall data files (-u) - # -u removes state, log, freedesktop - # XDG_SHARE_HOME/dzgui - # XDG_STATE_HOME/dzgui - # XDG_DATA_HOME/dzgui - pass - - def main() -> None: # TODO: isolate single flags if args.version is True: print(get_version()) sys.exit(0) if args.uninstall is True: - uninstall() + paths = get_xdg_paths() + UninstallWizard(False, paths) sys.exit(0) if args.debug is True: warnings.filterwarnings("default", category=DeprecationWarning) diff --git a/dzgui/strings/uninstall.py b/dzgui/strings/uninstall.py new file mode 100644 index 0000000..676392f --- /dev/null +++ b/dzgui/strings/uninstall.py @@ -0,0 +1,38 @@ +pg1_title = "Choose uninstall targets" +pg1_heading = "Choose uninstall targets" +pg1_blurb = "Here you can choose which DZGUI configuration/state files to remove. " + +pg2_title = "Complete" +pg2_heading = "Uninstall complete" +pg2_blurb = "Finished removing the selected files. See the system log for errors." + +config_label = "Configuration files" +config_details = ( + "User preferences and API key. " + "Keep this file if you want to import it again later." +) +desktop_label = "Desktop shortcut" +desktop_details = "Shortcut set on the desktop, if applicable." + +start_menu_label = "Start menu shortcut" +start_menu_details = ( + "Shortcut set under the Games category of the start menu, if applicable." +) +state_label = "Long-term state files" +state_details = ( + "UI size, filter preferences, column size, server notes, and mod signatures." +) +steam_shortcut = "Steam shortcut" +steam_details = "Shortcut to DZGUI installed within Steam, if applicable. Requires Steam to be restarted to reflect the change." + + +path_remove_prefix = "The following path will be removed:" +not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall." +standalone_uninstall = ( + "It looks like you are running the standalone version of DZGUI. " + "To finalize the uninstall process, you can remove the 'dzgui' file." +) +system_uninstall = ( + "It looks like DZGUI was installed via your system package manager or directly from source code.\n" + "To finalize the uninstall process, follow the standard removal procedure for that context." +) diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py new file mode 100644 index 0000000..afda8eb --- /dev/null +++ b/dzgui/views/dialogs/uninstall.py @@ -0,0 +1,226 @@ +import os +import subprocess + +from pathlib import Path +from typing import Self, TYPE_CHECKING + +from dzgui.api.shortcuts import Shortcuts +from dzgui.const.constants import APP_NAME +from dzgui.strings import uninstall +from dzgui.util._json import read_json +from dzgui.util.css import load_css +from dzgui.views.components.box import HBox +from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, Pango # noqa E402 + + +class Assistant(Gtk.Assistant): + def __init__(self, is_deck: bool, paths: dict[str, str]): + super().__init__() + if is_deck: + # NOTE: deemed to be "safe" dimensions that exclude taskbar size + self.set_default_size(1085, 670) + else: + self.set_default_size(1500, 900) + + self.set_forward_page_func(self._advance_page) + + pyapp = os.getenv("PYAPP") + + self.page1 = UninstallPage(paths, pyapp) + self.page2 = CompletionPage(pyapp) + + self.append_page(self.page1) + self.set_page_type(self.page1, Gtk.AssistantPageType.INTRO) + self.set_page_title(self.page1, uninstall.pg1_title) + self.set_page_complete(self.page1, True) + + self.append_page(self.page2) + self.set_page_title(self.page2, uninstall.pg2_title) + self.set_page_type(self.page2, Gtk.AssistantPageType.SUMMARY) + + self.connect("cancel", self.destroy_and_quit) + self.connect("close", self.destroy_and_quit) + self.show_all() + load_css() + + def _advance_page(self, index: int) -> int: + cur_page = self.get_nth_page(index) + if cur_page == self.page1: + self.page1.uninstall() + return index + 1 + + def destroy_and_quit(self, widget: Self) -> None: + self.destroy() + Gtk.main_quit() + + +class Window(Gtk.Window): + def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: + super().__init__(title=APP_NAME, icon_name=APP_NAME) + self.assistant = Assistant(is_deck, paths) + + +class CompletionPage(ScrolledWizardPage): + def __init__(self, pyapp: str | None): + super().__init__(heading=uninstall.pg2_heading, description=uninstall.pg2_blurb) + + text = ( + uninstall.standalone_uninstall + if pyapp is not None + else uninstall.system_uninstall + ) + + label = Gtk.Label( + label=text, + wrap_mode=Pango.WrapMode.WORD, + margin=10, + max_width_chars=80, + justify=Gtk.Justification.CENTER, + ) + frame = Gtk.Frame() + frame.add(label) + self.add_start(frame) + + +class FileLabel(HBox): + def __init__(self, path: Path) -> None: + super().__init__(spacing=10) + + label = Gtk.Label(label=uninstall.path_remove_prefix, halign=Gtk.Align.START) + textview = Gtk.TextView( + editable=False, + halign=Gtk.Align.START, + left_margin=10, + right_margin=10, + ) + textview.set_buffer(Gtk.TextBuffer(text=str(path))) + self.extend([label, textview]) + + +class CheckboxWithPath(CheckboxWithLabel): + def __init__( + self, label: str, details: str, path: Path, active: bool = True + ) -> None: + super().__init__(text=label, blurb_text=details) + + self.conf_path = path + self.set_active(active) + fl = FileLabel(path) + self.indent_below(fl) + + def get_conf_path(self) -> Path: + return self.conf_path + + +class UninstallPage(ScrolledWizardPage): + def __init__(self, paths: dict[str, str], pyapp: str | None): + super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb) + + self.pyapp = pyapp + + config = Path(paths["XDG_CONFIG_HOME"]) + state = Path(paths["XDG_STATE_HOME"]) + self.share = Path(paths["XDG_DATA_HOME"]) + shortcut = self.share.parent.joinpath("applications/dzgui.desktop") + desktop = config.parent.parent.joinpath("Desktop/dzgui.desktop") + self.steam_path = self.parse_steam_path(config, self.share) + + self.boxes: list[CheckboxWithPath] = [] + self.checks_area = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=20, margin_top=20 + ) + self.config_box = CheckboxWithPath( + uninstall.config_label, uninstall.config_details, config, active=False + ) + self.desktop_box = CheckboxWithPath( + uninstall.desktop_label, uninstall.desktop_details, desktop + ) + self.start_menu_box = CheckboxWithPath( + uninstall.start_menu_label, uninstall.start_menu_details, shortcut + ) + self.share_box = CheckboxWithPath( + uninstall.desktop_label, uninstall.desktop_details, desktop + ) + self.state_box = CheckboxWithPath( + uninstall.state_label, uninstall.state_details, state + ) + self.steam_box = CheckboxWithLabel( + uninstall.steam_shortcut, uninstall.steam_details + ) + + for box in ( + self.config_box, + self.state_box, + self.desktop_box, + self.start_menu_box, + ): + self.checks_area.add(box) + self.boxes.append(box) + + self.checks_area.add(self.steam_box) + + self.add_start(self.checks_area) + self.show_all() + + def parse_steam_path(self, config: Path, steam: Path) -> Path | None: + try: + file = config.joinpath("config.json") + conf = read_json(file) + steam = conf["default_steam_path"] + self.steam_path = Path(steam) + return Path(steam) + except Exception as e: + print(e) + return None + + def wipe_conf_file(self, box: CheckboxWithPath) -> None: + if box.get_active(): + path = box.get_conf_path() + try: + # TODO: + # path.unlink() + print(f"Deleted '{path}'") + except Exception as e: + print(e) + + def wipe_steam_shortcut(self) -> None: + # TODO: + pass + # try: + # shortcuts = Shortcuts(self.steam_path) + # shortcuts.delete_shortcut(self.share) + # except Exception as e: + # print(e) + + def wipe_pyapp(self) -> None: + if self.pyapp is None: + return + res = subprocess.run([self.pyapp, "self", "remove"]) + print(res) + + def uninstall(self) -> None: + for box in self.boxes: + self.wipe_conf_file(box) + if self.steam_box.get_active(): + self.wipe_steam_shortcut() + # TODO: + # self.wipe_pyapp() + pass + + +class UninstallWizard(Gtk.Application): + def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: + super().__init__() + config = Path(paths["XDG_CONFIG_HOME"]) + # TODO: tests + if not config.is_dir() or not config.joinpath("config.json").is_file(): + print(uninstall.not_installed) + return + GLib.set_prgname(APP_NAME) + self.win = Window(is_deck, paths) + Gtk.main() diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 3a57570..d0f0aa0 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -99,6 +99,8 @@ class ScrolledWizardPage(Gtk.ScrolledWindow): self.box.pack_start(image, expand=False, fill=True, padding=0) self.add(self.box) + self.box.pack_start(self.heading, expand=False, fill=True, padding=0) + self.box.pack_start(self.description, expand=False, fill=True, padding=0) def get_page_type(self) -> Gtk.AssistantPageType: return self.page_type @@ -123,12 +125,9 @@ class EnumeratedWizardPage(ScrolledWizardPage): def __init__(self, enum: PageNum, heading: str, description: str) -> None: super().__init__(heading=heading, description=description) - self.enum = enum self.prog = Progress() self.box.pack_end(self.prog, expand=False, fill=False, 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) def get_enum(self) -> PageNum: return self.enum @@ -541,7 +540,9 @@ class Assistant(Gtk.Assistant): return self.set_page_complete(page, True) - def _add_page(self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType) -> None: + def _add_page( + self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType + ) -> None: self.append_page(page) self.set_page_type(page, ptype) self.set_page_title(page, page.get_title()) @@ -567,15 +568,21 @@ class CheckboxWithLabel(Gtk.Box): def __init__(self, text: str, blurb_text: str) -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5) + self.indent = 20 self.button = Gtk.CheckButton(label=text) self.button.set_active(True) - label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20) + label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=self.indent) + wrapped = textwrap.fill(blurb_text, width=100) label.set_markup(f"- {wrapped}") for el in self.button, label: self.add(el) + def indent_below(self, widget: Gtk.Widget) -> None: + widget.set_margin_start(self.indent) + self.add(widget) + def get_checkbox(self) -> Gtk.CheckButton: return self.button From aeffce2579810a3728e0df56bd0fffb5c5de4525 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:14:12 +0900 Subject: [PATCH 54/58] chore: drop unused import --- dzgui/views/dialogs/uninstall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py index afda8eb..5973d29 100644 --- a/dzgui/views/dialogs/uninstall.py +++ b/dzgui/views/dialogs/uninstall.py @@ -2,7 +2,7 @@ import os import subprocess from pathlib import Path -from typing import Self, TYPE_CHECKING +from typing import Self from dzgui.api.shortcuts import Shortcuts from dzgui.const.constants import APP_NAME From 71d760dc569f11a5fe22d8159778863c068a2b2c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:20:26 +0900 Subject: [PATCH 55/58] change: center align content area --- dzgui/views/dialogs/uninstall.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py index 5973d29..a46513e 100644 --- a/dzgui/views/dialogs/uninstall.py +++ b/dzgui/views/dialogs/uninstall.py @@ -132,7 +132,11 @@ class UninstallPage(ScrolledWizardPage): self.boxes: list[CheckboxWithPath] = [] self.checks_area = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, spacing=20, margin_top=20 + orientation=Gtk.Orientation.VERTICAL, + halign=Gtk.Align.CENTER, + spacing=20, + margin_top=20, + margin_bottom=20, ) self.config_box = CheckboxWithPath( uninstall.config_label, uninstall.config_details, config, active=False From 96b91fd87cdf297b09040040feaeab46dfc5efba Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:20:34 +0900 Subject: [PATCH 56/58] chore: update strings --- dzgui/strings/uninstall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/strings/uninstall.py b/dzgui/strings/uninstall.py index 676392f..a609899 100644 --- a/dzgui/strings/uninstall.py +++ b/dzgui/strings/uninstall.py @@ -2,9 +2,9 @@ pg1_title = "Choose uninstall targets" pg1_heading = "Choose uninstall targets" pg1_blurb = "Here you can choose which DZGUI configuration/state files to remove. " -pg2_title = "Complete" +pg2_title = "Uninstall complete" pg2_heading = "Uninstall complete" -pg2_blurb = "Finished removing the selected files. See the system log for errors." +pg2_blurb = "Finished removing the selected files. See the console for any errors." config_label = "Configuration files" config_details = ( From dfeafc3d28d1ec6a3f2cfabeed4680b118256497 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:18:08 +0900 Subject: [PATCH 57/58] feat: delete shortcut --- dzgui/api/shortcuts.py | 7 ++++--- dzgui/views/dialogs/uninstall.py | 29 ++++++++++++++++------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 50c1d33..594d1a1 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -161,9 +161,10 @@ class Shortcuts: return NEW_ENTRY def delete_shortcut(self, start_path: Path) -> None: - for s in self.shortcuts["shortcuts"]: - if self.shortcuts[s]["StartDir"] == str(start_path): - del self.shortcuts[s] + shortcuts = self.shortcuts["shortcuts"] + for s in shortcuts: + if shortcuts[s]["StartDir"] == str(start_path): + del shortcuts[s] break self.save_shortcuts() diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py index a46513e..1e50129 100644 --- a/dzgui/views/dialogs/uninstall.py +++ b/dzgui/views/dialogs/uninstall.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from pathlib import Path @@ -9,6 +10,7 @@ from dzgui.const.constants import APP_NAME from dzgui.strings import uninstall from dzgui.util._json import read_json from dzgui.util.css import load_css +from dzgui.util.format import format_exception from dzgui.views.components.box import HBox from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel @@ -179,27 +181,29 @@ class UninstallPage(ScrolledWizardPage): self.steam_path = Path(steam) return Path(steam) except Exception as e: - print(e) + print(format_exception(e)) return None def wipe_conf_file(self, box: CheckboxWithPath) -> None: if box.get_active(): path = box.get_conf_path() try: - # TODO: - # path.unlink() + if path.is_dir(): + shutil.rmtree(path) + else: + path.unlink() print(f"Deleted '{path}'") except Exception as e: - print(e) + print(format_exception(e)) def wipe_steam_shortcut(self) -> None: - # TODO: - pass - # try: - # shortcuts = Shortcuts(self.steam_path) - # shortcuts.delete_shortcut(self.share) - # except Exception as e: - # print(e) + if self.steam_path is None: + return + try: + shortcuts = Shortcuts(self.steam_path) + shortcuts.delete_shortcut(self.share) + except Exception as e: + print(format_exception(e)) def wipe_pyapp(self) -> None: if self.pyapp is None: @@ -212,8 +216,7 @@ class UninstallPage(ScrolledWizardPage): self.wipe_conf_file(box) if self.steam_box.get_active(): self.wipe_steam_shortcut() - # TODO: - # self.wipe_pyapp() + self.wipe_pyapp() pass From 5ff936f2661f2e175d4e7a6afef160932107c23e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:29:47 +0900 Subject: [PATCH 58/58] fix: trigger on dayz launcher process --- dzgui/api/steam.py | 5 +++-- dzgui/const/constants.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 6139e9a..668def6 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -16,6 +16,7 @@ from dzgui.const.constants import ( APPID_DAYZ_EXP, APP_NAME, DAYZ_BINARY, + DAYZ_LAUNCHER, DEBIAN_STEAM_PATH, DEFAULT_STEAM_PATH, FLATPAK_STEAM_PATH, @@ -398,14 +399,14 @@ def get_client_allows_downloads(path: Path) -> bool: def is_dayz_running() -> bool: """Subprocesses spawned from Steam will not show up in regular process tree""" procs = [] - substring = DAYZ_BINARY + substring = [DAYZ_BINARY, DAYZ_LAUNCHER] for proc in psutil.process_iter(): try: procs.append(proc.cmdline()) except Exception as e: logger.warning(e) continue - return any(substring in item for sublist in procs for item in sublist) + return any(s in item for sublist in procs for item in sublist for s in substring) def get_app_path(folders_path: Path, appid: int) -> Path: diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index 5be3d0b..d75fc31 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -12,6 +12,7 @@ APPNAME_DAYZ = "DayZ" APPNAME_DAYZ_EXP = "DayZ Exp" APPNAME_DAYZ_EXP_HUMAN = "DayZ Experimental" DAYZ_BINARY = "DayZ_x64.exe" +DAYZ_LAUNCHER = "DayZLauncher.exe" LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf" WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)