Compare commits

..
41 Commits
Author SHA1 Message Date
aclist b47cb482f5 Merge pull request #412 from aclist/fix/deck-unfullscreen-dimensions
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: set explicitly safe dimensions for Steam Deck
2026-07-21 16:03:20 +09:00
aclist 152999d728 fix: set explicitly safe dimensions for Steam Deck 2026-07-21 16:01:48 +09:00
aclist cdcb3b794f Merge pull request #411 from aclist/fix/deck-unfullscreen
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: delegate dialog size to system settings on Steam Deck
2026-07-21 15:22:37 +09:00
aclist cd23f403c9 fix: try delegating size to desktop resolution 2026-07-21 15:21:34 +09:00
aclist a40bef8f82 fix: hotfix for Steam OS fullscreen bug 2026-07-21 15:16:42 +09:00
aclist 5c07a77b63 Merge pull request #409 from aclist/fix/beta-client-vdf
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: handle alternate client versions when parsing vdf
2026-07-15 11:29:54 +09:00
aclist 91773c933c chore: bump version 2026-07-15 11:26:01 +09:00
aclist 6a30ed44fc fix: handle alternate client versions when parsing vdf 2026-07-15 11:25:47 +09:00
aclist bbaec35a5f Merge pull request #408 from aclist/fix/conditional-warning
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: make warning conditional when mods > 0
2026-07-14 17:40:09 +09:00
aclist c46477f0a7 fix: make warning conditional when mods > 0 2026-07-14 17:28:46 +09:00
aclist f47446a2e1 Merge pull request #407 from aclist/fix/parse-nsg-ids
fix: parse NSG ids
2026-07-14 17:18:22 +09:00
aclist cdb311cb1f chore: clear typehinting errors 2026-07-14 17:14:11 +09:00
aclist 11cdcb62b2 chore: drop unused import 2026-07-14 17:08:11 +09:00
aclist 1913f69e7b chore: drop unused import 2026-07-14 17:07:56 +09:00
aclist 8c2cea0fdc chore: bump version 2026-07-14 17:07:47 +09:00
aclist 8c29b9b35a fix: parse out-of-band appids as NSGs 2026-07-14 17:06:40 +09:00
aclist d8239f1c34 Merge pull request #405 from aclist/chore/update-licenses
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: list missing library
2026-07-12 12:34:32 +09:00
aclist 8c3ec4512a chore: list missing library 2026-07-12 12:34:00 +09:00
aclist 1a18c8ff02 Merge pull request #404 from aclist/fix/app-path-tests
chore: update tests to reflect get_app_path() changes
2026-07-12 12:23:05 +09:00
aclist 92c23ec609 chore: update tests to reflect get_app_path() changes 2026-07-12 12:22:12 +09:00
aclist e9d4054e76 Merge pull request #403 from aclist/prerelease/7.0.0b16
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: bump version
2026-07-12 10:35:11 +09:00
aclist 1c9e026f70 chore: bump version 2026-07-12 10:34:24 +09:00
aclist eb076b7dcd Merge pull request #402 from aclist/fix/clean-acf
chore: drop comment
2026-07-12 10:32:27 +09:00
aclist 8629e02670 chore: drop comment 2026-07-12 10:30:06 +09:00
aclist 3ba38f30ad Merge pull request #401 from aclist/feat/collapsible-changelog
feat: collapsible changelog
2026-07-12 10:28:18 +09:00
aclist 7c37fd8b6e Merge pull request #400 from aclist/feat/shortcuts
feat: generate Steam shortcut
2026-07-12 10:27:27 +09:00
aclist 1a065123bb Merge branch 'feat/shortcuts' of github.com:aclist/dztui into feat/shortcuts
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-12 10:25:06 +09:00
aclist 2586306323 chore: drop comment 2026-07-12 10:24:45 +09:00
aclist 1ccb99d935 Merge branch 'dzgui7' into feat/shortcuts 2026-07-12 10:24:04 +09:00
aclist 054fe968d1 Merge pull request #399 from aclist/feat/reaper-proc
feat: config parsers and reaper process monitor
2026-07-12 10:22:45 +09:00
aclist 1760e6e928 chore: cleanup
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-12 10:21:06 +09:00
aclist a2a39e9927 fix: backport fixes from feat/shortcuts 2026-07-12 10:15:58 +09:00
aclist e664f68624 fix: restore prior proc monitor logic (avoids conflicts) 2026-07-12 10:14:55 +09:00
aclist 2e41856348 chore: backport changelog fixes 2026-07-12 10:14:15 +09:00
aclist 27d444ea6f feat: parse changelog chunks 2026-07-11 22:59:28 +09:00
aclist 26a2362172 fix: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-07-11 20:11:18 +09:00
aclist 50cf347164 fix: typo 2026-07-11 20:10:25 +09:00
aclist dd69bc88b1 chore: add shortcut tests 2026-07-11 20:04:45 +09:00
aclist 5c84b5ae0a chore: drop test code 2026-07-11 19:23:37 +09:00
aclist e92df8c062 Merge pull request #398 from u-alexandru/fix/history-file-not-found
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: prevent stuck launch dialog when history file is missing
2026-07-08 10:41:36 +09:00
u-alexandru 494311e8df fix: prevent stuck launch dialog when history file is missing
update_history_file() opened dzg.history in read mode without handling a
missing file. dzg.history is only created the first time it is written, so
on a fresh install the very first server connection raised FileNotFoundError
inside the post-launch cleanup callback:

  _destroy_on_idle -> _add_to_history_and_return -> update_history_file

