Merge branch 'fix/dayz-launcher' of github.com:aclist/dztui into fix/dayz-launcher

This commit is contained in:
aclist 2026-08-20 14:14:49 +09:00
commit 3f1aff7cc6
18 changed files with 256 additions and 85 deletions

View File

@ -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 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. the server's query port.
In addition, packets sent from server responses are expected to be a standard size (see warning below). In addition, a uniform packet size is expected. If your router's MTU setting differs from the default of 1,500,
Deviation from this may cause your router to discard incoming responses from the server. it could lead to incoming responses being discarded (see warning below).
.. important:: .. important::

View File

@ -151,6 +151,22 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
return hashes 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, # TODO: set config to name=user, use official server and no mods,
# ensure that formatted string is identical to fixture with same hash # 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: def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:

View File

@ -73,12 +73,6 @@ class Preferences(EnumWithAttrs):
NAME = { NAME = {
"key": "name", "key": "name",
} }
INSTALL = {
"key": "auto_install",
}
DEBUG = {
"key": "debug",
}
DEFAULT = { DEFAULT = {
"key": "default_steam_path", "key": "default_steam_path",
} }
@ -91,9 +85,6 @@ class Preferences(EnumWithAttrs):
IP_LIST = { IP_LIST = {
"key": "ip_list", "key": "ip_list",
} }
BRANCH = {
"key": "branch",
}
START_TAB = { START_TAB = {
"key": "start_tab", "key": "start_tab",
} }

View File

@ -114,9 +114,6 @@ class Controller(GObject.GObject):
def query_config(self, key: Preferences) -> Any: def query_config(self, key: Preferences) -> Any:
return self.config_man.lookup(key) return self.config_man.lookup(key)
def is_auto_install(self) -> bool:
return bool(self.query_config(Preferences.INSTALL))
def suppress_signal( def suppress_signal(
self, owner: Any, child: Any, func_name: str, state: bool self, owner: Any, child: Any, func_name: str, state: bool
) -> None: ) -> None:
@ -266,6 +263,7 @@ class Controller(GObject.GObject):
self.open_page(NotebookPage.LOG) self.open_page(NotebookPage.LOG)
except Exception as e: except Exception as e:
dialog = ExceptionDialog(self, str(e)) dialog = ExceptionDialog(self, str(e))
dialog.show_all()
dialog.run() dialog.run()
def select_colorized(self) -> None: def select_colorized(self) -> None:
@ -299,6 +297,7 @@ class Controller(GObject.GObject):
write_diagnostic(self.prefs.paths.config, file) write_diagnostic(self.prefs.paths.config, file)
except Exception as e: except Exception as e:
dialog = ExceptionDialog(self, str(e)) dialog = ExceptionDialog(self, str(e))
dialog.show_all()
dialog.run() dialog.run()
def update_steam_api_key(self, text: str) -> None: def update_steam_api_key(self, text: str) -> None:
@ -526,6 +525,9 @@ class Controller(GObject.GObject):
ind = self.config_man.get_start_tab() ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind) 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: def update_and_load_to_menu(self) -> None:
self.connection_man.update_and_connect(menu_only=True) self.connection_man.update_and_connect(menu_only=True)

View File

@ -178,8 +178,8 @@ class ConfigManager:
logger.critical(e) logger.critical(e)
trace = traceback.format_exc() trace = traceback.format_exc()
dialog = ExceptionDialog(self.controller, trace) dialog = ExceptionDialog(self.controller, trace)
dialog.show_all()
dialog.run() dialog.run()
raise e
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None: def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
columns = tv.get_columns() columns = tv.get_columns()

View File

