Compare commits

...
Author SHA1 Message Date
aclist f4be9a31e9 change: check actual client preference
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-10-25 15:56:06 +09:00
aclist a6282e4401 chore: clean up test blockage
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
2026-10-24 10:58:21 +09:00
aclist a9ae5f59b5 chore: add flatpak tests 2026-10-24 09:21:50 +09:00
aclist cdec81071d feat: show notice on override
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
2026-10-23 15:39:56 +09:00
aclist 97b5c0e6ee fix: split arguments 2026-10-23 15:19:37 +09:00
aclist 67c19fb90d feat: add SimpleTree 2026-10-23 15:11:05 +09:00
aclist d9d3f747bb feat: add override module 2026-10-23 15:10:24 +09:00
aclist 1440871819 chore: update strings 2026-10-23 14:14:44 +09:00
aclist 761988d8c2 feat: add override module 2026-10-23 14:14:30 +09:00
aclist ab5637ae3b Merge branch 'dzgui7' into feat/dynamic-flatpak-toggle 2026-10-23 12:35:53 +09:00
aclist 682532a867 chore: remove erroneous debug string 2026-10-23 12:34:04 +09:00
aclist d577ba0970 change: remove Flatpak option if binary is not installed 2026-10-23 12:20:10 +09:00
aclist c2993ad289 change: revert prior VDF change, no observable effect on D-Bus 2026-10-23 12:11:19 +09:00
aclist edb87893f9 chore: clear typehinting errors 2026-10-23 11:46:05 +09:00
aclist 519e6c354d chore: patch VDF changes from main 2026-10-23 11:42:08 +09:00
aclist e3d1d62b4d Merge pull request #499 from aclist/fix/vdf-missing
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: missing shortcuts file
2026-10-23 11:38:48 +09:00
aclist 1dcfdda657 change: allow D-Bus registration when sandboxed 2026-10-23 11:36:13 +09:00
aclist 019847dcf9 fix: handle missing grid directory 2026-10-23 11:35:13 +09:00
aclist c2cfe2b4d9 fix: handle missing shortcut file 2026-10-23 11:34:52 +09:00
aclist 01b3babdf5 feat: FlatpakOverridePage WIP 2026-10-23 11:23:28 +09:00
aclist e286f02342 chore: add flatpak toggle test 2026-10-22 12:36:23 +09:00
aclist 9f3c39fdd6 chore: drop unused print statement 2026-10-22 12:35:56 +09:00
aclist 0359aacd54 chore: clear typehinting errors 2026-10-22 12:09:40 +09:00
aclist 80e127889e chore: drop unused import 2026-10-22 12:09:07 +09:00
aclist da98b1d5b1 feat: toggle flatpak client dynamically 2026-10-22 12:08:44 +09:00
aclist 33869158de change: explicitly disable NSG creation on Flatpak 2026-10-22 12:07:59 +09:00
aclist 5cb8c68f5c Merge pull request #496 from aclist/fix/always-show-icon
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: always show icon
2026-10-22 10:16:44 +09:00
aclist 2f92c2a44f chore: clear typehinting errors 2026-10-22 10:12:38 +09:00
aclist da48834fa7 feat: load Steam icon into search paths 2026-10-22 10:11:55 +09:00
aclist 197c2eba06 fix: always show icon 2026-10-22 10:11:13 +09:00
aclist 0131ee6f0e Merge pull request #486 from aclist/change/flatpak-sens
change: set flatpak option sensitivity dynamically
2026-10-22 08:20:09 +09:00
aclist b70edebd05 Merge branch 'dzgui7' into change/flatpak-sens
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
2026-10-21 16:44:56 +09:00
aclist ae1c3a19d4 chore: update tests 2026-10-21 16:35:01 +09:00
aclist 7822672cda chore: clear typehinting errors 2026-10-21 16:33:17 +09:00
aclist f2a7bde432 fix: collision with steam-runtime-launcher-service 2026-10-21 16:32:38 +09:00
aclist 00b5476aaf change: treat steam clients as separate processes 2026-10-21 14:51:56 +09:00
aclist 2a08b9ba82 chore: clear typehinting errors 2026-10-21 13:04:12 +09:00
aclist 0f8b6fcaad change: drop client combo from wizard 2026-10-21 13:03:30 +09:00
aclist 95988459d5 chore: drop unused import 2026-10-21 13:00:06 +09:00
aclist eed40e6f1a change: return client name from controller 2026-10-21 12:59:54 +09:00
aclist 07460a1f8f chore: clear typehinting errors 2026-10-21 12:44:41 +09:00
aclist a74d576387 change: interpolate client name from full command 2026-10-21 12:43:20 +09:00
aclist 052f7f0708 chore: remove client combo from options page 2026-10-21 12:42:50 +09:00
aclist 6feae44887 chore: add import 2026-10-21 12:41:10 +09:00
aclist 8cdb615796 chore: drop unused imports 2026-10-21 12:40:56 +09:00
aclist 31b6705017 change: parse client from path 2026-10-21 12:40:03 +09:00
aclist ef1e7d17f5 chore: drop unused functions 2026-10-21 12:39:23 +09:00
aclist 3c52072eb5 Merge pull request #493 from aclist/feat/paused-downloads
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
feat: paused downloads
2026-10-21 10:26:51 +09:00
aclist bf69936891 Merge pull request #495 from aclist/fix/vdf-timestamp
fix: support changes to VDF timestamp
2026-10-21 10:22:35 +09:00
aclist fa180b750a Merge pull request #494 from aclist/change/unmodded-filter
change: separate modded into unmodded
2026-10-21 10:20:46 +09:00
aclist 96d50ffafa chore: resolve pytest errors 2026-10-18 07:44:31 +09:00
aclist 6c801ffec5 chore: update tests 2026-10-18 07:44:14 +09:00
aclist 5eb70a1eb8 fix: support changes to VDF format 2026-10-18 07:35:17 +09:00
aclist a3fff40f8f change: separate modded into unmodded 2026-10-18 01:54:59 +09:00
aclist 343630d2da chore: update placeholder appid 2026-10-18 01:52:43 +09:00
aclist d63c7ce3ec chore: add test 2026-10-18 01:50:10 +09:00
aclist f7ad357e6f feat: print paused download warning on preconnect 2026-10-18 01:49:14 +09:00
aclist 035a01fe34 feat: check for pending patches 2026-10-18 01:48:42 +09:00
aclist 7dd14804aa Merge pull request #491 from aclist/fix/dword-parse
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: dword parse
2026-10-16 00:09:26 +09:00
aclist 1cc1ba2864 chore: bump version 2026-10-16 00:06:54 +09:00
aclist 8c4d0aecdc fix: parse alternate VS_FIXEDFILEINFO structure 2026-10-16 00:06:37 +09:00
aclist 616f18452a Merge pull request #489 from aclist/fix/badlands
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: badlands
2026-10-15 19:26:43 +09:00
aclist 1064f7a9d6 chore: clear typehinting errors 2026-10-15 19:23:42 +09:00
aclist b0e85f0e35 fix: update Badlands flag 2026-10-15 19:22:44 +09:00
aclist 4eec00e8e7 chore: trigger checks 2026-10-15 00:36:09 +09:00
aclist 09ef3e446a change: set flatpak option sensitivity dynamically 2026-10-15 00:35:02 +09:00
aclist 4d02604bcf Merge pull request #485 from aclist/fix/gint-warning
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: use more stable GObject signal
2026-10-15 00:32:28 +09:00
aclist ebffbe39f2 fix: use more stable GObject signal 2026-10-15 00:23:11 +09:00
aclist 61860f6aee Merge pull request #482 from aclist/fix/duplicate-warnings
Fix/duplicate warnings
2026-10-14 23:12:18 +09:00
aclist 191776ce74 fix: use correct logger method 2026-10-14 23:11:29 +09:00
aclist 7ae34747e9 fix: add missing dummy method to test 2026-10-14 23:07:22 +09:00
aclist 1555ba2d64 chore: drop no longer relevant filter 2026-10-14 23:07:00 +09:00
aclist 4bac804fd7 fix: duplicate warnings (#480) 2026-10-14 23:06:41 +09:00
aclist 748e1870da Merge pull request #479 from sylvaingirardbe/fix/478-flatpak-steam-detection
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: detect running Flatpak Steam correctly
2026-10-14 23:03:43 +09:00
aclist cd4c6fdfb2 Merge pull request #481 from aclist/fix/workshop-403
fix: handle HTTPErrors from workshop (#477)
2026-10-14 23:01:48 +09:00
aclist 1752018331 fix: handle HTTPErrors from workshop (#477) 2026-10-14 22:53:02 +09:00
aclist b3b05918b7 chore: add test case 2026-10-09 06:45:59 +09:00
aclist e6e0a3036d fix: drop unused assignment 2026-10-09 04:03:36 +09:00
aclist 8c44a6e255 fix: user-facing Steam client name not available until Options populated
The old method was checking the active ComboBox string after the Options
page was initialized, but before it was actually populated with the real
setting. This caused it to always return the string 'Steam' instead of
the actual client name.
2026-10-09 04:03:17 +09:00
aclist 7fc1264b65 fix: false positive when Flatpak client is running
The process ultimately spawned from the Flatpak run command has the same
process name as the one expected for STEAM_CMD (essentially just the
Steam client wrapped in a Flatpak), so the old logic would return True
for is_steam_running() if the Flatpak version had already been launched.
To prevent unintended collisions and isolate launch parameters,
STEAM_CMD and FLATPAK_RUN_CMD/FLATPAK_SANDBOX should be
mutually exclusive.
2026-10-09 04:02:58 +09:00
Sylvain Girard f748b09ba7 fix: detect running Flatpak Steam correctly
Request only application IDs from flatpak ps so running Steam instances match correctly. Fix client selection validation and cover detection with regression tests.

Refs aclist/dztui#478.
2026-10-08 15:58:51 +02:00
aclist 01be1247b4 Merge pull request #476 from aclist/fix/missing-mission
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: missing mission
2026-09-24 13:48:24 +09:00
45 changed files with 723 additions and 185 deletions

No files matched your search

+20 -8
View File
@@ -255,16 +255,28 @@ class PeFileError(Exception):
pass pass
def parse_version_number(data: BinaryIO) -> FileVersion: def parse_version_number(data: BinaryIO) -> FileVersion:
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo # https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF """
major = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF Note: structure differs from standard VS_FIXEDFILEINFO
build = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF Major: lower 16 bits of first DWORD
# NOTE: 0-pad revision number Minor: upper 16 bits of first DWORD
revision = str(struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF) Build: lower 16 bits of third DWORD
offset = 3 - len(revision) Revision: upper 16 bits of second DWORD, zero-padded
padded = "0" * offset + revision Lower 16 bits of revision DWORD appear unused.
return FileVersion(major, minor, build, padded) """
major_minor = struct.unpack("<I", data.read(4))[0]
revision_pair = struct.unpack("<I", data.read(4))[0]
build_dword = struct.unpack("<I", data.read(4))[0]
major = major_minor & 0xFFFF
minor = (major_minor >> 16) & 0xFFFF
build = build_dword & 0xFFFF
revision = (revision_pair >> 16) & 0xFFFF
revision = f"{revision:03d}"
return FileVersion(major, minor, build, revision)
def seek_to_hex(address: str, data: BinaryIO) -> None: def seek_to_hex(address: str, data: BinaryIO) -> None:
+1 -1
View File
@@ -346,7 +346,7 @@ def get_details(record: Record) -> Details | None:
dlc = strings.dlc_none dlc = strings.dlc_none
case 2: case 2:
dlc = strings.dlc_frostline dlc = strings.dlc_frostline
case 3: case 4:
dlc = strings.dlc_badlands dlc = strings.dlc_badlands
case _: case _:
dlc = strings.unspecified dlc = strings.unspecified
+8 -1
View File
@@ -11,7 +11,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, make_parents
from dzgui.util.strings import unknown from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -75,6 +75,8 @@ class Shortcuts:
shortcuts = vdf.binary_load(f) shortcuts = vdf.binary_load(f)
self.shortcuts = shortcuts self.shortcuts = shortcuts
logger.debug("Loaded shortcuts file") logger.debug("Loaded shortcuts file")
except FileNotFoundError:
self.shortcuts = {"shortcuts": {}}
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
raise e raise e
@@ -109,6 +111,7 @@ class Shortcuts:
for img in ("_hero", "p", "_logo"): for img in ("_hero", "p", "_logo"):
filename = str(self.bpid) + img filename = str(self.bpid) + img
dest = grid_path.joinpath(filename).with_suffix(".png") dest = grid_path.joinpath(filename).with_suffix(".png")
make_parents(dest)
b = images.joinpath(img).with_suffix(".png").read_bytes() b = images.joinpath(img).with_suffix(".png").read_bytes()
dest.write_bytes(b) dest.write_bytes(b)
@@ -156,6 +159,7 @@ class Shortcuts:
NEW_ENTRY["DevkitOverrideAppID"] = 0 NEW_ENTRY["DevkitOverrideAppID"] = 0
NEW_ENTRY["LastPlayTime"] = 0 NEW_ENTRY["LastPlayTime"] = 0
NEW_ENTRY["FlatpakAppID"] = "" NEW_ENTRY["FlatpakAppID"] = ""
NEW_ENTRY["sortas"] = "" NEW_ENTRY["sortas"] = ""
NEW_ENTRY["tags"] = {} NEW_ENTRY["tags"] = {}
return NEW_ENTRY return NEW_ENTRY
@@ -172,6 +176,9 @@ class Shortcuts:
try: try:
backup = self.shortcuts_path.with_suffix(".vdf.bak") backup = self.shortcuts_path.with_suffix(".vdf.bak")
shutil.copy(self.shortcuts_path, backup) shutil.copy(self.shortcuts_path, backup)
except Exception as e:
logger.critical(e)
try:
with open(self.shortcuts_path, "wb") as f: with open(self.shortcuts_path, "wb") as f:
vdf.binary_dump(self.shortcuts, f) vdf.binary_dump(self.shortcuts, f)
except Exception as e: except Exception as e:
+51 -5
View File
@@ -1,16 +1,19 @@
import logging import logging
import os import os
import psutil import psutil
import re
import requests import requests
import subprocess import subprocess
import vdf # type: ignore import vdf # type: ignore
from pathlib import Path from pathlib import Path
from typing import Any, Union from requests.exceptions import HTTPError
from typing import Any, Generator, Union
from warnings import deprecated from warnings import deprecated
from dzgui.api.acf import ACF from dzgui.api.acf import ACF
from dzgui.init.prereqs import has_steam_client from dzgui.init.prereqs import has_any_client
from dzgui.init.proc import has_cmd
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
@@ -19,6 +22,8 @@ from dzgui.const.constants import (
DAYZ_LAUNCHER, DAYZ_LAUNCHER,
DEBIAN_STEAM_PATH, DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH, DEFAULT_STEAM_PATH,
DOWNLOADS_PATH,
FLATPAK_CMD,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
LIBRARYFOLDERS_PATH, LIBRARYFOLDERS_PATH,
UBUNTU_STEAM_PATH, UBUNTU_STEAM_PATH,
@@ -32,6 +37,7 @@ from dzgui.const.endpoints import (
UNSUB_ENDPOINT, UNSUB_ENDPOINT,
) )
from dzgui.strings import wizard from dzgui.strings import wizard
from dzgui.strings.errors import api_validation_error, workshop_endpoint_error
from dzgui.util.bash import concat_bash_args from dzgui.util.bash import concat_bash_args
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -57,7 +63,7 @@ class VDFLoadError(Exception):
def get_steam_paths() -> list[tuple[Path, str]]: def get_steam_paths() -> list[tuple[Path, str]]:
paths = [] paths = []
if has_steam_client(): if has_any_client():
HOME = Path.home() HOME = Path.home()
env = os.environ.get("XDG_DATA_HOME") env = os.environ.get("XDG_DATA_HOME")
if env is not None: if env is not None:
@@ -78,6 +84,8 @@ def get_steam_paths() -> list[tuple[Path, str]]:
} }
for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH: for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH:
if path is FLATPAK_PATH and has_cmd(FLATPAK_CMD) is False:
continue
if path.joinpath(VDF_PATH).is_file(): if path.joinpath(VDF_PATH).is_file():
paths.append((path, human[path])) paths.append((path, human[path]))
return paths return paths
@@ -194,6 +202,7 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
"""Loads to the menu screen with the selected mods; used for AFK/pre-joining""" """Loads to the menu screen with the selected mods; used for AFK/pre-joining"""
concat = concat_mods(mods) concat = concat_mods(mods)
client_args = concat_bash_args(client) client_args = concat_bash_args(client)
logger.debug(f"Setting client arguments to: {client_args}")
params = [ params = [
"-applaunch", "-applaunch",
str(appid), str(appid),
@@ -243,9 +252,9 @@ def find_user_id(path: Path) -> str | None:
last_user = str(next(iter(users))) last_user = str(next(iter(users)))
if len(users) == 1: if len(users) == 1:
return last_user return last_user
last_stamp = int(users[last_user]["Timestamp"]) last_stamp = int(users[last_user]["timestamp"])
for user in users: for user in users:
stamp = int(users[user]["Timestamp"]) stamp = int(users[user]["timestamp"])
if stamp > last_stamp: if stamp > last_stamp:
last_stamp = stamp last_stamp = stamp
last_user = user last_user = user
@@ -262,6 +271,16 @@ def find_user_id_32(path: Path) -> int:
return int(uid) & 0xFFFFFFFF return int(uid) & 0xFFFFFFFF
class SteamAPIKeyError(Exception):
def __init__(self, msg: str | None = None) -> None:
super().__init__(msg or api_validation_error)
class SteamWorkshopEndpointError(Exception):
def __init__(self, msg: str | None = None) -> None:
super().__init__(msg or workshop_endpoint_error)
def update_workshop(key: str, mod: int, endpoint: str) -> None: def update_workshop(key: str, mod: int, endpoint: str) -> None:
payload: dict[str, Union[int, str]] = { payload: dict[str, Union[int, str]] = {
"publishedfileid": mod, "publishedfileid": mod,
@@ -273,8 +292,19 @@ def update_workshop(key: str, mod: int, endpoint: str) -> None:
try: try:
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT) res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
res.raise_for_status() res.raise_for_status()
except HTTPError as e:
logger.critical(e)
match res.status_code:
case 403:
raise SteamAPIKeyError()
case 404:
raise SteamWorkshopEndpointError()
case _:
# TODO: apply redaction filter
raise e
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
raise e
def subscribe(key: str, mod: int) -> None: def subscribe(key: str, mod: int) -> None:
@@ -416,6 +446,22 @@ def is_dayz_running() -> bool:
return any(s in item for sublist in procs for item in sublist for s in substring) return any(s in item for sublist in procs for item in sublist for s in substring)
def get_pending_downloads(steam_path: Path, appid: int) -> Generator[Path, None, None]:
root_path = get_app_path(steam_path, appid)
downloads_dir = root_path / DOWNLOADS_PATH
files = downloads_dir.iterdir()
return files
def is_dayz_downloading(steam_path: Path, appid: int) -> bool:
files = get_pending_downloads(steam_path, appid)
reg = re.compile(rf"^state_(?:{APPID_DAYZ_EXP}|{APPID_DAYZ})_.*\.patch$")
pending = [f for f in files if reg.match(f.name)]
if len(pending) > 0:
return True
return False
def get_app_path(folders_path: Path, appid: int) -> Path: def get_app_path(folders_path: Path, appid: int) -> Path:
app_path = None app_path = None
+40 -5
View File
@@ -1,10 +1,18 @@
import logging import logging
import os import os
import shutil import shutil
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from warnings import deprecated
from dzgui.const.constants import APP_NAME from dzgui.const.constants import (
APP_NAME,
FLATPAK_ROOT_PATH,
STEAM_ICON,
STEAM_ICONS_PATH,
)
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
@@ -15,7 +23,7 @@ from dzgui.init.migrate import (
migrate_cols_file, migrate_cols_file,
) )
from dzgui.init.flock import lock_acquire from dzgui.init.flock import lock_acquire
from dzgui.init.prereqs import has_steam_client from dzgui.init.prereqs import has_flatpak_client, has_steam_client
from dzgui.strings import boot from dzgui.strings import boot
from dzgui.util.deck import is_steam_deck, is_game_mode from dzgui.util.deck import is_steam_deck, is_game_mode
@@ -23,7 +31,7 @@ from dzgui.util.dirs import make_parents
from dzgui.util.localize import set_locale from dzgui.util.localize import set_locale
from dzgui.util.map_count import test_map_count from dzgui.util.map_count import test_map_count
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
from dzgui.util.strings import init from dzgui.util.strings import init, steam_icon_missing
from dzgui.views.base import App from dzgui.views.base import App
from dzgui.views.dialogs.boot import BootWindow from dzgui.views.dialogs.boot import BootWindow
@@ -96,6 +104,28 @@ def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
return (config_changed, state_changed) return (config_changed, state_changed)
@deprecated("Currently unused")
def load_steam_icon(config: Path) -> None:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
default = lookup(config, Preferences.DEFAULT)
icon_path = Path(default).joinpath(STEAM_ICONS_PATH)
try:
theme = Gtk.IconTheme.get_default()
paths = theme.get_search_path()
theme.set_search_path(paths + [str(icon_path)])
paths = theme.get_search_path()
icons = theme.list_icons(None)
if STEAM_ICON not in icons:
logger.warning(steam_icon_missing)
except Exception as e:
print(e)
logger.warning(e)
def load_gui(version: str, is_debug: bool) -> None: def load_gui(version: str, is_debug: bool) -> None:
lock = lock_acquire() # noqa lock = lock_acquire() # noqa
@@ -123,7 +153,7 @@ def load_gui(version: str, is_debug: bool) -> None:
migrate_cols_file(XDG.columns) migrate_cols_file(XDG.columns)
copy_state_files(xdg_paths["XDG_STATE_HOME"]) copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard # TODO: add logging inside wizard
wizard = SetupWizard(_is_steam_deck, XDG) wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths)
if not wizard.is_setup_complete(): if not wizard.is_setup_complete():
return return
@@ -136,13 +166,18 @@ def load_gui(version: str, is_debug: bool) -> None:
# TODO: sudo escalation dialog # TODO: sudo escalation dialog
# count = get_map_count() # count = get_map_count()
# TODO: move into module # TODO: move into module
if has_steam_client() is False: if FLATPAK_ROOT_PATH in lookup(XDG.config, Preferences.DEFAULT):
has_client = has_flatpak_client()
else:
has_client = has_steam_client()
if has_client is False:
EarlyAlertDialog(init.requires_steam) EarlyAlertDialog(init.requires_steam)
bootwin = BootWindow(XDG, version) bootwin = BootWindow(XDG, version)
local_coords, latest_release = bootwin.get_results() local_coords, latest_release = bootwin.get_results()
use_miles = lookup(XDG.config, Preferences.DIST) use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs( prefs = UserPrefs(
is_steam_deck=_is_steam_deck, is_steam_deck=_is_steam_deck,
is_game_mode=_is_game_mode, is_game_mode=_is_game_mode,
-20
View File
@@ -4,8 +4,6 @@ from pathlib import Path
from dzgui.util._json import read_json from dzgui.util._json import read_json
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.const.constants import STEAM_CMD, FLATPAK_RUN_CMD, FLATPAK_SANDBOX
class ConfigFileError(Exception): class ConfigFileError(Exception):
pass pass
@@ -40,23 +38,5 @@ def get_favorites(path: Path) -> list[str]:
return list(conf["ip_list"]) return list(conf["ip_list"])
# def is_in_favs(record: str, path: Path) -> bool:
# favs = get_favorites(path)
# if record in favs:
# return True
# return False
def enum_to_key(enum: Preferences) -> str: def enum_to_key(enum: Preferences) -> str:
return str(enum.dict["key"]) return str(enum.dict["key"])
def get_client_index(client: str) -> int:
if client == STEAM_CMD:
return 0
elif client == FLATPAK_RUN_CMD:
return 1
elif client == FLATPAK_SANDBOX:
return 2
else:
return 0
+10 -2
View File
@@ -3,7 +3,13 @@ import os
from pathlib import Path from pathlib import Path
from dataclasses import dataclass from dataclasses import dataclass
from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG from dzgui.const.constants import (
APP_NAME_LOWER,
DEBUG_LOG,
FLATPAK_ROOT_PATH,
SYSTEM_LOG,
)
from dzgui.init.proc import is_sandboxed
@dataclass @dataclass
@@ -53,10 +59,12 @@ def get_xdg_paths() -> dict:
"XDG_DATA_HOME": home / ".local" / "share", "XDG_DATA_HOME": home / ".local" / "share",
"XDG_CACHE_HOME": home / ".cache", "XDG_CACHE_HOME": home / ".cache",
} }
resolved_paths = {} resolved_paths = {}
for path in xdg_paths: for path in xdg_paths:
real_path = os.environ.get(path) real_path = os.environ.get(path)
if real_path is not None and is_sandboxed():
search_string = FLATPAK_ROOT_PATH + "/"
real_path = real_path.replace(search_string, "")
if real_path is not None and is_writeable(real_path): if real_path is not None and is_writeable(real_path):
new_path = Path(real_path) new_path = Path(real_path)
else: else:
+4 -1
View File
@@ -15,6 +15,7 @@ DAYZ_BINARY = "DayZ_x64.exe"
DAYZ_LAUNCHER = "DayZLauncher.exe" DAYZ_LAUNCHER = "DayZLauncher.exe"
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf" LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
DOWNLOADS_PATH = "steamapps/downloading"
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ) WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
STEAM_CMD = "steam" STEAM_CMD = "steam"
@@ -84,9 +85,11 @@ HERO_PATH = "data/images/_hero.png"
CSS_PATH = "data/app.css" CSS_PATH = "data/app.css"
VDF_PATH = "steamapps/libraryfolders.vdf" VDF_PATH = "steamapps/libraryfolders.vdf"
DEFAULT_STEAM_PATH = ".local/share/Steam" DEFAULT_STEAM_PATH = ".local/share/Steam"
FLATPAK_STEAM_PATH = ".var/app/com.valvesoftware.Steam/data/Steam" FLATPAK_ROOT_PATH = ".var/app/com.valvesoftware.Steam"
FLATPAK_STEAM_PATH = FLATPAK_ROOT_PATH + "/data/Steam"
UBUNTU_STEAM_PATH = ".steam/steam" UBUNTU_STEAM_PATH = ".steam/steam"
DEBIAN_STEAM_PATH = ".steam/debian-installation" DEBIAN_STEAM_PATH = ".steam/debian-installation"
STEAM_ICONS_PATH = "public"
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG") LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
+4 -2
View File
@@ -541,8 +541,10 @@ class Controller(GObject.GObject):
def update_status(self) -> None: def update_status(self) -> None:
self.mediator.preconnect.mark_finished() self.mediator.preconnect.mark_finished()
def get_steam_client_name(self) -> str: def get_client_name(self) -> str:
return self.mediator.options.get_client_name() # NOTE: only possible values are "Steam" and "Flatpak"
cmd = self.query_config(Preferences.CLIENT)
return str(cmd.split()[0].capitalize())
def present_window(self) -> None: def present_window(self) -> None:
self.mediator.window.present_with_time(Gdk.CURRENT_TIME) self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
+20 -7
View File
@@ -7,19 +7,32 @@ from dzgui.const.constants import APP_NAME, FLATPAK_APPID, FLATPAK_CMD, STEAM_CM
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
def has_steam_client() -> bool: def has_flatpak_client() -> bool:
if shutil.which(STEAM_CMD):
return True
if not shutil.which(FLATPAK_CMD):
return False
try: try:
res = subprocess.run( res = subprocess.run(
[FLATPAK_CMD, "list"], capture_output=True, text=True, check=True [FLATPAK_CMD, "list", "--columns=application"],
capture_output=True,
text=True,
check=True,
) )
if FLATPAK_APPID in res.stdout: if FLATPAK_APPID in res.stdout:
return True return True
else: else:
return False return False
except subprocess.CalledProcessError as e: except Exception as e:
logger.critical(e) logger.critical(e)
return False return False
def has_steam_client() -> bool:
if shutil.which(STEAM_CMD):
return True
return False
def has_any_client() -> bool:
if shutil.which(STEAM_CMD):
return True
if not shutil.which(FLATPAK_CMD):
return False
return has_flatpak_client()
+42 -16
View File
@@ -1,7 +1,8 @@
import psutil
import subprocess
import shutil
import logging import logging
import os
import psutil
import shutil
import subprocess
from dzgui.const.constants import ( from dzgui.const.constants import (
APP_NAME, APP_NAME,
@@ -10,42 +11,67 @@ from dzgui.const.constants import (
FLATPAK_CMD, FLATPAK_CMD,
FLATPAK_RUN_CMD, FLATPAK_RUN_CMD,
FLATPAK_SANDBOX, FLATPAK_SANDBOX,
FLATPAK_ROOT_PATH,
FLATPAK_STEAM_PATH,
) )
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
def is_steam_running(cmd: str) -> bool: def is_steam_running(cmd: str) -> bool:
if cmd == STEAM_CMD: if cmd in (FLATPAK_RUN_CMD, FLATPAK_SANDBOX):
if has_cmd(STEAM_CMD) is False:
return False
return is_running(STEAM_CMD)
elif cmd == FLATPAK_RUN_CMD or FLATPAK_SANDBOX:
return is_flatpak_steam_running() return is_flatpak_steam_running()
elif cmd == STEAM_CMD:
return is_native_steam_running()
else: else:
raise TypeError("Not a valid Steam client selection") raise TypeError("Not a valid Steam client selection")
# CHORE: test alternate clients def is_sandboxed() -> bool:
# TODO: has_flatpak_steam return bool(os.environ.get("FLATPAK_ID"))
def is_flatpak_steam_running() -> bool: def is_flatpak_steam_running() -> bool:
if is_sandboxed():
return True
if has_cmd(FLATPAK_CMD) is False: if has_cmd(FLATPAK_CMD) is False:
return False return False
proc = subprocess.check_output([FLATPAK_CMD, "ps"], text=True) proc = subprocess.check_output(
[FLATPAK_CMD, "ps", "--columns=application"], text=True
)
lines = proc.splitlines() lines = proc.splitlines()
if FLATPAK_APPID in lines: if FLATPAK_APPID in lines:
return True return True
return False return False
def is_running(proc: str) -> bool: def get_steam_procs() -> list[list[str]]:
for process in psutil.process_iter(["pid", "name"]): return [
if process.info["name"] == proc: p.cmdline()
return True for p in psutil.process_iter(["pid", "name"])
return False if STEAM_CMD in p.info["name"]
]
def is_native_steam_running() -> bool:
if has_cmd(STEAM_CMD) is False:
return False
substrings = [FLATPAK_ROOT_PATH, "steam-runtime-launcher-service"]
steam_procs = get_steam_procs()
native_steam_procs = [
proc
for proc in steam_procs
if not any(sub in item for item in proc for sub in substrings)
]
return bool(native_steam_procs)
def has_cmd(cmd: str) -> bool: def has_cmd(cmd: str) -> bool:
if shutil.which(cmd) is not None: if shutil.which(cmd) is not None:
return True return True
return False return False
def path_to_steam_cmd(path: str) -> str:
cmd = FLATPAK_RUN_CMD if FLATPAK_STEAM_PATH in path else STEAM_CMD
return cmd
-12
View File
@@ -6,9 +6,6 @@ from typing import Any, TYPE_CHECKING
from dzgui.const.constants import ( from dzgui.const.constants import (
APP_NAME, APP_NAME,
FLATPAK_RUN_CMD,
FLATPAK_SANDBOX,
STEAM_CMD,
WINDOW_DEFAULT_X, WINDOW_DEFAULT_X,
WINDOW_DEFAULT_Y, WINDOW_DEFAULT_Y,
) )
@@ -129,15 +126,6 @@ class ConfigManager:
def enum_to_key(self, enum: Preferences) -> str: def enum_to_key(self, enum: Preferences) -> str:
return str(enum.dict["key"]) return str(enum.dict["key"])
def get_client_index(self, client: str) -> int:
if client == STEAM_CMD:
return 0
if client == FLATPAK_RUN_CMD:
return 1
if client == FLATPAK_SANDBOX:
return 2
return 0
def toggle_config(self, key: Preferences) -> None: def toggle_config(self, key: Preferences) -> None:
try: try:
real_key = self.enum_to_key(key) real_key = self.enum_to_key(key)
+19 -4
View File
@@ -14,6 +14,7 @@ from dzgui.api.shortcuts import Shortcuts
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
connect_debug, connect_debug,
is_dayz_downloading,
get_app_allows_downloads, get_app_allows_downloads,
get_app_name, get_app_name,
get_needs_update, get_needs_update,
@@ -48,7 +49,7 @@ from dzgui.strings.dialogs import (
) )
from dzgui.strings import kb from dzgui.strings import kb
from dzgui.strings.server_mods import checkmark, resync from dzgui.strings.server_mods import checkmark, resync
from dzgui.util.format import format_mib from dzgui.util.format import format_exception, format_mib
from dzgui.util.strings import dialog, server_timeout from dzgui.util.strings import dialog, server_timeout
from dzgui.util.symlink import rebuild_symlinks from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
@@ -84,6 +85,7 @@ class Prerequisites:
available_space: float available_space: float
passworded: bool passworded: bool
dayz_running: bool dayz_running: bool
download_paused: bool
steam_proc: SteamProcess steam_proc: SteamProcess
mods: list[list[str]] mods: list[list[str]]
game_mode: bool game_mode: bool
@@ -169,6 +171,7 @@ class ConnectionManager:
free_mib = format_mib(free) free_mib = format_mib(free)
dayz_running = is_dayz_running() dayz_running = is_dayz_running()
download_paused = is_dayz_downloading(steam_path, self.appid)
running_app = get_running_app() running_app = get_running_app()
if running_app is not None: if running_app is not None:
@@ -184,8 +187,8 @@ class ConnectionManager:
else: else:
allows_downloads = (True, "") allows_downloads = (True, "")
client_name = self.controller.get_steam_client_name()
client = self.controller.query_config(Preferences.CLIENT) client = self.controller.query_config(Preferences.CLIENT)
client_name = self.controller.get_client_name()
running = is_steam_running(client) running = is_steam_running(client)
steam_proc = SteamProcess(client_name, running) steam_proc = SteamProcess(client_name, running)
self.client = client self.client = client
@@ -210,6 +213,7 @@ class ConnectionManager:
available_space=free_mib, available_space=free_mib,
passworded=info.password_protected, passworded=info.password_protected,
dayz_running=dayz_running, dayz_running=dayz_running,
download_paused=download_paused,
steam_proc=steam_proc, steam_proc=steam_proc,
mods=remote_mods, mods=remote_mods,
game_mode=game_mode, game_mode=game_mode,
@@ -300,6 +304,11 @@ class ConnectionManager:
dialog = ServerDetailsDialog(self.controller, details) dialog = ServerDetailsDialog(self.controller, details)
dialog.run() dialog.run()
def _workshop_timeout(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg)
dialog.show_all()
dialog.run()
def _server_timeout(self) -> None: def _server_timeout(self) -> None:
dialog = ExceptionDialog(self.controller, server_timeout) dialog = ExceptionDialog(self.controller, server_timeout)
dialog.set_details_buffer(kb.DZG_006) dialog.set_details_buffer(kb.DZG_006)
@@ -351,8 +360,14 @@ class ConnectionManager:
for title, mod, stamp, size in self.missing_mods: for title, mod, stamp, size in self.missing_mods:
if self.controller.is_cancel_pending(): if self.controller.is_cancel_pending():
return return
subscribe(key, int(mod)) try:
time.sleep(API_RATE_LIMIT) subscribe(key, int(mod))
time.sleep(API_RATE_LIMIT)
except Exception as e:
msg = format_exception(e)
func = StoredFunc(self._workshop_timeout, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
return
self.thread_man.update_dialog(waiting_for_directories) self.thread_man.update_dialog(waiting_for_directories)
for title, mod, stamp, size in self.missing_mods: for title, mod, stamp, size in self.missing_mods:
+4 -2
View File
@@ -22,11 +22,12 @@ class FilterManager:
strings.filter_night: True, strings.filter_night: True,
strings.filter_full: show_full, strings.filter_full: show_full,
strings.filter_official: True, strings.filter_official: True,
strings.filter_nonascii: False, strings.filter_modded: True,
strings.filter_lowpop: True, strings.filter_lowpop: True,
strings.filter_unofficial: True, strings.filter_unofficial: True,
strings.filter_unmodded: True,
strings.filter_duplicate: False, strings.filter_duplicate: False,
strings.filter_modded: True, strings.filter_nonascii: False,
} }
# TODO: strings # TODO: strings
@@ -43,6 +44,7 @@ class FilterManager:
strings.filter_unofficial: "Third-party servers", strings.filter_unofficial: "Third-party servers",
strings.filter_duplicate: "Duplicates of existing servers (usually spoofed)", strings.filter_duplicate: "Duplicates of existing servers (usually spoofed)",
strings.filter_modded: "Servers with one or more mods", strings.filter_modded: "Servers with one or more mods",
strings.filter_unmodded: "Servers with no mods",
} }
self.active_keyword = "" self.active_keyword = ""
+5 -1
View File
@@ -122,7 +122,11 @@ class ModManager:
def unsub_atomic_mod(self, mod: str) -> None: def unsub_atomic_mod(self, mod: str) -> None:
config_man = self.controller.get_config_man() config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM) key = config_man.lookup(Preferences.STEAM)
unsubscribe(key, int(mod)) try:
unsubscribe(key, int(mod))
except Exception as e:
logger.critical(e)
return
steam_path = Path(self.path) steam_path = Path(self.path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
+2
View File
@@ -243,6 +243,8 @@ class ProxyModelManager:
rows = [row for row in rows if (row[4] / row[5] * 100) > 30] rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
case strings.filter_modded: case strings.filter_modded:
rows = [row for row in rows if not row[11]] rows = [row for row in rows if not row[11]]
case strings.filter_unmodded:
rows = [row for row in rows if row[11]]
return rows return rows
def filter_toggle_on(self, filters: tuple[str]) -> list: def filter_toggle_on(self, filters: tuple[str]) -> list:
+1
View File
@@ -4,3 +4,4 @@ api_validation_error = (
api_popover = "API key validation failed." api_popover = "API key validation failed."
dayz_not_found = "Could not find a valid DayZ installation." dayz_not_found = "Could not find a valid DayZ installation."
workshop_endpoint_error = "Steam Workshop service unavailable, try again later."
+7
View File
@@ -17,3 +17,10 @@ fullscreen_eventbox = "This option is not available on Steam Deck."
save_button = "Save" save_button = "Save"
api_failed = "API validation failed" api_failed = "API validation failed"
flatpak_label = "Flatpak client"
flatpak_standard = "Standard"
flatpak_container = "Containerized (flatpak-spawn --host)"
flatpak_info = (
"Containerized mode is used on certain distributions. May have side effects."
)
+4
View File
@@ -47,3 +47,7 @@ running_app = (
"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"
"Otherwise, mods may be queued for download but never update." "Otherwise, mods may be queued for download but never update."
) )
downloads_paused = (
"It looks like DayZ may have a pending update on Steam that was paused.\n"
"To enqueue mods for download, unpause any pending DayZ update and allow it to complete."
)
+15 -1
View File
@@ -3,6 +3,8 @@ title_intro = "Welcome"
blurb_intro = """ blurb_intro = """
This wizard will help you set up some common config options before launching the application. This wizard will help you set up some common config options before launching the application.
""" """
error_sandbox = "Setup cannot be run from inside Flatpak sandbox.\nPlease launch the application directly."
welcome = "Welcome to {0}!"
### SteamPathPage ### SteamPathPage
heading_steam_path = "Steam path" heading_steam_path = "Steam path"
@@ -70,7 +72,8 @@ checkbox_steam_shortcut = (
( (
"This creates a standalone copy of DZGUI and adds it to Steam. " "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. " "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." "If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version. "
"Not available if you selected the Flatpak client as your Steam default."
), ),
) )
checkbox_start_menu = ( checkbox_start_menu = (
@@ -79,6 +82,17 @@ checkbox_start_menu = (
) )
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.") checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
### Flatpak page
heading_flatpak = "Flatpak sandbox overrides"
blurb_flatpak = (
"You chose to add a shortcut directly to Flatpak Steam. "
"To launch DZGUI from Flatpak's sandbox, you must grant "
"permission to read from and write to the following locations."
)
tree_column = "Paths"
overrides_applied = "Overrides applied."
button_flatpak = "Grant permissions"
### Completion page ### Completion page
heading_completion = "Setup complete" heading_completion = "Setup complete"
blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI." blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI."
+7
View File
@@ -1,5 +1,12 @@
import shlex import shlex
from dzgui.const.constants import STEAM_CMD
from dzgui.init.proc import is_sandboxed
def concat_bash_args(command: str) -> list[str]: def concat_bash_args(command: str) -> list[str]:
# NOTE: NSGs spawned from sandbox cannot access host application commands,
# so fallback to Steam internal URIs
if is_sandboxed():
return [STEAM_CMD]
return shlex.split(command) return shlex.split(command)
+31
View File
@@ -0,0 +1,31 @@
import logging
import subprocess
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID
logger = logging.getLogger(APP_NAME)
def apply_overrides(xdg: dict[str, str]) -> None:
try:
# NOTE: D-Bus registration required for sandbox
args = [
"flatpak",
"--user",
"override",
f"--own-name={GTK_APP_ID}",
FLATPAK_APPID,
]
subprocess.run([*args], capture_output=True)
for path in xdg.values():
args = [
"flatpak",
"--user",
"override",
f"--filesystem={path}",
FLATPAK_APPID,
]
subprocess.run([*args], capture_output=True)
except Exception as e:
logger.critical(e)
+2 -1
View File
@@ -156,6 +156,7 @@ filter_duplicate = "Duplicate"
filter_official = "Official" filter_official = "Official"
filter_unofficial = "Unofficial" filter_unofficial = "Unofficial"
filter_modded = "Modded" filter_modded = "Modded"
filter_unmodded = "Unmodded"
# maps # maps
all_maps = "All maps" all_maps = "All maps"
@@ -296,7 +297,7 @@ class Options:
init = Init( init = Init(
is_steam_running="Is Steam running? DZGUI must be run on top of Steam.", is_steam_running="Is Steam running? DZGUI must be run on top of Steam.",
is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.", is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.",
requires_steam="DZGUI requires that Steam or Flatpak Steam be installed on the system.", requires_steam="Your chosen client (Steam/Flatpak Steam) must be installed on the system.",
) )
mod_panel = ModPanelStrings( mod_panel = ModPanelStrings(
+1 -18
View File
@@ -1,6 +1,5 @@
import logging import logging
import signal import signal
import warnings
from typing import TYPE_CHECKING, Literal, Self from typing import TYPE_CHECKING, Literal, Self
@@ -8,12 +7,11 @@ from dzgui.const.constants import (
APP_NAME, APP_NAME,
APP_NAME_LOWER, APP_NAME_LOWER,
GTK_APP_ID, GTK_APP_ID,
STEAM_ICON,
) )
from dzgui.const.enum import NotebookPage from dzgui.const.enum import NotebookPage
from dzgui.controllers.emitter import Emitter from dzgui.controllers.emitter import Emitter
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.util import css, strings from dzgui.util import css
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.views.components.connect_panel import ConnectPanel from dzgui.views.components.connect_panel import ConnectPanel
from dzgui.views.components.crumbs import Breadcrumbs from dzgui.views.components.crumbs import Breadcrumbs
@@ -47,10 +45,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
# TODO: drop
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
class OuterWindow(Gtk.ApplicationWindow): class OuterWindow(Gtk.ApplicationWindow):
def __init__(self, app: Gtk.Application) -> None: def __init__(self, app: Gtk.Application) -> None:
@@ -337,15 +331,4 @@ class App(ColorAwareApp, Gtk.Application): # type: ignore
) -> None: ) -> None:
self.win.halt_proc_and_quit() self.win.halt_proc_and_quit()
MainController = Controller() MainController = Controller()
try:
theme = Gtk.IconTheme.get_default()
icons = theme.list_icons(None)
if STEAM_ICON not in icons:
logger.warn(strings.steam_icon_missing)
warnings.warn(strings.steam_icon_missing, stacklevel=2)
except Exception as e:
logger.warning(e)
warnings.warn(strings.gtk_theme_missing, stacklevel=2)
+1
View File
@@ -65,6 +65,7 @@ class IconButton(Gtk.Button):
self.set_image(self.icon) self.set_image(self.icon)
self.set_image_position(position) self.set_image_position(position)
self.set_focus_on_click(False) self.set_focus_on_click(False)
self.set_always_show_image(True)
def swap_icon(self, icon: str) -> None: def swap_icon(self, icon: str) -> None:
start = self.icon.get_margin_start() start = self.icon.get_margin_start()
+34 -4
View File
@@ -1,42 +1,72 @@
from typing import Self
from warnings import deprecated
from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
from dzgui.init.prereqs import has_steam_client, has_flatpak_client
from dzgui.strings import options from dzgui.strings import options
from dzgui.views.components.box import HBox from dzgui.views.components.box import HBox
from dzgui.views.components.buttons import Icon from dzgui.views.components.buttons import Icon
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk # noqa
@deprecated("Client choice is determined at setup time")
class ClientCombo(Gtk.ComboBox): class ClientCombo(Gtk.ComboBox):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.client_store = Gtk.ListStore(str, str) has_steam = has_steam_client()
clients = ( has_flatpak = has_flatpak_client()
self.client_store = Gtk.ListStore(str, str, bool)
self.clients = (
( (
options.steam_combo, options.steam_combo,
STEAM_CMD, STEAM_CMD,
has_steam,
), ),
( (
options.flatpak_combo, options.flatpak_combo,
FLATPAK_RUN_CMD, FLATPAK_RUN_CMD,
has_flatpak,
), ),
( (
options.flatpak_container_combo, options.flatpak_container_combo,
FLATPAK_SANDBOX, FLATPAK_SANDBOX,
has_flatpak,
), ),
) )
for client in clients: for client in self.clients:
self.client_store.append(client) self.client_store.append(client)
self.set_model(self.client_store) self.set_model(self.client_store)
renderer_text = Gtk.CellRendererText() renderer_text = Gtk.CellRendererText()
self.pack_start(renderer_text, True) self.pack_start(renderer_text, True)
self.add_attribute(renderer_text, "text", 0) self.add_attribute(renderer_text, "text", 0)
self.set_active(0) self.set_active(0)
self.set_cell_data_func(self.get_cells()[0], self.data_func)
def data_func(
self,
combo: Self,
cell: Gtk.CellRendererText,
model: Gtk.ListStore,
_iter: Gtk.TreeIter,
) -> None:
sens = model[_iter][2]
cell.set_property("sensitive", sens)
def get_client_human_readable(self, cmd: str) -> str:
for client in self.clients:
human, command, sens = client
if command == cmd:
return human
raise TypeError("Not a valid Steam client selection")
class ErrorPopover(Gtk.Popover): class ErrorPopover(Gtk.Popover):
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None: def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
super().__init__(relative_to=relative_to, position=position) super().__init__(relative_to=relative_to, position=position)
+90 -27
View File
@@ -12,21 +12,24 @@ from dzgui.const.constants import (
APP_NAME, APP_NAME,
APP_NAME_LOWER, APP_NAME_LOWER,
HERO_PATH, HERO_PATH,
FLATPAK_ROOT_PATH,
LEGACY_CONFIG_PATH, LEGACY_CONFIG_PATH,
) )
from dzgui.const.boilerplate import config_boilerplate from dzgui.const.boilerplate import config_boilerplate
from dzgui.const.endpoints import STEAM_API_SETUP from dzgui.const.endpoints import STEAM_API_SETUP
from dzgui.config import freedesktop from dzgui.config import freedesktop
from dzgui.init.proc import is_sandboxed, path_to_steam_cmd
from dzgui.init.migrate import migrate_legacy_conf from dzgui.init.migrate import migrate_legacy_conf
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import wizard from dzgui.strings import wizard
from dzgui.util._json import write_json from dzgui.util._json import write_json
from dzgui.util.flatpak import apply_overrides
from dzgui.util.open_links import open_link_by_url from dzgui.util.open_links import open_link_by_url
from dzgui.util.css import add_class, load_css from dzgui.util.css import add_class, load_css
from dzgui.views.components.buttons import WebButton from dzgui.views.components.buttons import WebButton
from dzgui.views.components.entry import APIEntry from dzgui.views.components.entry import APIEntry
from dzgui.views.components.misc import ClientCombo
from dzgui.views.mixins.colorscheme import ColorAwareApp from dzgui.views.mixins.colorscheme import ColorAwareApp
from dzgui.views.trees.tree_simple import SimpleTree
import gi import gi
@@ -44,7 +47,8 @@ class PageNum(Enum):
STEAM_API = 4 STEAM_API = 4
USER_PREFS = 5 USER_PREFS = 5
SHORTCUTS = 6 SHORTCUTS = 6
FINAL = 7 FLATPAK = 7
FINAL = 8
class OptionalPageMixin: class OptionalPageMixin:
@@ -143,7 +147,7 @@ class NotificationFrame(Gtk.Frame):
margin_start=50, margin_start=50,
margin_end=50, margin_end=50,
) )
wrapped = textwrap.fill(label, width=80) wrapped = textwrap.fill(label, width=60)
self.label = Gtk.Label( self.label = Gtk.Label(
label=wrapped, label=wrapped,
justify=Gtk.Justification.CENTER, justify=Gtk.Justification.CENTER,
@@ -157,7 +161,7 @@ class NotificationFrame(Gtk.Frame):
add_class(self, "error-frame") add_class(self, "error-frame")
def set_text(self, text: str) -> None: def set_text(self, text: str) -> None:
wrapped = textwrap.fill(text, width=80) wrapped = textwrap.fill(text, width=60)
self.label.set_markup(wrapped) self.label.set_markup(wrapped)
@@ -242,12 +246,23 @@ class IntroductionPage(EnumeratedWizardPage):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
enum=PageNum.INTRO, enum=PageNum.INTRO,
heading=f"Welcome to {APP_NAME}!", heading=wizard.welcome.format(APP_NAME),
description=wizard.blurb_intro, description=wizard.blurb_intro,
) )
self.page_type = Gtk.AssistantPageType.INTRO self.page_type = Gtk.AssistantPageType.INTRO
self.set_title(wizard.title_intro) self.set_title(wizard.title_intro)
self.error_box = NotificationFrame(wizard.error_sandbox, error=True)
self.add_start(self.error_box)
self.connect("map", self._on_map)
def _on_map(self, page: "EnumeratedWizardPage") -> None:
self.error_box.hide()
if is_sandboxed():
self.error_box.show()
EMITTER.emit("step_pending")
class Heading(Gtk.Label): class Heading(Gtk.Label):
def __init__(self, label: str): def __init__(self, label: str):
@@ -384,26 +399,17 @@ class PreferencesPage(EnumeratedWizardPage):
self.dist_box.add(self.radio_km) self.dist_box.add(self.radio_km)
self.dist_box.add(self.radio_miles) self.dist_box.add(self.radio_miles)
client_label = Gtk.Label(label=wizard.label_client, halign=Gtk.Align.START)
self.client_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
self.client_combo = ClientCombo()
self.client_box.add(client_label)
self.client_box.add(self.client_combo)
outer_box = Gtk.Box( outer_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER, spacing=10 orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER, spacing=10
) )
for el in self.name_box, self.client_box, self.dist_box: for el in self.name_box, self.dist_box:
outer_box.add(el) outer_box.add(el)
self.add_start(outer_box) self.add_start(outer_box)
def get_prefs(self) -> tuple[str, bool, str]: def get_prefs(self) -> tuple[str, bool]:
name = self.name_entry.get_text().strip() name = self.name_entry.get_text().strip()
use_miles = self.radio_miles.get_active() use_miles = self.radio_miles.get_active()
model = self.client_combo.get_model() return name, use_miles
ind = self.client_combo.get_active()
client = model[ind][1]
return name, use_miles, client
def _on_entry_changed(self, entry: Gtk.Entry) -> None: def _on_entry_changed(self, entry: Gtk.Entry) -> None:
text = entry.get_text() text = entry.get_text()
@@ -413,6 +419,42 @@ class PreferencesPage(EnumeratedWizardPage):
EMITTER.emit("step_complete") EMITTER.emit("step_complete")
class FlatpakOverridePage(EnumeratedWizardPage):
def __init__(self, xdg_paths: dict[str, str]) -> None:
super().__init__(
enum=PageNum.FLATPAK,
heading=wizard.heading_flatpak,
description=wizard.blurb_flatpak,
)
self.xdg_paths = xdg_paths
self.page_type = Gtk.AssistantPageType.INTRO
store = Gtk.ListStore(str)
for path in xdg_paths.values():
store.append((str(path),))
tree = SimpleTree(store, wizard.tree_column)
tree.set_halign(Gtk.Align.CENTER)
self.authorize_button = Gtk.Button(
label=wizard.button_flatpak, halign=Gtk.Align.CENTER
)
self.authorize_button.connect("clicked", self._on_authorize)
self.notice = NotificationFrame(wizard.overrides_applied)
self.connect("map", self._on_map)
for el in tree, self.authorize_button, self.notice:
self.add_start(el)
def _on_map(self, page: "EnumeratedWizardPage") -> None:
self.notice.hide()
def _on_authorize(self, button: Gtk.Button) -> None:
apply_overrides(self.xdg_paths)
self.notice.show()
EMITTER.emit("step_complete")
class CompletionPage(EnumeratedWizardPage): class CompletionPage(EnumeratedWizardPage):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
@@ -426,7 +468,7 @@ class CompletionPage(EnumeratedWizardPage):
class Assistant(Gtk.Assistant): class Assistant(Gtk.Assistant):
def __init__(self, is_deck: bool, XDG: "Xdg"): def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]):
super().__init__() super().__init__()
if is_deck: if is_deck:
# NOTE: deemed to be "safe" dimensions that exclude taskbar size # NOTE: deemed to be "safe" dimensions that exclude taskbar size
@@ -445,6 +487,7 @@ class Assistant(Gtk.Assistant):
self.page_api = SteamValidationPage() self.page_api = SteamValidationPage()
self.page_prefs = PreferencesPage() self.page_prefs = PreferencesPage()
self.page_shortcuts = ShortcutCreationPage(XDG.shortcut) self.page_shortcuts = ShortcutCreationPage(XDG.shortcut)
self.page_flatpak = FlatpakOverridePage(xdg_paths)
self.page_completion = CompletionPage() self.page_completion = CompletionPage()
self.set_forward_page_func(self._advance_page) self.set_forward_page_func(self._advance_page)
@@ -464,6 +507,7 @@ class Assistant(Gtk.Assistant):
self.page_api, self.page_api,
self.page_prefs, self.page_prefs,
self.page_shortcuts, self.page_shortcuts,
self.page_flatpak,
self.page_completion, self.page_completion,
): ):
# NOTE: skip config migration page if no legacy config file # NOTE: skip config migration page if no legacy config file
@@ -484,7 +528,10 @@ class Assistant(Gtk.Assistant):
def write_config(self, config: dict[str, Any]) -> None: def write_config(self, config: dict[str, Any]) -> None:
write_json(config, self.config_path) write_json(config, self.config_path)
def get_final_page(self) -> int: def get_completion_page(self) -> int:
return self.pages[self.page_completion]
def get_shortcut_page(self) -> int:
return self.pages[self.page_shortcuts] return self.pages[self.page_shortcuts]
def _advance_page(self, index: int) -> int: def _advance_page(self, index: int) -> int:
@@ -497,26 +544,39 @@ class Assistant(Gtk.Assistant):
sp = page.migrated_conf["default_steam_path"] sp = page.migrated_conf["default_steam_path"]
self.page_shortcuts.set_steam_path(sp) self.page_shortcuts.set_steam_path(sp)
self.config_values = self.page_migration.migrated_conf self.config_values = self.page_migration.migrated_conf
return self.get_final_page() return self.get_shortcut_page()
case SteamPathPage(): case SteamPathPage():
self.config_values["default_steam_path"] = page.get_path_from_radio() path = page.get_path_from_radio()
self.config_values["default_steam_path"] = path
self.config_values["client"] = path_to_steam_cmd(path)
case SteamValidationPage(): case SteamValidationPage():
self.config_values["steam_api"] = page.get_api_key() self.config_values["steam_api"] = page.get_api_key()
case PreferencesPage(): case PreferencesPage():
# NOTE: collects config values before advancing to last page # NOTE: collects config values before advancing to last page
name, use_miles, client = self.page_prefs.get_prefs() name, use_miles = self.page_prefs.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.page_shortcuts.set_steam_path( self.page_shortcuts.set_steam_path(
self.config_values["default_steam_path"] self.config_values["default_steam_path"]
) )
case ShortcutCreationPage(): case ShortcutCreationPage():
page.create_shortcuts() page.create_shortcuts()
if not self.needs_flatpak_override():
return self.get_completion_page()
case FlatpakOverridePage():
pass
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
def needs_flatpak_override(self) -> bool:
if (
self.page_shortcuts.is_steam_checkbox_active()
and FLATPAK_ROOT_PATH in self.config_values["default_steam_path"]
):
return True
return False
def is_setup_complete(self) -> bool: def is_setup_complete(self) -> bool:
return self.setup_complete return self.setup_complete
@@ -642,6 +702,9 @@ class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ig
def set_steam_path(self, path: Path) -> None: def set_steam_path(self, path: Path) -> None:
self.steam_path = path self.steam_path = path
def is_steam_checkbox_active(self) -> bool:
return self.steam_checkbox.get_active()
def create_shortcuts(self) -> None: def create_shortcuts(self) -> None:
# NOTE: best-effort, permissive even on failure (page is already marked as complete) # NOTE: best-effort, permissive even on failure (page is already marked as complete)
if self.steam_checkbox.get_active(): if self.steam_checkbox.get_active():
@@ -710,10 +773,10 @@ class SteamPathPage(EnumeratedWizardPage):
class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
def __init__(self, is_deck: bool, XDG: "Xdg") -> None: def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
super().__init__() super().__init__()
GLib.set_prgname(APP_NAME) GLib.set_prgname(APP_NAME)
self.win = Window(is_deck, XDG) self.win = Window(is_deck, XDG, xdg_paths)
Gtk.main() Gtk.main()
def is_setup_complete(self) -> int: def is_setup_complete(self) -> int:
@@ -721,9 +784,9 @@ class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
class Window(Gtk.Window): class Window(Gtk.Window):
def __init__(self, is_deck: bool, XDG: "Xdg") -> None: def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
super().__init__(title=APP_NAME, icon_name=APP_NAME) super().__init__(title=APP_NAME, icon_name=APP_NAME)
self.assistant = Assistant(is_deck, XDG) self.assistant = Assistant(is_deck, XDG, xdg_paths)
class Emitter(GObject.GObject): class Emitter(GObject.GObject):
+33 -32
View File
@@ -9,6 +9,9 @@ from dzgui.const.constants import (
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
APPNAME_DAYZ, APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN, APPNAME_DAYZ_EXP_HUMAN,
FLATPAK_RUN_CMD,
FLATPAK_SANDBOX,
FLATPAK_STEAM_PATH,
NO_EXPAND, NO_EXPAND,
NO_FILL, NO_FILL,
NO_PADDING, NO_PADDING,
@@ -25,7 +28,7 @@ from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.labels import LeftLabel from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.buttons import SpinnerButton, WebButton from dzgui.views.components.buttons import SpinnerButton, WebButton
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo, ErrorPopover from dzgui.views.components.misc import ErrorPopover
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
@@ -76,7 +79,7 @@ class SubmitField(Gtk.Box):
self.button = Gtk.Button(label=options.save_button) self.button = Gtk.Button(label=options.save_button)
self.button.connect("clicked", self._on_save_clicked) self.button.connect("clicked", self._on_save_clicked)
self.entry.connect("insert-text", self._on_text_typed) self.entry.connect("changed", self._on_text_typed)
self.entry.connect("activate", self._on_field_activated) self.entry.connect("activate", self._on_field_activated)
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted) self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
@@ -113,13 +116,9 @@ class SubmitField(Gtk.Box):
def _on_text_typed( def _on_text_typed(
self, self,
entry: Gtk.Entry, entry: Gtk.Entry,
text: str,
length: int,
pos: int,
) -> None: ) -> None:
buffer = entry.get_property("buffer") text = entry.get_text()
text = buffer.get_text() + text
state = self._is_valid_text(text) state = self._is_valid_text(text)
self.button.set_sensitive(state) self.button.set_sensitive(state)
@@ -250,6 +249,26 @@ class ToggleField(Gtk.Box):
el.set_sensitive(state) el.set_sensitive(state)
class FlatpakToggleField(ToggleField):
def __init__(
self,
controller: "Controller",
) -> None:
super().__init__(
controller,
first_option=options.flatpak_standard,
second_option=options.flatpak_container,
context=Preferences.CLIENT,
)
self.add(InfoEventBox(options.flatpak_info, self.controller))
self.controller = controller
self.radio1.connect("toggled", self._on_radio_toggled, Preferences.CLIENT)
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
cmd = FLATPAK_RUN_CMD if self.radio1.get_active() else FLATPAK_SANDBOX
self.controller.update_config(context, cmd)
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
def __init__(self, controller: "Controller"): def __init__(self, controller: "Controller"):
super().__init__( super().__init__(
@@ -278,6 +297,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
self.player_box.set_halign(Gtk.Align.START) self.player_box.set_halign(Gtk.Align.START)
self.player_box.get_children()[0].set_width_chars(30) # type: ignore self.player_box.get_children()[0].set_width_chars(30) # type: ignore
self.flatpak_toggle = FlatpakToggleField(self.controller)
self.fullscreen_toggle = ToggleField( self.fullscreen_toggle = ToggleField(
self.controller, self.controller,
strings.options.last_used, strings.options.last_used,
@@ -285,11 +305,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
Preferences.WINDOW, Preferences.WINDOW,
) )
self.client_combo = ClientCombo()
self.client_combo.connect("changed", self._on_client_changed)
client_hbox = ShortHBox(self.client_combo)
self.distance_toggle = ToggleField( self.distance_toggle = ToggleField(
self.controller, strings.options.km, strings.options.mi, Preferences.DIST self.controller, strings.options.km, strings.options.mi, Preferences.DIST
) )
@@ -313,12 +328,17 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
self.start_tab_combo.connect("changed", self._on_start_tab_changed) self.start_tab_combo.connect("changed", self._on_start_tab_changed)
pref_rows = [ pref_rows = [
[LeftLabel(strings.options.client), client_hbox],
[LeftLabel(strings.options.window_size), self.fullscreen_toggle], [LeftLabel(strings.options.window_size), self.fullscreen_toggle],
[LeftLabel(strings.options.distance), self.distance_toggle], [LeftLabel(strings.options.distance), self.distance_toggle],
[LeftLabel(options.start_tab), start_tab_hbox], [LeftLabel(options.start_tab), start_tab_hbox],
[LeftLabel(strings.options.name), self.player_box], [LeftLabel(strings.options.name), self.player_box],
] ]
default = self.controller.query_config(Preferences.DEFAULT)
if FLATPAK_STEAM_PATH in default:
pref_rows.insert(
0,
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
)
self.dayz_version_label = Gtk.Label(label=strings.null) self.dayz_version_label = Gtk.Label(label=strings.null)
self.dayz_exp_version_label = Gtk.Label(label=strings.null) self.dayz_exp_version_label = Gtk.Label(label=strings.null)
@@ -371,19 +391,11 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
self.connect("key-press-event", self._on_keypress) self.connect("key-press-event", self._on_keypress)
def get_client_name(self) -> str:
model = self.client_combo.get_model()
ind = self.client_combo.get_active()
return str(model[ind][0])
def block_text_entry(self) -> None: def block_text_entry(self) -> None:
self.steam_box.block_text_entry() self.steam_box.block_text_entry()
# self.steam_entry.set_position(-1)
# self.steam_entry.set_can_focus(False)
def unblock_text_entry(self) -> None: def unblock_text_entry(self) -> None:
self.steam_box.unblock_text_entry() self.steam_box.unblock_text_entry()
# self.steam_entry.set_can_focus(True)
def _on_developers_clicked(self, button: Gtk.Button) -> None: def _on_developers_clicked(self, button: Gtk.Button) -> None:
self.controller.show_developers_page() self.controller.show_developers_page()
@@ -418,13 +430,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
index = enum.value index = enum.value
self.controller.update_config(Preferences.START_TAB, index) self.controller.update_config(Preferences.START_TAB, index)
def _on_client_changed(self, combo: Gtk.ComboBoxText) -> None:
_iter = combo.get_active_iter()
if _iter is None:
raise ValueError(f"No active iterator set on {combo}")
real_cmd = combo.get_model()[_iter][1]
self.controller.update_config(Preferences.CLIENT, real_cmd)
def populate_settings(self) -> None: def populate_settings(self) -> None:
prefs = self.controller.get_prefs() prefs = self.controller.get_prefs()
# NOTE: re-check in case file was removed by user between runs # NOTE: re-check in case file was removed by user between runs
@@ -470,10 +475,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
self.dayz_version_label.set_text(dayz_version) self.dayz_version_label.set_text(dayz_version)
self.dayz_exp_version_label.set_text(dayz_exp_version) self.dayz_exp_version_label.set_text(dayz_exp_version)
# TODO: bicolumn list store with no cell renderer on index 1, use raw command names
active_combo = query.get_client_index(config["client"])
self.client_combo.set_active(active_combo)
start_tab = self.controller.query_config(Preferences.START_TAB) start_tab = self.controller.query_config(Preferences.START_TAB)
self.start_tab_combo.set_active(start_tab) self.start_tab_combo.set_active(start_tab)
+2
View File
@@ -279,6 +279,8 @@ class PreConnectionAssistant(Gtk.Box):
warnings.append(preconnect.protected_server) warnings.append(preconnect.protected_server)
if prereqs.dayz_running is True: if prereqs.dayz_running is True:
warnings.append(preconnect.dayz_running) warnings.append(preconnect.dayz_running)
if len(prereqs.mods) > 0 and prereqs.download_paused:
warnings.append(preconnect.downloads_paused)
allows_dl, running_app = prereqs.allows_downloads allows_dl, running_app = prereqs.allows_downloads
if len(prereqs.mods) > 0 and allows_dl is False: if len(prereqs.mods) > 0 and allows_dl is False:
+35
View File
@@ -0,0 +1,35 @@
from typing import TYPE_CHECKING, Union
if TYPE_CHECKING:
from dzgui.model.model_factory import FastInsertListStore
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class SimpleTree(Gtk.ScrolledWindow):
"""Simple scrollable 1-column tree"""
def __init__(
self, model: Union[Gtk.ListStore, "FastInsertListStore"], title: str
) -> None:
super().__init__(
overlay_scrolling=False,
propagate_natural_width=True,
propagate_natural_height=True,
)
self.tree = Gtk.TreeView(model=model, enable_search=False)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(title, renderer, text=0)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
column.set_resizable(False)
column.set_sort_column_id(0)
self.tree.append_column(column)
self.add(self.tree)
def get_tree(self) -> Gtk.TreeView:
return self.tree
+3 -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.0b25" version = "7.0.0b28"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
@@ -110,10 +110,11 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
markers = [ markers = [
"apitest: checks remote endpoints", "apitest: checks remote endpoints",
"config: config file keys/values", "config: config file keys/values",
"realconfig: depends on actual generated local config file", "flatpak: depends on local flatpak installation",
"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",
"realconfig: depends on actual generated local config file",
"redact: log redaction mechanisms", "redact: log redaction mechanisms",
"slow: long-running tests", "slow: long-running tests",
"webtest: depends on remote endpoint", "webtest: depends on remote endpoint",
+1 -1
View File
@@ -8,6 +8,6 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "0" "timestamp" "0"
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1787904411" "timestamp" "1787904411"
} }
"1" "1"
{ {
@@ -18,6 +18,6 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1787904647" "timestamp" "1787904647"
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1797904411" "timestamp" "1797904411"
} }
"1" "1"
{ {
@@ -18,6 +18,6 @@
"WantsOfflineMode" "0" "WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AutoLogin" "1" "AutoLogin" "1"
"Timestamp" "1787904647" "timestamp" "1787904647"
} }
} }
+1 -1
View File
@@ -9,6 +9,6 @@
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1" "AllowAutoLogin" "1"
"MostRecent" "1" "MostRecent" "1"
"Timestamp" "0" "timestamp" "0"
} }
} }
+2 -2
View File
@@ -9,7 +9,7 @@
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1" "AllowAutoLogin" "1"
"MostRecent" "0" "MostRecent" "0"
"Timestamp" "0" "timestamp" "0"
} }
"1" "1"
{ {
@@ -20,6 +20,6 @@
"SkipOfflineModeWarning" "0" "SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1" "AllowAutoLogin" "1"
"MostRecent" "1" "MostRecent" "1"
"Timestamp" "10" "timestamp" "10"
} }
} }
+19
View File
@@ -0,0 +1,19 @@
import pytest
from dzgui.const.constants import FLATPAK_RUN_CMD, STEAM_CMD
from dzgui.controllers.mc import Controller
@pytest.mark.parametrize(
"value, expected", [(FLATPAK_RUN_CMD, "Flatpak"), (STEAM_CMD, "Steam")]
)
def test_human_name(monkeypatch, value, expected) -> None:
def mock_query_config(*args) -> str:
return value
monkeypatch.setattr(
"dzgui.controllers.mc.Controller.query_config", mock_query_config
)
controller = Controller()
client = controller.get_client_name()
assert client == expected
+3 -4
View File
@@ -10,7 +10,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib # noqa from gi.repository import Gtk, Gio, GLib # noqa
class TestWidget(ColorAwareApp, Gtk.Button): class DummyWidget(ColorAwareApp, Gtk.Button):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -22,11 +22,10 @@ class TestWidget(ColorAwareApp, Gtk.Button):
@pytest.fixture @pytest.fixture
def widget() -> TestWidget: def widget() -> DummyWidget:
return TestWidget() return DummyWidget()
@pytest.mark.integration
@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)]) @pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)])
def test_light_mode(widget, key: str, expect: bool) -> None: def test_light_mode(widget, key: str, expect: bool) -> None:
local_pref = subprocess.run( local_pref = subprocess.run(
+43
View File
@@ -0,0 +1,43 @@
import configparser
import pytest
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID
from dzgui.config.xdg import get_xdg_paths
from dzgui.util.flatpak import apply_overrides
PATHS_KEY = "Context"
PATHS_SUBKEY = "filesystems"
BUS_KEY = "Session Bus Policy"
pytestmark = pytest.mark.flatpak
@pytest.fixture
def xdg_paths() -> dict[str, str]:
return get_xdg_paths()
@pytest.fixture
def override(xdg_paths) -> None:
apply_overrides(xdg_paths)
@pytest.fixture
def config(xdg_paths) -> configparser.ConfigParser:
conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath(
f"flatpak/overrides/{FLATPAK_APPID}"
)
config = configparser.ConfigParser()
config.read(conf_path)
return config
def test_session_bus(override, config) -> None:
bus = config[BUS_KEY][GTK_APP_ID]
assert bus == "own"
def test_paths(override, xdg_paths, config) -> None:
expect = xdg_paths
paths = config[PATHS_KEY][PATHS_SUBKEY].split(";")
assert all(str(path) in paths for path in expect.values())
+30
View File
@@ -0,0 +1,30 @@
import pytest
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX
from dzgui.const.enum import Preferences
from dzgui.views.pages.options import FlatpakToggleField
class MockController:
def __init__(self) -> None:
self.command: str
def update_config(self, context: Preferences, cmd: str) -> None:
self.command = cmd
def get_command(self) -> str:
return self.command
@pytest.mark.parametrize("index, expect", [(0, FLATPAK_RUN_CMD), (1, FLATPAK_SANDBOX)])
def test_toggle(index: int, expect: str) -> None:
controller = MockController()
toggle = FlatpakToggleField(controller)
match index:
case 0:
toggle.radio2.set_active(True)
toggle.radio1.set_active(True)
case 1:
toggle.radio1.set_active(True)
toggle.radio2.set_active(True)
assert controller.get_command() == expect
+5
View File
@@ -1,3 +1,5 @@
import pytest
from dzgui.api.servers import Details from dzgui.api.servers import Details
from dzgui.views.dialogs.servers import ServerDetailsDialog from dzgui.views.dialogs.servers import ServerDetailsDialog
@@ -21,6 +23,7 @@ class MockController:
return None return None
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_html_escaping() -> None: def test_html_escaping() -> None:
name = "Test server" name = "Test server"
desc = "<Server description> Game & mods" desc = "<Server description> Game & mods"
@@ -31,5 +34,7 @@ def test_html_escaping() -> None:
controller = MockController() controller = MockController()
dialog = ServerDetailsDialog(controller, details) dialog = ServerDetailsDialog(controller, details)
dialog.destroy() dialog.destroy()
while Gtk.events_pending():
Gtk.main_iteration()
assert dialog.description.get_text() == desc assert dialog.description.get_text() == desc
+9
View File
@@ -3,6 +3,7 @@ import pytest
from pathlib import Path from pathlib import Path
from dzgui.api.steam import find_user_id from dzgui.api.steam import find_user_id
from dzgui.const.constants import DEFAULT_STEAM_PATH
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
pytestmark = pytest.mark.apitest pytestmark = pytest.mark.apitest
@@ -25,3 +26,11 @@ def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers) monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers)
uid = find_user_id(Path("")) uid = find_user_id(Path(""))
assert int(uid) == expect assert int(uid) == expect
@pytest.mark.realconfig
def test_live_loginusers() -> None:
"""Added on 2026-10-17 following changes to VDF keys"""
steam = Path.home().joinpath(DEFAULT_STEAM_PATH)
uid = find_user_id(steam)
assert uid is not None
+27
View File
@@ -0,0 +1,27 @@
import pytest
from pathlib import Path
from dzgui.api.steam import is_dayz_downloading
@pytest.mark.parametrize(
"file_list, expect",
[
(["state_221100_1111.patch"], True),
(["state_730_1111.patch"], False),
(["state_1024020_1111.patch"], True),
(["221100"], False),
(["221100", "state_221100_1111.patch"], True),
(["state_221100_1111.patch", "state_1024020_1111.patch"], True),
(["state_221100_1111"], False),
],
)
def test_dayz_patch_pending(monkeypatch, file_list, expect) -> None:
def mock_get_pending_downloads(steam_path: Path, appid: int) -> None:
files = [Path(file) for file in file_list]
return files
monkeypatch.setattr(
"dzgui.api.steam.get_pending_downloads", mock_get_pending_downloads
)
assert is_dayz_downloading(Path("/tmp"), Path("/tmp")) == expect
+78
View File
@@ -0,0 +1,78 @@
import pytest
import unittest
from unittest.mock import patch
from dzgui.const.constants import (
FLATPAK_APPID,
FLATPAK_ROOT_PATH,
FLATPAK_RUN_CMD,
FLATPAK_SANDBOX,
)
from dzgui.init import proc
pytestmark = pytest.mark.flatpak
class TestSteamDetection(unittest.TestCase):
def test_flatpak_detection_with_multiple_running_apps(self):
def flatpak_output(args, *, text):
# Default `flatpak ps` includes instance, PID, app and runtime columns.
if args == ["flatpak", "ps", "--columns=application"]:
return f"com.spotify.Client\n{FLATPAK_APPID}\n{FLATPAK_APPID}\n"
return f"3904921611\t1257689\t{FLATPAK_APPID}\torg.freedesktop.Platform\n"
with (
patch.object(proc, "has_cmd", return_value=True),
patch.object(proc.subprocess, "check_output", side_effect=flatpak_output),
):
for client in (FLATPAK_RUN_CMD, FLATPAK_SANDBOX):
with self.subTest(client=client):
self.assertTrue(proc.is_steam_running(client))
def test_flatpak_not_running(self):
with patch.object(proc, "has_cmd", return_value=True):
for output in ("", "com.spotify.Client\n", f"{FLATPAK_APPID}Helper\n"):
with (
self.subTest(output=output),
patch.object(proc.subprocess, "check_output", return_value=output),
):
self.assertFalse(proc.is_flatpak_steam_running())
def test_flatpak_not_installed(self):
with (
patch.object(proc, "has_cmd", return_value=False),
patch.object(proc.subprocess, "check_output") as check_output,
):
self.assertFalse(proc.is_flatpak_steam_running())
check_output.assert_not_called()
def test_native_steam(self):
with patch.object(proc, "has_cmd", return_value=True):
for output, expected in (
(
[
[FLATPAK_ROOT_PATH],
"/home/user/.local/share/Steam/ubuntu12_64/steamwebhelper",
],
True,
),
([[FLATPAK_ROOT_PATH]], False),
([[FLATPAK_ROOT_PATH], ["steam-runtime-launcher-service"]], False),
([["/home/user/.local/share/Steam/ubuntu12_64/steamwebhelper"]], True),
(
[
["/home/user/.local/share/Steam/ubuntu12_64/steamwebhelper"],
"steam-runtime-launcher-service",
],
True,
),
):
with (
self.subTest(running=output),
patch.object(proc, "get_steam_procs", return_value=output),
):
self.assertEqual(proc.is_native_steam_running(), expected)
def test_invalid_client(self):
with self.assertRaises(TypeError):
proc.is_steam_running("invalid-client")
+5 -1
View File
@@ -14,7 +14,11 @@ from dzgui.views.dialogs.calc import (
class DummyEmitter: class DummyEmitter:
pass def __init__(self) -> None:
pass
def connect(self, _, __) -> None:
pass
@pytest.fixture(params=[("05:00", 10.0), ("15:30", 5.0)]) @pytest.fixture(params=[("05:00", 10.0), ("15:30", 5.0)])