Compare commits

...
9 Commits
Author SHA1 Message Date
aclist daa4aa0059 chore: clear typehinting errors resulting from mixins
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-07-24 00:48:45 +09:00
aclist 71a0481600 fix: add comma 2026-07-24 00:47:28 +09:00
aclist ba1b980a7f chore: rename test namespace 2026-07-24 00:47:08 +09:00
aclist 1dc20b838d fix: support source version of DZGUI 2026-07-24 00:45:27 +09:00
aclist 0c6421fa63 chore: update freedesktop comment
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-21 16:30:07 +09:00
aclist a4f9a67da1 fix: use drop-in mixin to set page progress state 2026-07-21 15:01:11 +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
12 changed files with 145 additions and 48 deletions

No files matched your search

+21 -13
View File
@@ -49,9 +49,21 @@ class Shortcuts:
def find_appname_by_unsigned_id(self, appid: int) -> str: def find_appname_by_unsigned_id(self, appid: int) -> str:
# NOTE: bitmask signed int back to 32-bit CRC # NOTE: bitmask signed int back to 32-bit CRC
for key in self.shortcuts["shortcuts"].keys(): # varying client versions treat case sensitivity differently
if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid: try:
return str(self.shortcuts["shortcuts"][key]["appname"]) 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 return unknown
def get_shortcuts(self) -> Any: def get_shortcuts(self) -> Any:
@@ -119,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"] = ""
@@ -142,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
+9 -2
View File
@@ -205,21 +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:
+1 -1
View File
@@ -21,7 +21,7 @@ def write_desktop_file(exe_path: Path) -> Path:
Terminal=false Terminal=false
Exec={exe_path} Exec={exe_path}
Name=DZGUI Name=DZGUI
Comment=dzgui Comment=DayZ server browser and mod manager
Icon={icon} Icon={icon}
Categories=Game""" Categories=Game"""
+2 -1
View File
@@ -162,7 +162,8 @@ class APIEntry(Gtk.Box):
self.entry.connect("icon-release", self._on_icon_release) self.entry.connect("icon-release", self._on_icon_release)
self.entry.connect("activate", self._on_field_activated) self.entry.connect("activate", self._on_field_activated)
self.submit = Gtk.Button(label="Submit") # TODO: strings
self.submit = Gtk.Button(label="Validate")
self.submit.set_sensitive(False) self.submit.set_sensitive(False)
self.submit.connect("clicked", self._on_submit) self.submit.connect("clicked", self._on_submit)
+27 -23
View File
@@ -50,6 +50,13 @@ class PageNum(Enum):
FINAL = 8 FINAL = 8
class OptionalPageMixin:
"""Marks optional pages as advanceable"""
def _on_map(self, page: "ScrolledWizardPage") -> None:
EMITTER.emit("step_complete")
class DescriptionArea(Gtk.Box): class DescriptionArea(Gtk.Box):
def __init__(self, text: str): def __init__(self, text: str):
super().__init__(orientation=Gtk.Orientation.VERTICAL) super().__init__(orientation=Gtk.Orientation.VERTICAL)
@@ -91,6 +98,7 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
margin_top=50, margin_top=50,
spacing=20, spacing=20,
) )
self.add(self.box) self.add(self.box)
self.prog = Progress() self.prog = Progress()
self.box.pack_end(self.prog, expand=False, fill=False, padding=0) self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
@@ -215,7 +223,7 @@ class APIValidationPage(ScrolledWizardPage):
self.spinner.stop() self.spinner.stop()
class BMValidationPage(APIValidationPage): class BMValidationPage(OptionalPageMixin, APIValidationPage): # type: ignore
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
enum=PageNum.BM_API, enum=PageNum.BM_API,
@@ -224,6 +232,7 @@ class BMValidationPage(APIValidationPage):
link=BM_API_SETUP, link=BM_API_SETUP,
func=self._validate, func=self._validate,
) )
self.connect("map", self._on_map)
@call_on_thread("", show_dialog=False) @call_on_thread("", show_dialog=False)
def _validate(self, key: str) -> None: def _validate(self, key: str) -> None:
@@ -444,6 +453,7 @@ class Assistant(Gtk.Assistant):
else: else:
self.set_default_size(1500, 900) self.set_default_size(1500, 900)
self.is_binary = False if os.getenv("PYAPP") is None else True
self.config_path = XDG.config self.config_path = XDG.config
self.config_values: dict[str, Any] = config_boilerplate self.config_values: dict[str, Any] = config_boilerplate
@@ -484,7 +494,7 @@ class Assistant(Gtk.Assistant):
): ):
continue continue
# NOTE: disabled for now on system-provided packages # NOTE: disabled for now on system-provided packages
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None: if isinstance(page, ShortcutCreationPage) and not self.is_binary:
continue continue
self._add_page(page, page.get_page_type()) self._add_page(page, page.get_page_type())
@@ -499,32 +509,33 @@ class Assistant(Gtk.Assistant):
def _advance_page(self, index: int) -> int: def _advance_page(self, index: int) -> int:
page = self.get_nth_page(index) page = self.get_nth_page(index)
# TODO: use enums/isinstance
match page: match page:
case self.page1: case IntroductionPage():
pass pass
case self.page2: case ConfigMigrationPage():
if self.page2.is_migrated(): if page.is_migrated():
steam_path = lookup(self.config_path, Preferences.DEFAULT) steam_path = lookup(self.config_path, Preferences.DEFAULT)
self.page7.set_steam_path(steam_path) self.page7.set_steam_path(steam_path)
return self.get_n_pages() - 2 offset = 1 if not self.is_binary else 2
case self.page3: self.setup_complete = True
return self.get_n_pages() - offset
case SteamPathPage():
self.config_values["default_steam_path"] = page.get_path_from_radio() self.config_values["default_steam_path"] = page.get_path_from_radio()
case self.page4: case SteamValidationPage():
self.config_values["steam_api"] = page.get_api_key() self.config_values["steam_api"] = page.get_api_key()
case self.page5: case BMValidationPage():
self.config_values["bm_api"] = page.get_api_key() self.config_values["bm_api"] = page.get_api_key()
# NOTE: collects config values before advancing to last page case PreferencesPage():
case self.page6: # NOTE: collects config values before advancing to last page
name, use_miles, client = self.page6.get_prefs() name, use_miles, client = self.page6.get_prefs()
self.config_values["name"] = name self.config_values["name"] = name
self.config_values["use_miles"] = use_miles self.config_values["use_miles"] = use_miles
self.config_values["client"] = client self.config_values["client"] = client
self.write_config() self.write_config()
self.page7.set_steam_path(self.config_values["default_steam_path"]) self.page7.set_steam_path(self.config_values["default_steam_path"])
case self.page7:
self.page7.create_shortcuts()
self.setup_complete = True self.setup_complete = True
case ShortcutCreationPage():
page.create_shortcuts()
case _: case _:
raise AttributeError("Trying to advance a non-canonical page") raise AttributeError("Trying to advance a non-canonical page")
return index + 1 return index + 1
@@ -568,11 +579,7 @@ class Assistant(Gtk.Assistant):
bar.set_fraction(fraction) bar.set_fraction(fraction)
bar.set_text(f"{page_num}/{total}") bar.set_text(f"{page_num}/{total}")
# NOTE: disable forward action if not isinstance(page, IntroductionPage):
# TODO: use page enums
if page == self.page5:
return
if page != self.page1:
EMITTER.emit("step_pending") EMITTER.emit("step_pending")
@@ -599,7 +606,7 @@ class CheckboxWithLabel(Gtk.Box):
self.button.set_active(state) self.button.set_active(state)
class ShortcutCreationPage(ScrolledWizardPage): class ShortcutCreationPage(OptionalPageMixin, ScrolledWizardPage): # type: ignore
def __init__(self, shortcut: Path) -> None: def __init__(self, shortcut: Path) -> None:
super().__init__( super().__init__(
enum=PageNum.SHORTCUTS, enum=PageNum.SHORTCUTS,
@@ -642,9 +649,6 @@ class ShortcutCreationPage(ScrolledWizardPage):
self.desktop_checkbox.set_active(state) self.desktop_checkbox.set_active(state)
self.desktop_checkbox.set_sensitive(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: def set_steam_path(self, path: Path) -> None:
self.steam_path = path self.steam_path = path
+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.0b17" version = "7.0.0b18"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
@@ -107,11 +107,11 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
[tool.pytest.ini_options] [tool.pytest.ini_options]
markers = [ markers = [
"apitest: checks remote endpoints",
"config: config file keys/values", "config: config file keys/values",
"mods: tests mod metadata/link creation", "mods: tests mod metadata/link creation",
"pefile: validate PE files", "pefile: validate PE files",
"post_install: requires a completed installation", "post_install: requires a completed installation",
"redact: log redaction mechanisms", "redact: log redaction mechanisms",
"slow: long-running tests", "slow: long-running tests",
"webtest: checks remote endpoints"
] ]
+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"
}
}
+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
+1 -1
View File
@@ -4,7 +4,7 @@ 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
from dzgui.api import probe from dzgui.api import probe
pytestmark = pytest.mark.webtest pytestmark = pytest.mark.apitest
@pytest.fixture @pytest.fixture
+5 -5
View File
@@ -30,9 +30,9 @@ def test_no_shortcuts(monkeypatch) -> None:
@pytest.fixture @pytest.fixture
def dummy_app() -> None: def dummy_app() -> None:
d = { d = {
"appname": "TEST APP", "AppName": "TEST APP",
"StartDir": "TEST_DIR", "StartDir": "TEST_DIR",
"exe": "TEST_DIR/TEST_EXE.EXE", "Exe": "TEST_DIR/TEST_EXE.EXE",
"icon": "IMAGES_DIR/TEST_IMAGE.PNG", "icon": "IMAGES_DIR/TEST_IMAGE.PNG",
} }
return d return d
@@ -41,8 +41,8 @@ def dummy_app() -> None:
def test_wrap_exe(dummy_app) -> None: def test_wrap_exe(dummy_app) -> None:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values()) s.add_shortcut(*dummy_app.values())
assert s.shortcuts["shortcuts"]["0"]["exe"][0] == '"' assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
assert s.shortcuts["shortcuts"]["0"]["exe"][-1] == '"' assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
def test_add_shortcut(dummy_app) -> None: def test_add_shortcut(dummy_app) -> None:
@@ -51,7 +51,7 @@ def test_add_shortcut(dummy_app) -> None:
new = s.shortcuts["shortcuts"] new = s.shortcuts["shortcuts"]
ind = str(len(new) - 1) ind = str(len(new) - 1)
for k, v in dummy_app.items(): for k, v in dummy_app.items():
if k == "exe": if k == "Exe":
v = f'"{v}"' v = f'"{v}"'
assert new[ind][k] == v assert new[ind][k] == v