@ -13,6 +13,7 @@ from dzgui.api.shortcuts import Shortcuts
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
connect_debug,
get_app_allows_downloads, get_app_allows_downloads,
get_app_name, get_app_name,
get_needs_update, get_needs_update,
@ -45,6 +46,7 @@ from dzgui.strings.dialogs import (
waiting_for_mods, waiting_for_mods,
waiting_for_directories, waiting_for_directories,
) )
from dzgui.strings import kb
from dzgui.strings.server_mods import checkmark, resync from dzgui.strings.server_mods import checkmark, resync
from dzgui.util.format import format_mib from dzgui.util.format import format_mib
from dzgui.util.strings import dialog, server_timeout from dzgui.util.strings import dialog, server_timeout
@ -193,7 +195,9 @@ class ConnectionManager:
is_last = self.is_last_server() is_last = self.is_last_server()
# TODO: strings # 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( prereqs = Prerequisites(
name=info.server_name, name=info.server_name,
@ -298,8 +302,17 @@ class ConnectionManager:
def _server_timeout(self) -> None: def _server_timeout(self) -> None:
dialog = ExceptionDialog(self.controller, server_timeout) dialog = ExceptionDialog(self.controller, server_timeout)
dialog.set_details_buffer(kb.DZG_006)
dialog.show_all()
dialog.run() 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: def _connect_steam(self, menu_only: bool) -> None:
addr = f"{self.record.ip}:{self.record.gameport}" addr = f"{self.record.ip}:{self.record.gameport}"
playername = self.controller.query_config(Preferences.NAME) playername = self.controller.query_config(Preferences.NAME)

View File

@ -22,9 +22,7 @@ from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManage
from dzgui.model.model_factory import FastInsertListStore, ModelFactory from dzgui.model.model_factory import FastInsertListStore, ModelFactory
from dzgui.strings import dialogs from dzgui.strings import dialogs
from dzgui.util.format import format_mods from dzgui.util.format import format_mods
from dzgui.util.strings import server_timeout
from dzgui.util.symlink import rebuild_symlinks from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.dialogs.generic import ExceptionDialog
import dzgui.api.pefile as PeFile import dzgui.api.pefile as PeFile
@ -213,10 +211,6 @@ class ModManager:
func = StoredFunc(self._on_stale_mods_found, stale) func = StoredFunc(self._on_stale_mods_found, stale)
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
def _server_timeout(self) -> None:
dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run()
def select_colorized(self) -> None: def select_colorized(self) -> None:
model = self.treeview.get_model() model = self.treeview.get_model()
if model is None: if model is None:

View File

@ -64,4 +64,5 @@ class UpdateManager:
def _on_update_failure(self, msg: str) -> None: def _on_update_failure(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg) dialog = ExceptionDialog(self.controller, msg)
dialog.show_all()
dialog.run() dialog.run()

View File

@ -14,7 +14,7 @@ from dzgui.const.constants import (
) )
from dzgui.const.enum import FilterMode, Preferences, ServerTab from dzgui.const.enum import FilterMode, Preferences, ServerTab
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager 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.util.strings import server_timeout, dialog
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
@ -405,6 +405,7 @@ class ServerModelManager:
if show_dialog: if show_dialog:
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan) dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
dialog.show_all()
dialog.run() dialog.run()
def _cleanup_on_failure(self, show_dialog: bool = True) -> None: def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
@ -420,6 +421,8 @@ class ServerModelManager:
# customize statusbar and dialog accordingly # customize statusbar and dialog accordingly
if show_dialog: if show_dialog:
dialog = ExceptionDialog(self.controller, server_timeout) dialog = ExceptionDialog(self.controller, server_timeout)
dialog.set_details_buffer(kb.DZG_006)
dialog.show_all()
dialog.run() dialog.run()
def _push_data(self, data: list[Any]) -> None: def _push_data(self, data: list[Any]) -> None:

View File

@ -0,0 +1,2 @@
collapse_all = "Collapse all"
expand_all = "Expand all"

View File

@ -20,3 +20,6 @@ ok = "OK"
mission_dialog = "Set a mission folder location" mission_dialog = "Set a mission folder location"
custom_mod_dialog = "Set location to a custom mods folder" custom_mod_dialog = "Set location to a custom mods folder"
debug_heading = "Debug"
debug_secondary = "Launch arguments"

13
dzgui/strings/kb.py Normal file
View File