The exception propagated out of the GLib idle callback before destroy_dialog()
ran, orphaning the "Waiting for DayZ to launch" modal so it could never be
closed from the UI and its Cancel button was unresponsive.

Treat a missing history file as an empty history so the first connection
records history normally instead of crashing the cleanup callback.
2026-07-07 18:45:22 +03:00
24 changed files with 379 additions and 122 deletions

No files matched your search

+29 -28
View File
@@ -1,6 +1,7 @@
# Changelog # Changelog
## Added ## [7.0.0] Unreleased
### Added
- Setup wizard - Setup wizard
- Changelog text wrapping and formatting - Changelog text wrapping and formatting
- Changelog ships with source - Changelog ships with source
@@ -44,7 +45,7 @@
- Generate Steam shortcuts and cover art - Generate Steam shortcuts and cover art
- Warn user if background downloads are disabled - Warn user if background downloads are disabled
## Changed ### Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
- Reduce padding on keys button - Reduce padding on keys button
- Boldface breadcrumbs - Boldface breadcrumbs
@@ -61,58 +62,58 @@
- Enable LAN page Empty/Full filters on first run of app - Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client - Propagate subscribed mods to Steam client
## Dropped ### Dropped
- Debug mode - Debug mode
- Branch switching - Branch switching
- Manual mod install mode (describe rationale) - Manual mod install mode (describe rationale)
- Force update mods - Force update mods
## Fixed ### Fixed
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input - Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
- Center server title text on server dialogs - Center server title text on server dialogs
- Rare segfaults when changing maps (threading) - Rare segfaults when changing maps (threading)
- Moved dialogs out of threads - Moved dialogs out of threads
## Unreleased ### Unreleased
- Load offline mods - Load offline mods
- Local documentation - Local documentation
- Raw debug command in context menu - Raw debug command in context menu
## Developer-facing ### Developer-facing
- Add pyproject.toml file - Add pyproject.toml file
- Show deprecation warnings - Show deprecation warnings
- Options -> Dev page - Options -> Dev page
## [6.0.5] 2026-05-17 ## [6.0.5] 2026-05-17
## Changed ### Changed
- Update symlink hash logic to align with DZGUI 7 beta - Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit - Update A2S helper to latest commit
## [6.0.4] 2026-04-13 ## [6.0.4] 2026-04-13
## Fixed ### Fixed
- Update A2S module checksums to support servers with missing description field - Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12 ## [6.0.3] 2026-04-12
## Fixed ### Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback - Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26 ## [6.0.2] 2026-01-26
## Fixed ### Fixed
- Explicitly use Python 3.13 when calling subprocesses - Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module - Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs - Do not spawn dialogs from inside of thread when fetching prereqs
## Changed ### Changed
- Optimize time complexity of startup checks - Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first - Optimize coordinate calculation by using local records first
- Rename some internal functions - Rename some internal functions
- Wrap entire startup process in dialog - Wrap entire startup process in dialog
## [6.0.1] 2026-01-16 ## [6.0.1] 2026-01-16
## Fixed ### Fixed
- Explicitly use Python 3.13 when calling subprocesses - Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14 ## [6.0.0] 2026-01-14
## Added ### Added
- Support DayZ Experimental - Support DayZ Experimental
- Ping display on server tables - Ping display on server tables
- Save descriptive text notes on a per-server basis - Save descriptive text notes on a per-server basis
@@ -138,7 +139,7 @@
- Commandline usage help text (GaryBlackbourne) - Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing - More descriptive help dialogs when initial dependencies are missing
## Fixed ### Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne) - Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables - Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading - Servers returning malformed A2S_INFO blocking server browser from loading
@@ -177,7 +178,7 @@
- Path discovery during first-time setup when parsing filepaths with whitespaces - Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it - First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## Changed ### Changed
- Require Python 3.13 - Require Python 3.13
- Reduced global API cooldown from 60s to 30s - Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found - Clarify dialog messages when DayZ path could not be found
@@ -189,50 +190,50 @@
- Rewrote distance calculation module (GaryBlackbourne) - Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox - Changed preferred client setting from radio toggle to combobox
## Dropped ### Dropped
- Ping readout in statusbar - Ping readout in statusbar
- Extraneous information from right statusbar - Extraneous information from right statusbar
## [5.8.3] 2026-01-04 ## [5.8.3] 2026-01-04
## Fixed ### Fixed
- Normalize checksum numbers and dates - Normalize checksum numbers and dates
## [5.8.2] 2026-01-04 ## [5.8.2] 2026-01-04
## Fixed ### Fixed
- Fix regression in symlink routine introduced by previous hotfix - Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27 ## [5.8.1] 2025-12-27
## Fixed ### Fixed
- Path discovery during first-time setup when parsing filepaths with whitespaces - Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it - First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## [5.8.0] 2025-07-06 ## [5.8.0] 2025-07-06
## Added ### Added
- Filter servers by official/unofficial status - Filter servers by official/unofficial status
- Automatically fetch geolocation records - Automatically fetch geolocation records
## Changed ### Changed
- Updated internal versioning of helper files - Updated internal versioning of helper files
## Fixed ### Fixed
- Corrected erroneous 2024 date in prior changelog entries - Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17 ## [5.7.0] 2025-04-17
## Added ### Added
- Save application dimensions when quitting and restore on subsequent boot - Save application dimensions when quitting and restore on subsequent boot
## Fixed ### Fixed
- Issues with the window exceeding the bounds of the screen when displaying some table contexts - Issues with the window exceeding the bounds of the screen when displaying some table contexts
## Changed ### Changed
- More direct server ping query method that should return marginally more accurate times - More direct server ping query method that should return marginally more accurate times
## [5.6.7] 2025-04-04 ## [5.6.7] 2025-04-04
## Dropped ### Dropped
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet - Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
## [5.6.6] 2025-03-16 ## [5.6.6] 2025-03-16
## Changed ### Changed
- Update IP database records for 2025-03 - Update IP database records for 2025-03
## [5.6.5] 2025-03-04 ## [5.6.5] 2025-03-04
## Fixed ### Fixed
- Livonia server results being dropped from batch queries - Livonia server results being dropped from batch queries
## [5.6.4] 2025-02-10 ## [5.6.4] 2025-02-10
+2 -2
View File
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
# Requesting help # Requesting help
If you encounter a problem with DZGUI, you can submit tickets on the GitHub If you encounter a problem with DZGUI, you can submit tickets on the GitHub
(issue tracker)[https://github.com/aclist/dztui/issues] under the [issue tracker](https://github.com/aclist/dztui/issues) under the
`troubleshooting` tag. `troubleshooting` tag.
# How can I help the project? # How can I help the project?
@@ -24,7 +24,7 @@ accordingly.
## Submitting a ticket ## Submitting a ticket
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dztui/issues]. Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
From there, follow the onscreen prompts. You will be asked questions such as: From there, follow the onscreen prompts. You will be asked questions such as:
- What version are you using? - What version are you using?
-1
View File
@@ -86,7 +86,6 @@ class WorkshopACF(ACF):
def __init__(self, file: Path) -> None: def __init__(self, file: Path) -> None:
super().__init__(file) super().__init__(file)
# TODO: break into workshop parser and generic acf parser
def unpack(self, d: dict, lines: list[Any] = []) -> str: def unpack(self, d: dict, lines: list[Any] = []) -> str:
t1 = "AppWorkshop" t1 = "AppWorkshop"
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails") t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
+27 -12
View File
@@ -12,6 +12,7 @@ from typing import Any
from dzgui.api.steam import find_user_id_32 from dzgui.api.steam import find_user_id_32
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
from dzgui.util.dirs import copy_dzgui_to_xdg_data from dzgui.util.dirs import copy_dzgui_to_xdg_data
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -46,6 +47,25 @@ class Shortcuts:
wrapped_exe = f'"{exe}"' wrapped_exe = f'"{exe}"'
return appname + wrapped_exe return appname + wrapped_exe
def find_appname_by_unsigned_id(self, appid: int) -> str:
# NOTE: bitmask signed int back to 32-bit CRC
# varying client versions treat case sensitivity differently
try:
for key in self.shortcuts["shortcuts"].keys():
if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid:
try:
return str(self.shortcuts["shortcuts"][key]["appname"])
except Exception as e:
logger.debug(e)
try:
return str(self.shortcuts["shortcuts"][key]["AppName"])
except Exception as e:
logger.debug(e)
return unknown
except Exception as e:
logger.debug(e)
return unknown
def get_shortcuts(self) -> Any: def get_shortcuts(self) -> Any:
return self.shortcuts return self.shortcuts
@@ -59,7 +79,7 @@ class Shortcuts:
logger.critical(e) logger.critical(e)
raise e raise e
def find_shortcuts_path(self, steam_path: Path) -> None: def find_shortcuts_path(self, steam_path: Path) -> Path:
uid = find_user_id_32(steam_path) uid = find_user_id_32(steam_path)
self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config") self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config")
return self.user_config_path.joinpath("shortcuts.vdf") return self.user_config_path.joinpath("shortcuts.vdf")
@@ -111,22 +131,18 @@ class Shortcuts:
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
Wiki variously lists keys with title case and lowercase, but keys actually do Key case is not internally consistent and varies between client versions
not seem to be case-sensitive. Some entries generated by Steam do not match the wiki Most keys use Pascal case, but some do not
Keys are entered into the dictionary in a linear insertion order
# TODO: try to replicate key case sensitivity as it is created by Steam with generic shortcut
appid: signed int CRC appid: signed int CRC
exe: absolute path to the executable, must be wrapped in literal quotes Exe: absolute path to the executable, must be wrapped in literal quotes
StartDir: directory the executable starts in, generally the parent StartDir: directory the executable starts in, generally the parent
""" """
NEW_ENTRY: dict[str, Any] = {} NEW_ENTRY: dict[str, Any] = {}
NEW_ENTRY["appid"] = meta.appid NEW_ENTRY["appid"] = meta.appid
NEW_ENTRY["appname"] = meta.appname NEW_ENTRY["AppName"] = meta.appname
NEW_ENTRY["exe"] = f'"{meta.exe_path}"' NEW_ENTRY["Exe"] = f'"{meta.exe_path}"'
NEW_ENTRY["StartDir"] = f"{meta.start_dir}" NEW_ENTRY["StartDir"] = f"{meta.start_dir}"
NEW_ENTRY["icon"] = meta.icon NEW_ENTRY["icon"] = meta.icon
NEW_ENTRY["ShortcutPath"] = "" NEW_ENTRY["ShortcutPath"] = ""
@@ -134,7 +150,7 @@ class Shortcuts:
NEW_ENTRY["IsHidden"] = 0 NEW_ENTRY["IsHidden"] = 0
NEW_ENTRY["AllowDesktopConfig"] = 1 NEW_ENTRY["AllowDesktopConfig"] = 1
NEW_ENTRY["AllowOverlay"] = 1 NEW_ENTRY["AllowOverlay"] = 1
NEW_ENTRY["openvr"] = 0 NEW_ENTRY["OpenVR"] = 0
NEW_ENTRY["Devkit"] = 0 NEW_ENTRY["Devkit"] = 0
NEW_ENTRY["DevkitGameID"] = "" NEW_ENTRY["DevkitGameID"] = ""
NEW_ENTRY["DevkitOverrideAppID"] = 0 NEW_ENTRY["DevkitOverrideAppID"] = 0
@@ -162,7 +178,6 @@ def add_steam_shortcut(steam_path: Path, exe_path: Path) -> None:
images = Path(str(traversable)) images = Path(str(traversable))
icon = images.joinpath("icon.png") icon = images.joinpath("icon.png")
# TEST: assert that record (start dir and exe combo) is in shortcuts after creation
shortcuts = Shortcuts(Path(steam_path)) shortcuts = Shortcuts(Path(steam_path))
shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon) shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon)
shortcuts.save_shortcuts() shortcuts.save_shortcuts()
+50 -26
View File
@@ -15,6 +15,7 @@ from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
APP_NAME, APP_NAME,
DAYZ_BINARY,
DEBIAN_STEAM_PATH, DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH, DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
@@ -31,7 +32,6 @@ from dzgui.const.endpoints import (
) )
from dzgui.strings import wizard from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args from dzgui.util.bash import concat_bash_args
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -205,20 +205,28 @@ def launch_offline(
proc = subprocess.run([*client_args, *params]) proc = subprocess.run([*client_args, *params])
return proc.returncode return proc.returncode
def find_loginusers(path: Path) -> Path:
return path / "config" / "loginusers.vdf"
def find_user_id(path: Path) -> str | None: def find_user_id(path: Path) -> str | None:
resolved_path = path / "config" / "loginusers.vdf" resolved_path = find_loginusers(path)
try: try:
with open(resolved_path, "r") as f: with open(resolved_path, "r") as f:
v = vdf.load(f) v = vdf.load(f)
# NOTE: beta client
# /package/beta
if len(v["users"]) == 1:
return str(list(v["users"].keys())[0])
for user in v["users"]: for user in v["users"]:
if v["users"][user]["MostRecent"] == "1": if v["users"][user]["MostRecent"] == "1":
return str(user) return str(user)
return None return None
except Exception as e: except Exception as e:
logger.warn(e) logger.debug(e)
return None return None
def find_user_id_32(path: Path) -> int: def find_user_id_32(path: Path) -> int:
uid = find_user_id(path) uid = find_user_id(path)
if uid is None: if uid is None:
@@ -301,28 +309,34 @@ def get_running_app() -> int | None:
PROC_NAME = "steam" PROC_NAME = "steam"
SUBPROC_NAME = "reaper" SUBPROC_NAME = "reaper"
FLAG = "AppId" FLAG = "AppId"
# NOTE: may cause conflicts if multiple apps are running
for proc in psutil.process_iter(): try:
if proc.name() == PROC_NAME: for proc in psutil.process_iter():
subprocs = proc.children() if proc.name() == PROC_NAME:
filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME) subprocs = proc.children()
try: filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME)
proc = next(filtered) try:
except StopIteration: proc = next(filtered)
return None except StopIteration:
args = proc.cmdline() return None
appid = (row for row in args if FLAG in row) args = proc.cmdline()
try: appid = (row for row in args if FLAG in row)
return int(next(appid).split("=")[1]) try:
except StopIteration: return int(next(appid).split("=")[1])
return None except StopIteration:
return None
except Exception as e:
logger.debug(e)
return None return None
def get_app_allows_downloads(path: Path, appid: int) -> bool: def get_app_allows_downloads(path: Path, appid: int) -> bool:
root_path = get_app_path(path, appid) try:
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf") root_path = get_app_path(path, appid)
flag = ACF(acf).get_allows_downloads() acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
flag = ACF(acf).get_allows_downloads()
except AppNotInstalledError:
flag = 0
match flag: match flag:
# NOTE: adheres to global client setting # NOTE: adheres to global client setting
case 0: case 0:
@@ -337,9 +351,11 @@ def get_app_allows_downloads(path: Path, appid: int) -> bool:
case _: case _:
return True return True
def get_config(path: Path) -> Path: def get_config(path: Path) -> Path:
return path.joinpath("config/config.vdf") return path.joinpath("config/config.vdf")
def get_client_allows_downloads(path: Path) -> bool: def get_client_allows_downloads(path: Path) -> bool:
config = get_config(path) config = get_config(path)
try: try:
@@ -360,8 +376,16 @@ def get_client_allows_downloads(path: Path) -> bool:
def is_dayz_running() -> bool: def is_dayz_running() -> bool:
appid = get_running_app() """Subprocesses spawned from Steam will not show up in regular process tree"""
return appid in (APPID_DAYZ, APPID_DAYZ_EXP) procs = []
substring = DAYZ_BINARY
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)
def get_app_path(folders_path: Path, appid: int) -> Path: def get_app_path(folders_path: Path, appid: int) -> Path:
@@ -391,13 +415,13 @@ def get_app_path(folders_path: Path, appid: int) -> Path:
return Path(app_path) return Path(app_path)
def get_app_name(appid: int) -> str: def get_app_name(appid: int) -> str | None:
payload = {"appids": [appid]} payload = {"appids": [appid]}
res = requests.get(APP_DETAILS, params=payload) res = requests.get(APP_DETAILS, params=payload)
if res.status_code != 200: if res.status_code != 200:
return unknown return None
try: try:
return str(res.json()[str(appid)]["data"]["name"]) return str(res.json()[str(appid)]["data"]["name"])
except Exception as e: except Exception as e:
logger.debug(e) logger.debug(e)
return unknown return None
+29 -28
View File
@@ -1,6 +1,7 @@
# Changelog # Changelog
## Added ## [7.0.0] Unreleased
### Added
- Setup wizard - Setup wizard
- Changelog text wrapping and formatting - Changelog text wrapping and formatting
- Changelog ships with source - Changelog ships with source
@@ -44,7 +45,7 @@
- Generate Steam shortcuts and cover art - Generate Steam shortcuts and cover art
- Warn user if background downloads are disabled - Warn user if background downloads are disabled
## Changed ### Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
- Reduce padding on keys button - Reduce padding on keys button
- Boldface breadcrumbs - Boldface breadcrumbs
@@ -61,58 +62,58 @@
- Enable LAN page Empty/Full filters on first run of app - Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client - Propagate subscribed mods to Steam client
## Dropped ### Dropped
- Debug mode - Debug mode
- Branch switching - Branch switching
- Manual mod install mode (describe rationale) - Manual mod install mode (describe rationale)
- Force update mods - Force update mods
## Fixed ### Fixed
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input - Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
- Center server title text on server dialogs - Center server title text on server dialogs
- Rare segfaults when changing maps (threading) - Rare segfaults when changing maps (threading)
- Moved dialogs out of threads - Moved dialogs out of threads
## Unreleased ### Unreleased
- Load offline mods - Load offline mods
- Local documentation - Local documentation
- Raw debug command in context menu - Raw debug command in context menu
## Developer-facing ### Developer-facing
- Add pyproject.toml file - Add pyproject.toml file
- Show deprecation warnings - Show deprecation warnings
- Options -> Dev page - Options -> Dev page
## [6.0.5] 2026-05-17 ## [6.0.5] 2026-05-17
## Changed ### Changed
- Update symlink hash logic to align with DZGUI 7 beta - Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit - Update A2S helper to latest commit
## [6.0.4] 2026-04-13 ## [6.0.4] 2026-04-13
## Fixed ### Fixed
- Update A2S module checksums to support servers with missing description field - Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12 ## [6.0.3] 2026-04-12
## Fixed ### Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback - Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26 ## [6.0.2] 2026-01-26
## Fixed ### Fixed
- Explicitly use Python 3.13 when calling subprocesses - Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module - Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs - Do not spawn dialogs from inside of thread when fetching prereqs
## Changed ### Changed
- Optimize time complexity of startup checks - Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first - Optimize coordinate calculation by using local records first
- Rename some internal functions - Rename some internal functions
- Wrap entire startup process in dialog - Wrap entire startup process in dialog
## [6.0.1] 2026-01-16 ## [6.0.1] 2026-01-16
## Fixed ### Fixed
- Explicitly use Python 3.13 when calling subprocesses - Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14 ## [6.0.0] 2026-01-14
## Added ### Added
- Support DayZ Experimental - Support DayZ Experimental
- Ping display on server tables - Ping display on server tables
- Save descriptive text notes on a per-server basis - Save descriptive text notes on a per-server basis
@@ -138,7 +139,7 @@
- Commandline usage help text (GaryBlackbourne) - Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing - More descriptive help dialogs when initial dependencies are missing
## Fixed ### Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne) - Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables - Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading - Servers returning malformed A2S_INFO blocking server browser from loading
@@ -177,7 +178,7 @@
- Path discovery during first-time setup when parsing filepaths with whitespaces - Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it - First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## Changed ### Changed
- Require Python 3.13 - Require Python 3.13
- Reduced global API cooldown from 60s to 30s - Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found - Clarify dialog messages when DayZ path could not be found
@@ -189,50 +190,50 @@
- Rewrote distance calculation module (GaryBlackbourne) - Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox - Changed preferred client setting from radio toggle to combobox
## Dropped ### Dropped
- Ping readout in statusbar - Ping readout in statusbar
- Extraneous information from right statusbar - Extraneous information from right statusbar
## [5.8.3] 2026-01-04 ## [5.8.3] 2026-01-04
## Fixed ### Fixed
- Normalize checksum numbers and dates - Normalize checksum numbers and dates
## [5.8.2] 2026-01-04 ## [5.8.2] 2026-01-04
## Fixed ### Fixed
- Fix regression in symlink routine introduced by previous hotfix - Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27 ## [5.8.1] 2025-12-27
## Fixed ### Fixed
- Path discovery during first-time setup when parsing filepaths with whitespaces - Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it - First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## [5.8.0] 2025-07-06 ## [5.8.0] 2025-07-06
## Added ### Added
- Filter servers by official/unofficial status - Filter servers by official/unofficial status
- Automatically fetch geolocation records - Automatically fetch geolocation records
## Changed ### Changed
- Updated internal versioning of helper files - Updated internal versioning of helper files
## Fixed ### Fixed
- Corrected erroneous 2024 date in prior changelog entries - Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17 ## [5.7.0] 2025-04-17
## Added ### Added
- Save application dimensions when quitting and restore on subsequent boot - Save application dimensions when quitting and restore on subsequent boot
## Fixed ### Fixed
- Issues with the window exceeding the bounds of the screen when displaying some table contexts - Issues with the window exceeding the bounds of the screen when displaying some table contexts
## Changed ### Changed
- More direct server ping query method that should return marginally more accurate times - More direct server ping query method that should return marginally more accurate times
## [5.6.7] 2025-04-04 ## [5.6.7] 2025-04-04
## Dropped ### Dropped
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet - Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
## [5.6.6] 2025-03-16 ## [5.6.6] 2025-03-16
## Changed ### Changed
- Update IP database records for 2025-03 - Update IP database records for 2025-03
## [5.6.5] 2025-03-04 ## [5.6.5] 2025-03-04
## Fixed ### Fixed
- Livonia server results being dropped from batch queries - Livonia server results being dropped from batch queries
## [5.6.4] 2025-02-10 ## [5.6.4] 2025-02-10
+5 -2
View File
@@ -55,8 +55,11 @@ class ConfigManager:
self.update_config(Preferences.IP_LIST, ips) self.update_config(Preferences.IP_LIST, ips)
def update_history_file(self, fqip: str) -> None: def update_history_file(self, fqip: str) -> None:
with open(self.prefs.paths.history, "r") as f: try:
ips = [line.rstrip() for line in f.readlines()] with open(self.prefs.paths.history, "r") as f:
ips = [line.rstrip() for line in f.readlines()]
except FileNotFoundError:
ips = []
seen = set() seen = set()
ips.append(fqip) ips.append(fqip)
+7
View File
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
import dzgui.api.pefile as PeFile import dzgui.api.pefile as PeFile
import dzgui.api.servers as Servers import dzgui.api.servers as Servers
from dzgui.api.shortcuts import Shortcuts
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
@@ -175,6 +176,12 @@ class ConnectionManager:
if running_app is not None: if running_app is not None:
allows_dl = get_app_allows_downloads(steam_path, running_app) allows_dl = get_app_allows_downloads(steam_path, running_app)
running_app_name = get_app_name(running_app) running_app_name = get_app_name(running_app)
# NOTE: for out-of-range appid, assume NSG.
# dzgui.api.steam.get_app_name() will return None
if running_app_name is None:
s = Shortcuts(steam_path)
# NOTE: returns "Unknown" if unparseable
running_app_name = s.find_appname_by_unsigned_id(running_app)
allows_downloads = (allows_dl, running_app_name) allows_downloads = (allows_dl, running_app_name)
else: else:
allows_downloads = (True, "") allows_downloads = (True, "")
+2 -3
View File
@@ -440,7 +440,8 @@ class Assistant(Gtk.Assistant):
def __init__(self, is_deck: bool, XDG: "Xdg"): def __init__(self, is_deck: bool, XDG: "Xdg"):
super().__init__() super().__init__()
if is_deck: if is_deck:
self.fullscreen() # NOTE: deemed to be "safe" dimensions that exclude taskbar size
self.set_default_size(1085, 670)
else: else:
self.set_default_size(1500, 900) self.set_default_size(1500, 900)
@@ -484,8 +485,6 @@ class Assistant(Gtk.Assistant):
): ):
continue continue
# NOTE: disabled for now on system-provided packages # NOTE: disabled for now on system-provided packages
# TODO: drop, for testing purposes
os.environ["PYAPP"] = str(Path.home().joinpath("dzgui/dzgui"))
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None: if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None:
continue continue
self._add_page(page, page.get_page_type()) self._add_page(page, page.get_page_type())
+54 -5
View File
@@ -1,4 +1,5 @@
import logging import logging
import re
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from importlib import resources from importlib import resources
@@ -32,15 +33,63 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
# TODO: should long text be wrapped? # TODO: should long text be wrapped?
self.controller = controller self.controller = controller
formatted = format_pango(changelog) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
self.changelog_label = Gtk.Label(valign=Gtk.Align.START, margin=15)
self.changelog_label.set_markup(formatted)
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box.add(self.changelog_label)
self.add(self.box) self.add(self.box)
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)
self._generate_nodes(changes)
self.show_all()
def grab_content_area(self) -> None: def grab_content_area(self) -> None:
self.grab_focus() self.grab_focus()
def _parse_version(self, version: str) -> tuple[str, str]:
pattern = r"(^##\s\[)(.+)(\] )(.+$)"
match = re.match(pattern, version)
if match:
version = match.group(2)
date = match.group(4)
return version, date
raise ValueError("Changelog parse error")
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
for header, changes in releases:
for ind in (0, -1):
if changes[ind] == "":
del changes[ind]
text = "\n".join(changes)
formatted = format_pango(text)
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START)
label.set_markup(formatted)
version, date = self._parse_version(header)
button_text = f"{version} ({date})"
button = Gtk.Button(label=button_text, margin_start=5)
expander = Gtk.Expander()
expander.set_label_widget(button)
expander.add(label)
self.box.add(expander)
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
release = ""
releases: list[tuple[str, list[str]]] = []
release_notes: list[str] = []
for line in changelog.splitlines():
if line.startswith("# "):
continue
if line.startswith("## "):
if release != "":
releases.append((release, release_notes))
release_notes = []
release = ""
release = line
continue
release_notes.append(line.rstrip())
return releases
+1 -1
View File
@@ -273,7 +273,7 @@ class PreConnectionAssistant(Gtk.Box):
) )
allows_dl, running_app = prereqs.allows_downloads allows_dl, running_app = prereqs.allows_downloads
if allows_dl is False: if len(prereqs.mods) > 0 and allows_dl is False:
msg = ( msg = (
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n" f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
"Either stop the game first, or update your global Steam settings or the game's local settings.\n" "Either stop the game first, or update your global Steam settings or the game's local settings.\n"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b15" version = "7.0.0b19"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
@@ -76,7 +76,7 @@ build-backend = "setuptools.build_meta"
line-length = 88 line-length = 88
indent-width = 4 indent-width = 4
extend-exclude = ["*lib/a2s"] extend-exclude = ["*lib/a2s"]
lint.unfixable = ["F401"] lint.unfixable = ["F401", "I001"]
[tool.ruff.format] [tool.ruff.format]
quote-style = "double" quote-style = "double"
+1
View File
@@ -12,6 +12,7 @@ This file includes the combined licenses to the following components:
- psutil - psutil
- PyGObject - PyGObject
- python-a2s - python-a2s
- vdf
When shipped as a binary installer, these components are collectively bundled When shipped as a binary installer, these components are collectively bundled
as a turnkey application and used to run the DZGUI utility.\n\n as a turnkey application and used to run the DZGUI utility.\n\n
File renamed without changes.
+13
View File
@@ -0,0 +1,13 @@
"users"
{
"0"
{
"AccountName" "STEAMUSER"
"PersonaName" "STEAMUSER"
"RememberPassword" "0"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AutoLogin" "1"
"Timestamp" "0"
}
}
+14
View File
@@ -0,0 +1,14 @@
"users"
{
"0"
{
"AccountName" "STEAMUSER"
"PersonaName" "STEAMUSER"
"RememberPassword" "0"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "1"
"Timestamp" "0"
}
}
@@ -0,0 +1,25 @@
"users"
{
"0"
{
"AccountName" "STEAMUSER"
"PersonaName" "STEAMUSER"
"RememberPassword" "0"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "0"
"Timestamp" "0"
}
"1"
{
"AccountName" "STEAMUSER2"
"PersonaName" "STEAMUSER2"
"RememberPassword" "0"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "1"
"Timestamp" "0"
}
}
File renamed without changes.
Binary file not shown.
File renamed without changes.
+25
View File
@@ -0,0 +1,25 @@
import pytest
from pathlib import Path
from dzgui.api.steam import find_user_id
from tests.fixtures import fixture_path
pytestmark = pytest.mark.apitest
@pytest.mark.parametrize(
"fixture, expect",
[
("api/loginusers_legacy_client.vdf", 0),
("api/loginusers_legacy_client_multiple.vdf", 1),
("api/loginusers_beta_client.vdf", 0),
],
)
def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
def mock_loginusers(path: Path) -> Path:
return fixture_path(fixture)
monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers)
uid = find_user_id(Path(""))
assert int(uid) == expect
+26 -12
View File
@@ -1,8 +1,14 @@
import pytest import pytest
from pathlib import Path from pathlib import Path
import dzgui.api.pefile as PeFile import dzgui.api.pefile as PeFile
from dzgui.api.steam import VDFLoadError, AppNotInstalledError, AppMovedError, get_app_path from dzgui.api.steam import (
VDFLoadError,
AppNotInstalledError,
AppMovedError,
get_app_path
)
from dzgui.config.query import get_config from dzgui.config.query import get_config
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
@@ -10,12 +16,7 @@ from dzgui.const.constants import APPID_DAYZ
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
pytestmark = pytest.mark.pefile pytestmark = pytest.mark.apitest
@pytest.fixture
def second_drive():
return fixture_path("in_library_on_second_drive.vdf")
@pytest.fixture @pytest.fixture
@@ -26,6 +27,8 @@ def default_steam_path():
return conf["default_steam_path"] return conf["default_steam_path"]
# TODO: split into separate test file
# FIXME: use fixtures
def test_pefile_length(default_steam_path): def test_pefile_length(default_steam_path):
try: try:
pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ) pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ)
@@ -52,15 +55,26 @@ def test_invalid_pefile_path():
("malformed.vdf", VDFLoadError), ("malformed.vdf", VDFLoadError),
], ],
) )
def test_not_in_library(fixture, exception): def test_not_in_library(monkeypatch, fixture, exception):
fixture = fixture_path(fixture) import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture)
folder_path = Path(fixture_path("api"))
with pytest.raises(exception): with pytest.raises(exception):
try: try:
get_app_path(fixture, APPID_DAYZ) get_app_path(folder_path, APPID_DAYZ)
except Exception as e: except Exception as e:
raise e raise e
def test_on_second_drive(second_drive): @pytest.fixture
path = get_app_path(second_drive, APPID_DAYZ) def second_drive():
return "in_library_on_second_drive.vdf"
def test_on_second_drive(monkeypatch, second_drive):
import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", second_drive)
folder_path = Path(fixture_path("api"))
path = get_app_path(folder_path, APPID_DAYZ)
assert path == Path("/tmp") assert path == Path("/tmp")
+67
View File
@@ -0,0 +1,67 @@
import pytest
import tempfile
from pathlib import Path
from _pytest.monkeypatch import MonkeyPatch
from dzgui.api.shortcuts import Shortcuts
from tests.fixtures import fixture_path
pytestmark = pytest.mark.apitest
def mock_find_shortcuts(self, steam_path: Path) -> Path:
return Path(fixture_path("api/no_shortcuts.vdf"))
@pytest.fixture(scope="module", autouse=True)
def patch_api() -> None:
mp = MonkeyPatch()
mp.setattr("dzgui.api.shortcuts.Shortcuts.find_shortcuts_path", mock_find_shortcuts)
yield
mp.undo()
def test_no_shortcuts(monkeypatch) -> None:
s = Shortcuts(Path(""))
assert len(s.shortcuts["shortcuts"]) == 0
@pytest.fixture
def dummy_app() -> None:
d = {
"AppName": "TEST APP",
"StartDir": "TEST_DIR",
"Exe": "TEST_DIR/TEST_EXE.EXE",
"icon": "IMAGES_DIR/TEST_IMAGE.PNG",
}
return d
def test_wrap_exe(dummy_app) -> None:
s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values())
assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
def test_add_shortcut(dummy_app) -> None:
s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values())
new = s.shortcuts["shortcuts"]
ind = str(len(new) - 1)
for k, v in dummy_app.items():
if k == "Exe":
v = f'"{v}"'
assert new[ind][k] == v
def test_save_shortcut(dummy_app) -> None:
s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values())
with tempfile.NamedTemporaryFile() as f:
tmp = Path(f.name)
s.shortcuts_path = tmp
s.save_shortcuts()
s._load_shortcuts(tmp)
assert len(s.shortcuts["shortcuts"]) == 1