Compare commits

...
51 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 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 6fdf5547dc fix: handle zero-length shortcuts file 2026-07-11 18:10:30 +09:00
aclist fc1311579c fix: clear typehinting errors 2026-07-11 17:53:55 +09:00
aclist da993db2e2 fix: update imports 2026-07-11 17:53:13 +09:00
aclist 207d8fd407 chore: add logging 2026-07-11 17:52:39 +09:00
aclist 72b10ad543 chore: drop unused code 2026-07-11 17:52:21 +09:00
aclist 35680b1373 chore: clarify dialog strings 2026-07-11 17:51:32 +09:00
aclist 5121e6d005 docs: update changelog 2026-07-11 17:51:09 +09:00
aclist 2f61cc7ff7 feat: make checkboxes in lock-step 2026-07-11 16:33:40 +09:00
aclist 8733094267 chore: wrap in try/except statement 2026-07-11 16:33:15 +09:00
aclist 9d14b7f219 chore: error handling 2026-07-11 16:32:52 +09:00
aclist 0084af4959 feat: write desktop file 2026-07-11 16:32:24 +09:00
aclist caf071f997 feat: generate shortcuts 2026-07-11 15:33:52 +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
34 changed files with 754 additions and 134 deletions

No files matched your search

+31 -28
View File
@@ -1,6 +1,7 @@
# Changelog
## Added
## [7.0.0] Unreleased
### Added
- Setup wizard
- Changelog text wrapping and formatting
- Changelog ships with source
@@ -41,8 +42,10 @@
- Choose to jump into splash screen instead of server
- Collapsible connection panel
- Play offline (load mods directly)
- Generate Steam shortcuts and cover art
- Warn user if background downloads are disabled
## Changed
### Changed
- Conform to PEP 440 versioning for beta versions
- Reduce padding on keys button
- Boldface breadcrumbs
@@ -59,58 +62,58 @@
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped
### Dropped
- Debug mode
- Branch switching
- Manual mod install mode (describe rationale)
- Force update mods
## Fixed
### Fixed
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
- Center server title text on server dialogs
- Rare segfaults when changing maps (threading)
- Moved dialogs out of threads
## Unreleased
### Unreleased
- Load offline mods
- Local documentation
- Raw debug command in context menu
## Developer-facing
### Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
## [6.0.5] 2026-05-17
## Changed
### Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.4] 2026-04-13
## Fixed
### Fixed
- Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12
## Fixed
### Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26
## Fixed
### Fixed
- Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs
## Changed
### Changed
- Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first
- Rename some internal functions
- Wrap entire startup process in dialog
## [6.0.1] 2026-01-16
## Fixed
### Fixed
- Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14
## Added
### Added
- Support DayZ Experimental
- Ping display on server tables
- Save descriptive text notes on a per-server basis
@@ -136,7 +139,7 @@
- Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing
## Fixed
### Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading
@@ -175,7 +178,7 @@
- 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
## Changed
### Changed
- Require Python 3.13
- Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found
@@ -187,50 +190,50 @@
- Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox
## Dropped
### Dropped
- Ping readout in statusbar
- Extraneous information from right statusbar
## [5.8.3] 2026-01-04
## Fixed
### Fixed
- Normalize checksum numbers and dates
## [5.8.2] 2026-01-04
## Fixed
### Fixed
- Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27
## Fixed
### Fixed
- 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
## [5.8.0] 2025-07-06
## Added
### Added
- Filter servers by official/unofficial status
- Automatically fetch geolocation records
## Changed
### Changed
- Updated internal versioning of helper files
## Fixed
### Fixed
- Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17
## Added
### Added
- 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
## Changed
### Changed
- More direct server ping query method that should return marginally more accurate times
## [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
## [5.6.6] 2025-03-16
## Changed
### Changed
- Update IP database records for 2025-03
## [5.6.5] 2025-03-04
## Fixed
### Fixed
- Livonia server results being dropped from batch queries
## [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
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.
# How can I help the project?
@@ -24,7 +24,7 @@ accordingly.
## 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:
- What version are you using?
-1
View File
@@ -86,7 +86,6 @@ class WorkshopACF(ACF):
def __init__(self, file: Path) -> None:
super().__init__(file)
# TODO: break into workshop parser and generic acf parser
def unpack(self, d: dict, lines: list[Any] = []) -> str:
t1 = "AppWorkshop"
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
+189
View File
@@ -0,0 +1,189 @@
import binascii
import ctypes
import logging
import shutil
import vdf # type: ignore
from dataclasses import dataclass
from importlib import resources
from pathlib import Path
from typing import Any
from dzgui.api.steam import find_user_id_32
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.strings import unknown
logger = logging.getLogger(APP_NAME)
@dataclass(slots=True, frozen=True)
class ShortcutMetadata:
appname: str
appid: int
exe_path: str
start_dir: str
icon: str
class Shortcuts:
def __init__(self, steam_path: Path) -> None:
self.user_config_path: Path
self.shortcuts_path = self.find_shortcuts_path(steam_path)
self._load_shortcuts(self.shortcuts_path)
@classmethod
def gen_bpid(cls, uid: str) -> int:
u = uid.encode()
encoded = binascii.crc32(u)
return encoded | 0x80000000
@classmethod
def gen_signed_appid(cls, appid: int) -> int:
return ctypes.c_int(appid).value
@classmethod
def gen_exe_uid(cls, appname: str, exe: Path) -> str:
wrapped_exe = f'"{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:
return self.shortcuts
def _load_shortcuts(self, path: Path) -> None:
try:
with open(path, "rb") as f:
shortcuts = vdf.binary_load(f)
self.shortcuts = shortcuts
logger.debug("Loaded shortcuts file")
except Exception as e:
logger.critical(e)
raise e
def find_shortcuts_path(self, steam_path: Path) -> Path:
uid = find_user_id_32(steam_path)
self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config")
return self.user_config_path.joinpath("shortcuts.vdf")
def add_shortcut(
self, appname: str, start_dir: Path, exe_path: Path, icon: Path
) -> None:
uid = self.gen_exe_uid(appname, exe_path)
self.bpid = self.gen_bpid(uid)
self.signed_appid = self.gen_signed_appid(self.bpid)
meta = ShortcutMetadata(
appname, self.signed_appid, str(exe_path), str(start_dir), str(icon)
)
entry = self._create_entry(meta)
self._insert_at_last_index(entry)
def add_grid_images(self, images: Path) -> None:
"""
{BPID}_hero.png: hero (splash)
{BPID}p.ping: portrait boxart in library
{BPID}_logo.png: logo overlay on top of hero
{BPID}.png: legacy Big Picture/landscape image
"""
grid_path = self.user_config_path.joinpath("grid")
for img in ("_hero", "p", "_logo"):
filename = str(self.bpid) + img
dest = grid_path.joinpath(filename).with_suffix(".png")
b = images.joinpath(img).with_suffix(".png").read_bytes()
dest.write_bytes(b)
# NOTE: legacy Big Picture header
b = images.joinpath("_hero.png").read_bytes()
dest = grid_path.joinpath(str(self.bpid)).with_suffix(".png")
dest.write_bytes(b)
def _insert_at_last_index(self, entry: dict[str, Any]) -> None:
try:
last = list(self.shortcuts["shortcuts"].keys())[-1]
n = int(last) + 1
except Exception:
n = 0
self.shortcuts["shortcuts"][str(n)] = entry
@classmethod
def _create_entry(cls, meta: ShortcutMetadata) -> dict[str, Any]:
"""
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
Key case is not internally consistent and varies between client versions
Most keys use Pascal case, but some do not
appid: signed int CRC
Exe: absolute path to the executable, must be wrapped in literal quotes
StartDir: directory the executable starts in, generally the parent
"""
NEW_ENTRY: dict[str, Any] = {}
NEW_ENTRY["appid"] = meta.appid
NEW_ENTRY["AppName"] = meta.appname
NEW_ENTRY["Exe"] = f'"{meta.exe_path}"'
NEW_ENTRY["StartDir"] = f"{meta.start_dir}"
NEW_ENTRY["icon"] = meta.icon
NEW_ENTRY["ShortcutPath"] = ""
NEW_ENTRY["LaunchOptions"] = ""
NEW_ENTRY["IsHidden"] = 0
NEW_ENTRY["AllowDesktopConfig"] = 1
NEW_ENTRY["AllowOverlay"] = 1
NEW_ENTRY["OpenVR"] = 0
NEW_ENTRY["Devkit"] = 0
NEW_ENTRY["DevkitGameID"] = ""
NEW_ENTRY["DevkitOverrideAppID"] = 0
NEW_ENTRY["LastPlayTime"] = 0
NEW_ENTRY["FlatpakAppID"] = ""
NEW_ENTRY["sortas"] = ""
NEW_ENTRY["tags"] = {}
return NEW_ENTRY
def save_shortcuts(self) -> None:
try:
backup = self.shortcuts_path.with_suffix(".vdf.bak")
shutil.copy(self.shortcuts_path, backup)
with open(self.shortcuts_path, "wb") as f:
vdf.binary_dump(self.shortcuts, f)
except Exception as e:
logger.critical(e)
def add_steam_shortcut(steam_path: Path, exe_path: Path) -> None:
try:
start_dir = exe_path.parent
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
images = Path(str(traversable))
icon = images.joinpath("icon.png")
shortcuts = Shortcuts(Path(steam_path))
shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon)
shortcuts.save_shortcuts()
shortcuts.add_grid_images(images)
copy_dzgui_to_xdg_data(exe_path)
logger.debug("Finished creating Steam shortcut")
except Exception as e:
logger.critical(e)
+30 -23
View File
@@ -32,23 +32,25 @@ from dzgui.const.endpoints import (
)
from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME)
class AppNotInstalledError(Exception):
"""App not present in user's libraryfolders"""
pass
class AppMovedError(Exception):
"""VDF points to a nonexistent location on disk"""
pass
class VDFLoadError(Exception):
"""Malformed VDF or JSON conversion"""
pass
@@ -203,21 +205,35 @@ def launch_offline(
proc = subprocess.run([*client_args, *params])
return proc.returncode
def find_loginusers(path: Path) -> Path:
return path / "config" / "loginusers.vdf"
def find_user_id(path: Path) -> str | None:
resolved_path = path / "config" / "loginusers.vdf"
resolved_path = find_loginusers(path)
try:
with open(resolved_path, "r") as 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"]:
if v["users"][user]["MostRecent"] == "1":
return str(user)
return None
except Exception as e:
logger.warn(e)
logger.debug(e)
return None
def find_user_id_32(path: Path) -> int:
uid = find_user_id(path)
if uid is None:
raise ValueError("Failed to parse a valid Steam32 ID")
return int(uid) & 0xFFFFFFFF
def update_workshop(key: str, mod: int, endpoint: str) -> None:
payload: dict[str, Union[int, str]] = {
"publishedfileid": mod,
@@ -241,20 +257,6 @@ def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT)
def gen_shortcut() -> None:
# TODO:
"""
during setup, prompt user to select matching user id from loginusers
show user account name, select outer steam id
steam/userdata/<steamid_32>/config/shortcuts.vdf
"""
# STEAMID_MAGIC = 76561197960265728
# STEAMID_64 - STEAMID_MAGIC = STEAMID32
# or get right-most 32 bits
# STEAMID_64 & 0xFFFFFFFF
pass
@deprecated("Use subscribe()")
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client)
@@ -329,9 +331,12 @@ def get_running_app() -> int | None:
def get_app_allows_downloads(path: Path, appid: int) -> bool:
root_path = get_app_path(path, appid)
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
flag = ACF(acf).get_allows_downloads()
try:
root_path = get_app_path(path, appid)
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
flag = ACF(acf).get_allows_downloads()
except AppNotInstalledError:
flag = 0
match flag:
# NOTE: adheres to global client setting
case 0:
@@ -346,9 +351,11 @@ def get_app_allows_downloads(path: Path, appid: int) -> bool:
case _:
return True
def get_config(path: Path) -> Path:
return path.joinpath("config/config.vdf")
def get_client_allows_downloads(path: Path) -> bool:
config = get_config(path)
try:
@@ -408,13 +415,13 @@ def get_app_path(folders_path: Path, appid: int) -> Path:
return Path(app_path)
def get_app_name(appid: int) -> str:
def get_app_name(appid: int) -> str | None:
payload = {"appids": [appid]}
res = requests.get(APP_DETAILS, params=payload)
if res.status_code != 200:
return unknown
return None
try:
return str(res.json()[str(appid)]["data"]["name"])
except Exception as e:
logger.debug(e)
return unknown
return None
+4 -7
View File
@@ -20,6 +20,7 @@ from dzgui.strings import boot
# from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.dirs import make_parents
from dzgui.util.localize import set_locale
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
from dzgui.util.strings import init
@@ -34,10 +35,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(APP_NAME)
# TODO: profile load time
def make_parents(path: "Path") -> None:
path.parent.mkdir(parents=True, exist_ok=True)
def setup_logger(log_path: "Path") -> None:
@@ -124,11 +122,10 @@ def load_gui(version: str, is_debug: bool) -> None:
migrate_cols_file(XDG.columns)
copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard
SetupWizard(_is_steam_deck, XDG.config)
wizard = SetupWizard(_is_steam_deck, XDG)
# NOTE: implies that setup wizard failed or was closed
if has_new_config(XDG.config) is False:
return
if not wizard.is_setup_complete():
return
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
+30 -5
View File
@@ -1,18 +1,43 @@
import stat
import textwrap
from pathlib import Path
from dzgui.const.constants import APP_NAME
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
def get_share_path(exe_path: Path) -> Path:
return exe_path.parent.parent
def write_desktop_file(exe_path: Path) -> Path:
icon = find_icon_resource()
def write_desktop_file(share_path: Path) -> None:
template = f"""\
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Exec={share_path}/launch.sh
Exec={exe_path}
Name=DZGUI
Comment=dzgui
Icon={share_path}/dzgui
Icon={icon}
Categories=Game"""
file = share_path / "dzgui.desktop"
file.write_text(textwrap.dedent(template))
copy_dzgui_to_xdg_data(exe_path)
share_path = get_share_path(exe_path)
desktop_file = share_path.joinpath("applications/dzgui.desktop")
make_parents(desktop_file)
desktop_file.write_text(textwrap.dedent(template))
desktop_file.chmod(desktop_file.stat().st_mode | stat.S_IEXEC)
return desktop_file
def write_desktop_shortcut(desktop_file: Path) -> None:
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
link = Path.home().joinpath(f"Desktop/{APP_NAME}.desktop")
if link.exists():
link.unlink()
link.symlink_to(desktop_file)
+6 -1
View File
@@ -18,6 +18,7 @@ class Xdg:
debug: Path
ips: Path
filters: Path
shortcut: Path
def is_writeable(path_str: str) -> bool:
@@ -67,6 +68,7 @@ def get_xdg_paths() -> dict:
def parse_filepaths(xdg: dict) -> Xdg:
config = xdg["XDG_CONFIG_HOME"]
state = xdg["XDG_STATE_HOME"]
share = xdg["XDG_DATA_HOME"]
config = config / "config.json"
@@ -81,6 +83,8 @@ def parse_filepaths(xdg: dict) -> Xdg:
ips = state / "ips.csv"
filters = state / "dzg.filters.json"
shortcut = share / APP_NAME_LOWER
return Xdg(
config,
columns,
@@ -91,5 +95,6 @@ def parse_filepaths(xdg: dict) -> Xdg:
system,
debug,
ips,
filters
filters,
shortcut,
)
+3 -1
View File
@@ -72,7 +72,9 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
CHANGELOG_PATH = "data/CHANGELOG.md"
HERO_PATH = "data/images/hero.png"
IMAGES_PATH = "data/images"
HERO_PATH = "data/images/_hero.png"
CSS_PATH = "data/app.css"
VDF_PATH = "steamapps/libraryfolders.vdf"
DEFAULT_STEAM_PATH = ".local/share/Steam"
+31 -28
View File
@@ -1,6 +1,7 @@
# Changelog
## Added
## [7.0.0] Unreleased
### Added
- Setup wizard
- Changelog text wrapping and formatting
- Changelog ships with source
@@ -41,8 +42,10 @@
- Choose to jump into splash screen instead of server
- Collapsible connection panel
- Play offline (load mods directly)
- Generate Steam shortcuts and cover art
- Warn user if background downloads are disabled
## Changed
### Changed
- Conform to PEP 440 versioning for beta versions
- Reduce padding on keys button
- Boldface breadcrumbs
@@ -59,58 +62,58 @@
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped
### Dropped
- Debug mode
- Branch switching
- Manual mod install mode (describe rationale)
- Force update mods
## Fixed
### Fixed
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
- Center server title text on server dialogs
- Rare segfaults when changing maps (threading)
- Moved dialogs out of threads
## Unreleased
### Unreleased
- Load offline mods
- Local documentation
- Raw debug command in context menu
## Developer-facing
### Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
## [6.0.5] 2026-05-17
## Changed
### Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.4] 2026-04-13
## Fixed
### Fixed
- Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12
## Fixed
### Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26
## Fixed
### Fixed
- Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs
## Changed
### Changed
- Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first
- Rename some internal functions
- Wrap entire startup process in dialog
## [6.0.1] 2026-01-16
## Fixed
### Fixed
- Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14
## Added
### Added
- Support DayZ Experimental
- Ping display on server tables
- Save descriptive text notes on a per-server basis
@@ -136,7 +139,7 @@
- Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing
## Fixed
### Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading
@@ -175,7 +178,7 @@
- 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
## Changed
### Changed
- Require Python 3.13
- Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found
@@ -187,50 +190,50 @@
- Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox
## Dropped
### Dropped
- Ping readout in statusbar
- Extraneous information from right statusbar
## [5.8.3] 2026-01-04
## Fixed
### Fixed
- Normalize checksum numbers and dates
## [5.8.2] 2026-01-04
## Fixed
### Fixed
- Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27
## Fixed
### Fixed
- 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
## [5.8.0] 2025-07-06
## Added
### Added
- Filter servers by official/unofficial status
- Automatically fetch geolocation records
## Changed
### Changed
- Updated internal versioning of helper files
## Fixed
### Fixed
- Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17
## Added
### Added
- 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
## Changed
### Changed
- More direct server ping query method that should return marginally more accurate times
## [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
## [5.6.6] 2025-03-16
## Changed
### Changed
- Update IP database records for 2025-03
## [5.6.5] 2025-03-04
## Fixed
### Fixed
- Livonia server results being dropped from batch queries
## [5.6.4] 2025-02-10
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5 -2
View File
@@ -55,8 +55,11 @@ class ConfigManager:
self.update_config(Preferences.IP_LIST, ips)
def update_history_file(self, fqip: str) -> None:
with open(self.prefs.paths.history, "r") as f:
ips = [line.rstrip() for line in f.readlines()]
try:
with open(self.prefs.paths.history, "r") as f:
ips = [line.rstrip() for line in f.readlines()]
except FileNotFoundError:
ips = []
seen = set()
ips.append(fqip)
+7
View File
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
import dzgui.api.pefile as PeFile
import dzgui.api.servers as Servers
from dzgui.api.shortcuts import Shortcuts
from dzgui.api.steam import (
connect,
@@ -175,6 +176,12 @@ class ConnectionManager:
if running_app is not None:
allows_dl = get_app_allows_downloads(steam_path, 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)
else:
allows_downloads = (True, "")
+19
View File
@@ -64,6 +64,25 @@ radio_mi = "mi (miles)"
label_dist = "Distance display"
label_client = "Steam client"
### Shortcuts page
heading_shortcuts = "Create shortcuts"
blurb_shortcuts = (
"You can optionally create shortcuts to facilitate launching DZGUI faster."
)
button_shortcuts = "Create shortcuts"
checkbox_steam_shortcut = (
"Add DZGUI to Steam library",
(
"This creates a standalone copy of DZGUI and adds it to Steam. "
"You <b>must restart Steam</b> for the shortcut to appear. This will not replace or update existing shortcuts. "
"If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version."
),
)
checkbox_start_menu = (
"Add a start menu shortcut",
"Depends on local window manager settings. Will work on Steam Deck.",
)
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
### Completion page
heading_completion = "Setup complete"
+1 -1
View File
@@ -41,10 +41,10 @@ def print_mods(mods: list[int]) -> str:
def write_diagnostic(config: Path, outfile: Path) -> None:
# TODO: test availability on other distros
date = datetime.now().isoformat()
try:
# FIXME: ID_LIKE not available on CachyOS
distro = platform.freedesktop_os_release()["ID_LIKE"]
except Exception as e:
logger.warn(e)
+32
View File
@@ -0,0 +1,32 @@
import logging
import os
import shutil
from importlib import resources
from pathlib import Path
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
logger = logging.getLogger(APP_NAME)
def make_parents(path: "Path") -> None:
path.parent.mkdir(parents=True, exist_ok=True)
def copy_dzgui_to_xdg_data(exe_path: Path) -> None:
dzgui = os.getenv("PYAPP")
if dzgui is None:
return
try:
make_parents(exe_path)
shutil.copy(dzgui, exe_path)
except Exception as e:
logger.critical(e)
def find_icon_resource() -> "Path":
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
images = Path(str(traversable))
icon = images.joinpath("icon.png")
return icon
+136 -15
View File
@@ -1,11 +1,13 @@
import os
import textwrap
from enum import Enum
from importlib import resources
from pathlib import Path
from typing import Any, Callable, Self
from typing import Any, Callable, Self, TYPE_CHECKING
from dzgui.api.probe import test_steam_api, test_bm_api
from dzgui.api.shortcuts import add_steam_shortcut
from dzgui.api.steam import get_steam_paths
from dzgui.const.constants import (
APP_NAME,
@@ -15,6 +17,9 @@ from dzgui.const.constants import (
)
from dzgui.const.boilerplate import config_boilerplate
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
from dzgui.const.enum import Preferences
from dzgui.config import freedesktop
from dzgui.config.query import lookup
from dzgui.init.migrate import migrate_legacy_conf
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import wizard
@@ -30,6 +35,9 @@ import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
if TYPE_CHECKING:
from dzgui.config.xdg import Xdg
class PageNum(Enum):
INTRO = 1
@@ -38,7 +46,8 @@ class PageNum(Enum):
STEAM_API = 4
BM_API = 5
USER_PREFS = 6
FINAL = 7
SHORTCUTS = 7
FINAL = 8
class DescriptionArea(Gtk.Box):
@@ -58,7 +67,7 @@ class Progress(Gtk.ProgressBar):
class ScrolledWizardPage(Gtk.ScrolledWindow):
def __init__(self, enum: PageNum, heading: str, description: str):
super().__init__()
super().__init__(overlay_scrolling=False)
self.enum = enum
self.page_type: Gtk.AssistantPageType
@@ -141,6 +150,10 @@ class NotificationFrame(Gtk.Frame):
if error:
add_class(self, "error-frame")
def set_text(self, text: str) -> None:
wrapped = textwrap.fill(text, width=80)
self.label.set_markup(wrapped)
class APIValidationPage(ScrolledWizardPage):
def __init__(
@@ -339,6 +352,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
def _on_import_clicked(self, button: Gtk.Button) -> None:
self.grid.set_sensitive(False)
try:
# TODO: this could be deferred to the final page (prevents accidental destruction of dialog via ESC)
migrate_legacy_conf(self.config)
self.migrated = True
self.success_box.set_visible(True)
@@ -423,24 +437,28 @@ class CompletionPage(ScrolledWizardPage):
class Assistant(Gtk.Assistant):
def __init__(self, is_deck: bool, config: Path):
def __init__(self, is_deck: bool, XDG: "Xdg"):
super().__init__()
if is_deck:
self.fullscreen()
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
self.set_default_size(1085, 670)
else:
self.set_default_size(1500, 900)
self.config_path = config
self.config_path = XDG.config
self.config_values: dict[str, Any] = config_boilerplate
self.setup_complete = False
self.page1 = IntroductionPage()
self.page2 = ConfigMigrationPage(config)
self.page2 = ConfigMigrationPage(XDG.config)
self.page3 = SteamPathPage()
self.page4 = SteamValidationPage()
self.page5 = BMValidationPage()
self.page6 = PreferencesPage()
self.page7 = CompletionPage()
self.page7 = ShortcutCreationPage(XDG.shortcut)
self.page8 = CompletionPage()
self.set_forward_page_func(self._advance_page)
@@ -458,9 +476,16 @@ class Assistant(Gtk.Assistant):
self.page5,
self.page6,
self.page7,
self.page8,
):
# NOTE: skip config migration page if no legacy config file
if page == self.page2 and self.has_legacy_config is False:
if (
isinstance(page, ConfigMigrationPage)
and self.has_legacy_config is False
):
continue
# NOTE: disabled for now on system-provided packages
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None:
continue
self._add_page(page, page.get_page_type())
@@ -475,13 +500,15 @@ class Assistant(Gtk.Assistant):
def _advance_page(self, index: int) -> int:
page = self.get_nth_page(index)
# TODO: use enums
# TODO: use enums/isinstance
match page:
case self.page1:
pass
case self.page2:
if self.page2.is_migrated():
return self.get_n_pages() - 1
steam_path = lookup(self.config_path, Preferences.DEFAULT)
self.page7.set_steam_path(steam_path)
return self.get_n_pages() - 2
case self.page3:
self.config_values["default_steam_path"] = page.get_path_from_radio()
case self.page4:
@@ -495,10 +522,17 @@ class Assistant(Gtk.Assistant):
self.config_values["use_miles"] = use_miles
self.config_values["client"] = client
self.write_config()
self.page7.set_steam_path(self.config_values["default_steam_path"])
case self.page7:
self.page7.create_shortcuts()
self.setup_complete = True
case _:
raise AttributeError("Trying to advance a non-canonical page")
return index + 1
def is_setup_complete(self) -> bool:
return self.setup_complete
def destroy_and_quit(self, widget: Self) -> None:
self.destroy()
Gtk.main_quit()
@@ -543,6 +577,90 @@ class Assistant(Gtk.Assistant):
EMITTER.emit("step_pending")
class CheckboxWithLabel(Gtk.Box):
def __init__(self, text: str, blurb_text: str) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
self.button = Gtk.CheckButton(label=text)
self.button.set_active(True)
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
wrapped = textwrap.fill(blurb_text, width=100)
label.set_markup(f"- {wrapped}")
for el in self.button, label:
self.add(el)
def get_checkbox(self) -> Gtk.CheckButton:
return self.button
def get_active(self) -> bool:
return self.button.get_active()
def set_active(self, state: bool) -> None:
self.button.set_active(state)
class ShortcutCreationPage(ScrolledWizardPage):
def __init__(self, shortcut: Path) -> None:
super().__init__(
enum=PageNum.SHORTCUTS,
heading=wizard.heading_shortcuts,
description=wizard.blurb_shortcuts,
)
self.steam_path: Path
self.shortcut_path = shortcut
self.checks_area = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, spacing=10, margin_top=20
)
self.page_type = Gtk.AssistantPageType.INTRO
label, blurb = wizard.checkbox_steam_shortcut
self.steam_checkbox = CheckboxWithLabel(label, blurb)
label, blurb = wizard.checkbox_start_menu
self.start_menu_checkbox = CheckboxWithLabel(label, blurb)
cb = self.start_menu_checkbox.get_checkbox()
cb.connect("toggled", self._on_start_menu_toggled)
label, blurb = wizard.checkbox_desktop_shortcut
self.desktop_checkbox = CheckboxWithLabel(label, blurb)
for el in (
self.steam_checkbox,
self.start_menu_checkbox,
self.desktop_checkbox,
):
self.checks_area.add(el)
self.add_start(self.checks_area)
self.show_all()
self.connect("map", self._on_map)
def _on_start_menu_toggled(self, button: Gtk.CheckButton) -> None:
state = button.get_active()
if not state:
self.desktop_checkbox.set_active(state)
self.desktop_checkbox.set_sensitive(state)
def _on_map(self, page: "ScrolledWizardPage") -> None:
EMITTER.emit("step_complete")
def set_steam_path(self, path: Path) -> None:
self.steam_path = path
def create_shortcuts(self) -> None:
# NOTE: best-effort, permissive even on failure (page is already marked as complete)
if self.steam_checkbox.get_active():
add_steam_shortcut(self.steam_path, self.shortcut_path)
if self.start_menu_checkbox.get_active():
desktop_file = freedesktop.write_desktop_file(self.shortcut_path)
if self.desktop_checkbox.get_active():
freedesktop.write_desktop_shortcut(desktop_file)
class SteamPathPage(ScrolledWizardPage):
def __init__(self) -> None:
super().__init__(
@@ -599,17 +717,20 @@ class SteamPathPage(ScrolledWizardPage):
class SetupWizard(Gtk.Application):
def __init__(self, is_deck: bool, config: Path) -> None:
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
super().__init__()
GLib.set_prgname(APP_NAME)
Window(is_deck, config)
self.win = Window(is_deck, XDG)
Gtk.main()
def is_setup_complete(self) -> int:
return self.win.assistant.is_setup_complete()
class Window(Gtk.Window):
def __init__(self, is_deck: bool, config: Path) -> None:
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
super().__init__(title=APP_NAME, icon_name=APP_NAME)
Assistant(is_deck, config)
self.assistant = Assistant(is_deck, XDG)
class Emitter(GObject.GObject):
+54 -5
View File
@@ -1,4 +1,5 @@
import logging
import re
from typing import TYPE_CHECKING
from importlib import resources
@@ -32,15 +33,63 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
# TODO: should long text be wrapped?
self.controller = controller
formatted = format_pango(changelog)
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.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
self.add(self.box)
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 grab_content_area(self) -> None:
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
if allows_dl is False:
if len(prereqs.mods) > 0 and allows_dl is False:
msg = (
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"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b15"
version = "7.0.0b19"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
@@ -76,7 +76,7 @@ build-backend = "setuptools.build_meta"
line-length = 88
indent-width = 4
extend-exclude = ["*lib/a2s"]
lint.unfixable = ["F401"]
lint.unfixable = ["F401", "I001"]
[tool.ruff.format]
quote-style = "double"
+1
View File
@@ -12,6 +12,7 @@ This file includes the combined licenses to the following components:
- psutil
- PyGObject
- python-a2s
- vdf
When shipped as a binary installer, these components are collectively bundled
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
from pathlib import Path
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.xdg import get_xdg_paths, parse_filepaths
@@ -10,12 +16,7 @@ from dzgui.const.constants import APPID_DAYZ
from tests.fixtures import fixture_path
pytestmark = pytest.mark.pefile
@pytest.fixture
def second_drive():
return fixture_path("in_library_on_second_drive.vdf")
pytestmark = pytest.mark.apitest
@pytest.fixture
@@ -26,6 +27,8 @@ def default_steam_path():
return conf["default_steam_path"]
# TODO: split into separate test file
# FIXME: use fixtures
def test_pefile_length(default_steam_path):
try:
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),
],
)
def test_not_in_library(fixture, exception):
fixture = fixture_path(fixture)
def test_not_in_library(monkeypatch, fixture, exception):
import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture)
folder_path = Path(fixture_path("api"))
with pytest.raises(exception):
try:
get_app_path(fixture, APPID_DAYZ)
get_app_path(folder_path, APPID_DAYZ)
except Exception as e:
raise e
def test_on_second_drive(second_drive):
path = get_app_path(second_drive, APPID_DAYZ)
@pytest.fixture
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")
+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