@ -0,0 +1,13 @@
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, 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."""

View File

@ -2,6 +2,7 @@ update_mods = "Update mods and connect"
connect = "Connect" connect = "Connect"
connect_last = "Load to menu screen" connect_last = "Load to menu screen"
connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting." connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
debug = "Debug"
back = "Back" back = "Back"
cancel = "Cancel" cancel = "Cancel"
warnings = "Warnings" warnings = "Warnings"

View File

@ -1,6 +1,7 @@
from typing import Literal, Self, TYPE_CHECKING from typing import Literal, Self, TYPE_CHECKING
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL 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.util import strings
from dzgui.views.components.buttons import ClipboardButton from dzgui.views.components.buttons import ClipboardButton
@ -169,58 +170,30 @@ class QuitDialog(GenericDialog):
self.controller.save_res_and_quit() self.controller.save_res_and_quit()
class ExceptionDialog(GenericDialog): class TextBufferDialog(GenericDialog):
""" def __init__(
Error dialog with rich traceback. self,
Usage: controller: "Controller",
try: mtype: Gtk.MessageType,
foo() heading: str,
except Exception: secondary: str,
trace = traceback.format_exc() text: str,
dialog = ExceptionDialog(Controller, trace) ):
dialog.run()
"""
def __init__(self, controller: "Controller", trace: str):
super().__init__( super().__init__(
controller=controller, controller=controller,
text=strings.error_heading, text=heading,
mtype=Gtk.MessageType.ERROR, mtype=mtype,
buttons=Gtk.ButtonsType.NONE, buttons=Gtk.ButtonsType.NONE,
secondary=strings.something_wrong, secondary=secondary,
) )
self.text = text
self.trace = trace copy_button = ClipboardButton(controller, self.get_text)
# NOTE: box expands to end of content area
scrollable = Gtk.ScrolledWindow(
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
)
textview.set_buffer(Gtk.TextBuffer(text=self.trace))
box.pack_start(textview, EXPAND, FILL, 10)
scrollable.add(box)
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)
self.add_action_widget(copy_button, Gtk.ResponseType.NONE) self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
self.add_button("OK", Gtk.ResponseType.OK) 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) self.connect("response", self._on_response)
def get_trace(self) -> str: def get_text(self) -> str:
return self.trace return self.text
def _on_response( def _on_response(
self, dialog: Self, response: Gtk.ResponseType self, dialog: Self, response: Gtk.ResponseType
@ -236,3 +209,114 @@ class ExceptionDialog(GenericDialog):
return None return None
case _: case _:
return None return None
class DebugDialog(TextBufferDialog):
def __init__(self, controller: "Controller", debug: str):
super().__init__(
controller=controller,
mtype=Gtk.MessageType.INFO,
heading=dialogs.debug_heading,
secondary=dialogs.debug_secondary,
text=debug,
)
scrollable = Gtk.ScrolledWindow(
propagate_natural_height=False,
min_content_height=200,
max_content_height=200,
margin_bottom=10,
)
textview = Gtk.TextView(
wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10
)
textview.set_buffer(Gtk.TextBuffer(text=debug))
scrollable.add(textview)
content = self.get_content_area()
content.set_spacing(0)
content.add(scrollable)
class ExceptionDialog(TextBufferDialog):
"""
Error dialog with rich traceback.
Usage:
try:
foo()
except Exception:
trace = traceback.format_exc()
dialog = ExceptionDialog(Controller, trace)
dialog.show_all()
dialog.run()
"""
def __init__(self, controller: "Controller", trace: str):
super().__init__(
controller=controller,
heading=strings.error_heading,
secondary=strings.something_wrong,
text=trace,
mtype=Gtk.MessageType.ERROR,
)
# NOTE: box expands to end of content area
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,
top_margin=15,
bottom_margin=10,
)
textview.set_buffer(Gtk.TextBuffer(text=trace))
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)
content.add(scrollable)
def _on_page_changed(
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
) -> None:
if child == self.error_details:
GLib.idle_add(self.error_details.set_propagate_natural_height, True)
else:
GLib.idle_add(self.error_details.set_propagate_natural_height, False)
def set_details_buffer(self, text: str) -> None:
self.details_buffer.set_text(text)
self.error_notebook.set_show_tabs(True)

View File

@ -1,10 +1,12 @@
import logging import logging
import re import re
import textwrap
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from importlib import resources from importlib import resources
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH 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.strings import missing_changelog
from dzgui.util.format import format_pango from dzgui.util.format import format_pango
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
@ -31,20 +33,39 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
logger.critical(e) logger.critical(e)
changelog = missing_changelog changelog = missing_changelog
# TODO: should long text be wrapped?
self.controller = controller self.controller = controller
self.box = Gtk.Box( self.box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10 orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
) )
self.add(self.box) self.add(self.box)
expand_all = Gtk.Button(
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)
self.expanded = False
self.expanders: list[Gtk.Expander] = []
self.connect("key-press-event", self._on_keypress) self.connect("key-press-event", self._on_keypress)
self.connect("key-press-event", self._on_esc_keypress) self.connect("key-press-event", self._on_esc_keypress)
changes = self._parse(changelog) changes = self._parse(changelog)
self._generate_nodes(changes) self._generate_nodes(changes)
self.show_all() 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 = clog.collapse_all if self.expanded else clog.expand_all
button.set_label(label)
def grab_content_area(self) -> None: def grab_content_area(self) -> None:
self.grab_focus() self.grab_focus()
@ -65,9 +86,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
text = "\n".join(changes) text = "\n".join(changes)
formatted = format_pango(text) formatted = format_pango(text)
container = Gtk.Box( container = Gtk.Box(valign=Gtk.Align.START, halign=Gtk.Align.START)
valign=Gtk.Align.START, halign=Gtk.Align.START
)
label = Gtk.Label() label = Gtk.Label()
label.set_markup(formatted) label.set_markup(formatted)
container.add(label) container.add(label)
@ -81,6 +100,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
expander.add(container) expander.add(container)
expander.connect("activate", self._on_expand, container) expander.connect("activate", self._on_expand, container)
self.box.add(expander) self.box.add(expander)
self.expanders.append(expander)
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None: def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
""" """
@ -94,6 +114,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
container.set_margin_bottom(15) container.set_margin_bottom(15)
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]: def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
release = "" release = ""
releases: list[tuple[str, list[str]]] = [] releases: list[tuple[str, list[str]]] = []
release_notes: list[str] = [] release_notes: list[str] = []
@ -107,5 +128,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
release = "" release = ""
release = line release = line
continue continue
release_notes.append(line.rstrip()) release_notes.append(textwrap.fill(line.rstrip(), width=120))
return releases return releases

View File

@ -430,6 +430,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
# NOTE: re-check in case file was removed by user between runs # NOTE: re-check in case file was removed by user between runs
if prefs.paths.config.is_file() is False: if prefs.paths.config.is_file() is False:
dialog = ExceptionDialog(self.controller, strings.config_not_found) dialog = ExceptionDialog(self.controller, strings.config_not_found)
dialog.show_all()
dialog.run() dialog.run()
raise OSError(f"Config file '{prefs.paths.config}' not found") raise OSError(f"Config file '{prefs.paths.config}' not found")

View File

@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask
from dzgui.util.localize import number from dzgui.util.localize import number
from dzgui.strings.server_mods import checkmark from dzgui.strings.server_mods import checkmark
from dzgui.strings import preconnect from dzgui.strings import preconnect
from dzgui.views.dialogs.generic import DebugDialog
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.trees.tree_server_mods import ServerModTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView
@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box):
spacing=5, spacing=5,
) )
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
# NOTE: -d flag
if self.controller.get_prefs().is_debug:
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: for button in self.back, self.ok, self.connect_last:
box.add(button) box.add(button)
self.button_box.add(box) self.button_box.add(box)
@ -221,6 +228,12 @@ class PreConnectionAssistant(Gtk.Box):
def _on_connect_last_clicked(self, button: Gtk.Button) -> None: def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_load_to_menu() 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.show_all()
d.run()
def _on_ok_clicked(self, button: Gtk.Button) -> None: def _on_ok_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_connect() self.controller.update_and_connect()

View File

@ -1,20 +1,33 @@
import re
import pytest import pytest
from importlib import resources from pathlib import Path
from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH
@pytest.fixture @pytest.fixture
def changelog(): def changelog(request) -> None:
path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH) root = request.config.rootpath
return path changelog = Path(root).joinpath("CHANGELOG.md").read_text()
return changelog
def test_headings(changelog): def count_hash(line: str) -> int:
with open(changelog, "r") as f: cnt = 0
lines = f.readlines() for c in line:
for line in lines: if c == "#":
if line.startswith("#"): cnt += 1
pass return cnt
# TODO: use regex
pass
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