feat: dzgui 7 alpha

This commit is contained in:
aclist committed 2025-12-08 17:32:25 +09:00
1 parent 1acdbd58c5
commit 2e5b771137
150 files changed
+10359

No files matched your search

View File
Whitespace-only changes.
+27
View File
@@ -0,0 +1,27 @@
import json
from pathlib import Path
def read_json(path: Path) -> dict:
try:
with open(path, "r") as infile:
try:
data = json.load(infile)
except json.decoder.JSONDecodeError as e:
raise e
except OSError as e:
raise e
return data
def write_json(data: dict, path: Path) -> None:
try:
j = json.dumps(data, indent=2)
except Exception as e:
raise e
# TODO: Path.write_text()
try:
with open(path, "w") as outfile:
outfile.write(j)
except OSError as e:
raise e
+4
View File
@@ -0,0 +1,4 @@
import shlex
def concat_bash_args(command: str) -> list[str]:
return shlex.split(command)
+17
View File
@@ -0,0 +1,17 @@
from datetime import datetime
from pathlib import Path
def get_time() -> int:
return int(datetime.now().timestamp())
# TODO: deprecated
#def write_time(path: Path) -> None:
# time = get_time()
# Path.write_text(str(time))
def is_elapsed(time_before: int) -> bool:
time_now = get_time()
if time_now - time_before <= 30:
return False
else:
return True
+50
View File
@@ -0,0 +1,50 @@
import gi # noqa E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
def add_class(widget: Gtk.Widget, label: str) -> None:
"""
Sets the classname of a widget, used
to apply CSS styling later
"""
context = widget.get_style_context()
context.add_class(label)
def load_css() -> None:
# TODO: consider storing this in a data file
css = """
.frame {
border: 0px;
}
.toast-label {
background-color: black;
border: 1px solid white;
}
.frame > border {
border-radius: 5px;
padding: 5px;
}
.page-heading {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.5rem;
}
.settings-subheading {
font-size: 1.2rem;
font-weight: 700;
}
.left-label {
font-size: 1.3rem;
}
.details-heading {
font-size: 1.2rem
}
"""
prov = Gtk.CssProvider()
prov.load_from_data(css.encode("ascii"))
screen = Gdk.Screen.get_default()
if screen:
Gtk.StyleContext.add_provider_for_screen(
screen, prov, Gtk.STYLE_PROVIDER_PRIORITY_USER
)
+30
View File
@@ -0,0 +1,30 @@
import re
import subprocess
from pathlib import Path
CPU_FILE = "/proc/cpuinfo"
def is_steam_deck() -> bool:
# TODO: tests
r = r"AMD Custom APU [0-9]{4}"
with open(CPU_FILE, "r") as f:
s = f.read()
match = re.search(r, s)
if match:
if "0932" in match.group() or "0405" in match.group():
return True
return False
def is_game_mode() -> bool:
proc = subprocess.run(
["/usr/bin/pgrep", "-a", "gamescope"],
capture_output=True,
text=True
)
if proc.returncode != 0:
return False
if "generate-drm-mode" in proc.stdout:
return True
else:
return False
+88
View File
@@ -0,0 +1,88 @@
import os
import platform
import re
import textwrap
from datetime import datetime
from pathlib import Path
from dzgui.api.mods import get_local_mod_ids, get_local_mod_path
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences
from dzgui.config.query import lookup
from dzgui.init.prefix import get_version
from dzgui.util.redact import redact
def get_cpu_model() -> str:
cpuinfo = Path("/proc/cpuinfo")
text = cpuinfo.read_text()
lines = text.split("\n")
for i, line in enumerate(lines):
if line.startswith("model name"):
model = line.split(": ")[1]
return model
return ""
def print_servers(servers: list[str]) -> str:
s = ""
for server in servers:
s+= server + "\n"
return s
def print_mods(mods: list[int]) -> str:
s = ""
for mod in mods:
s+= str(mod) + "\n"
return s
def write_diagnostic(config: Path, outfile: Path) -> None:
# TODO: test availability on other distros
date = datetime.now().isoformat()
distro = platform.freedesktop_os_release()["ID_LIKE"]
kernel = os.uname().release
cpu = get_cpu_model()
version = get_version()
branch = lookup(config, Preferences.BRANCH)
debug = lookup(config, Preferences.DEBUG)
install = lookup(config, Preferences.INSTALL)
default = lookup(config, Preferences.DEFAULT)
steam_path = Path(default)
workshop_path = get_local_mod_path(steam_path)
steam_redacted = redact(default)
workshop_redacted = redact(str(workshop_path))
mods = get_local_mod_ids(steam_path)
mods_pretty = print_mods(mods)
servers = lookup(config, Preferences.IP_LIST)
servers_pretty = print_servers(servers)
# FIXME: extraneous newlines in lists
template = f"""\
{APP_NAME} version {version} ({branch})
Date: {date}
===============================
Distribution: {distro}
Kernel: {kernel}
CPU: {cpu}
Debug: {debug}
Auto-install: {install}
Steam path: {steam_redacted}
Workshop path: {workshop_redacted}
Mods:
{mods_pretty}
Servers:
{servers_pretty}
"""
output = "\n".join([line.lstrip() for line in template.split('\n')])
outfile.write_text(output)
+86
View File
@@ -0,0 +1,86 @@
import multiprocessing
from math import radians, cos, sin, asin, sqrt
from typing import Self, TYPE_CHECKING
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
import dzgui.util.ip as ip
from dzgui.util.ip import GeolocationError
from dzgui.util.localize import number
if TYPE_CHECKING:
from dzgui.util.ip import Coords
class Haversine():
def __init__(self, lat1: float, lon1: float, lat2: float, lon2: float) -> None:
R = 6371 * 1000
dLat = radians(lat2 - lat1)
dLon = radians(lon2 - lon1)
lat1 = radians(lat1)
lat2 = radians(lat2)
a = sin(dLat/2) ** 2 + cos(lat1) * cos(lat2) * sin(dLon/2) ** 2
c = 2 * asin(sqrt(a))
self.dist = R * c
def as_kilometers(self) -> float:
return self.dist / 1000
def as_miles(self) -> float:
return self.dist / 1609.344
def compare(local: "Coords", remote_c: str, fmt: str) -> int | None:
if local is None:
return None
try:
# FIXME: expects XDG.ips
remote = ip.get_coords(remote_c)
except GeolocationError:
return None
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
if fmt == "km":
dist = haversine.as_kilometers()
else:
dist = haversine.as_miles()
return round(dist)
class CalcDist(multiprocessing.Process):
def __init__(
self,
widget: Gtk.Widget,
addr: str,
result_queue: multiprocessing.Queue,
cache: dict,
):
super().__init__()
self.result_queue = result_queue
self.addr = addr
self.ip = addr.split(":")[0]
# TODO: pass controller correctly
def run(self) -> None:
use_miles = MainController.query_config("use_miles")
fmt = "mi" if use_miles else "km"
# TODO: get cache accordingly
if self.addr in cache:
if fmt in cache[self.addr]:
logger.info(f"Address '{self.addr}' already in cache")
self.result_queue.put([self.addr, cache[self.addr]])
return
prefs = MainController.get_prefs()
dist = compare(prefs.coords, prefs.paths.ips, self.ip, fmt)
if dist is None:
dist_pretty = "Unknown"
else:
d = number(dist)
dist_pretty = f"{d} {fmt}"
self.result_queue.put([self.addr, dist_pretty])
+44
View File
@@ -0,0 +1,44 @@
import re
from dzgui.util.localize import number
from dzgui.util.strings import no_mods
def format_pango(text: str) -> str:
medium = '<span size="medium"><b>'
large = '<span size="large"><b>'
xlarge = '<span size="x-large"><b>'
text = re.sub("^# ", xlarge, text, flags=re.M)
text = re.sub("^## ", large, text, flags=re.M)
text = re.sub("^### ", medium, text, flags=re.M)
text = re.sub(r"(<span size=.*)", r"\1</b></span>", text)
return text
def pluralize(plural: str, count: int) -> str:
suffix = plural[-2:]
if suffix == "es":
base = plural[:-2]
return f"{base}{'es'[:2*count ^ 2]}"
else:
base = plural[:-1]
return f"{base}{'s'[:count ^ 1]}"
def format_hyperlinks(text: str) -> str:
def href(string: str) -> str:
return f'<a href="{string}">{string}</a>'
reg = r"\s(www\.*?)"
text = re.sub(reg, " http://" + r"\1", text)
reg2 = r"(http.*?)([ ,\r\n]|$)"
text = re.sub(reg2, href(r"\1") + r"\2", text)
return text
def format_mods(size: int, mods: int) -> str:
if mods == 0:
return no_mods
l_size = number(size)
plural = pluralize("mods", mods)
return f"Found {mods:n} {plural} taking up {l_size} MiB"
+137
View File
@@ -0,0 +1,137 @@
import a2s
import gzip
import requests
import shutil
import subprocess
from dataclasses import dataclass
from typing import TYPE_CHECKING
from dzgui.api.servers import Record
from dzgui.const.constants import REQUEST_TIMEOUT
from dzgui.const.endpoints import COORDS_API, IP_ECHO
if TYPE_CHECKING:
from pathlib import Path
@dataclass
class Coords:
lat: float
lon: float
class GeolocationError(Exception):
pass
class ReservedIPError(Exception):
pass
def ip2list(ip: str) -> list:
return [int(digit) for digit in ip.split(".")]
# TODO: tests
def is_reserved(digits: list) -> bool:
reserved = [0, 10, 127]
if digits[0] in reserved:
return True
if digits[0] == 192 and digits[1] == 168:
return True
return False
def get_coords(ips: "Path", ip: str) -> Coords:
split = ip2list(ip)
if is_reserved(split):
raise ReservedIPError(f"{ip} is a reserved IP address.")
prefix = f"^{split[0]}.{split[1]}."
proc = subprocess.run(
["/usr/bin/grep", "-E", prefix, ips],
capture_output=True,
text=True
)
if proc.returncode != 0:
raise GeolocationError("Failed to split records")
ips = proc.stdout.splitlines()
for address in ips:
fields = address.split(",")
upper = fields[1]
upper_digits = ip2list(upper)
if split[2] > upper_digits[2]:
continue
if split[2] == upper_digits[2]:
if split[3] > upper_digits[3]:
continue
if split[2] <= upper_digits[2]:
return Coords(float(fields[-2]), float(fields[-1]))
raise GeolocationError("No matching records found")
def get_local_ip() -> str:
ip = ""
# TODO: use shlex
if shutil.which("dig") is not None:
proc = subprocess.run(
[
"/usr/bin/dig",
"-4",
"+short",
"myip.opendns.com",
"@resolver1.opendns.com"
],
capture_output=True,
text=True
)
if proc.returncode == 0:
ip = proc.stdout.rstrip("\n")
else:
res = requests.get(IP_ECHO, timeout=REQUEST_TIMEOUT)
res.raise_for_status()
ip = res.text
return ip
def resolve_ip(address: Record) -> Record:
# TODO: unit test for resolved ips
"""
Multiple game modes may be hosted on the same IP and query port,
but resolve to different game ports. The canonical record must
contain the real game port. This is merely used when saving a
record as a UUID.
"""
res = a2s.info((address.ip, address.qport))
real_gport = res.port
return Record(address.ip, real_gport, address.qport)
def is_valid_port(port: str) -> bool:
if len(port) < 1:
return True
if (
not port.isdigit()
or int(port) == 0
or int(port[0]) == 0
or int(port) > 65535
):
return True
return False
# TODO: deprecated
#def get_local_coords(ip: str):
# url = COORDS_API + "/" + ip
# #local res=$(curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"')
# return url
+26
View File
@@ -0,0 +1,26 @@
import gi # noqa E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk
def is_navkey(key: int) -> bool:
nav_keys = [
Gdk.KEY_Down,
Gdk.KEY_Up,
Gdk.KEY_Right,
Gdk.KEY_Page_Down,
Gdk.KEY_Page_Up,
Gdk.KEY_j,
Gdk.KEY_k,
Gdk.KEY_g,
Gdk.KEY_G,
]
if key in nav_keys:
return True
return False
def is_ctrl_mask(event: Gdk.EventKey) -> bool:
if event.keyval is Gdk.KEY_l \
and event.state is Gdk.ModifierType.CONTROL_MASK:
return True
else:
return False
+16
View File
@@ -0,0 +1,16 @@
import os
import locale
def number(num: int | float) -> str:
spec = ""
if type(num) is int:
spec = "%d"
if type(num) is float:
spec = "%.2f"
return locale.format_string(spec, num, grouping=True)
def set_locale():
locale.setlocale(locale.LC_ALL, "")
user_loc = loc if (loc := os.getenv("LC_CTYPE")) else "en_US.UTF-8"
locale.setlocale(locale.LC_NUMERIC, loc)
+28
View File
@@ -0,0 +1,28 @@
import subprocess
from pathlib import Path
from dzgui.const.constants import VM_FILE, MIN_COUNT
def get_map_count() -> int | None:
path = Path(VM_FILE)
if path.is_file() is False:
return None
count = int(path.read_text())
return count
# TODO: unfinished
#if count < MIN_COUNT:
# print("needs sudo escalation")
# # pop prompt
# # get response
# set_map_count()
#return count
# TODO: unfinished
def set_map_count() -> None:
conf = "/etc/sysctl.d/dayz.conf"
count = f"vm.max_map_count={MIN_COUNT}"
with open(conf, "w") as f:
f.write(count)
# TODO: use concat_bash_args()
subprocess.run(["/usr/bin/sudo", "sysctl", "-p", conf])
+12
View File
@@ -0,0 +1,12 @@
import gi # noqa E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
def set_surrounding_margins(widget: Gtk.Widget, margin: int) -> None:
"""
Utility function that sets all margins
on a widget to a uniform integer value
"""
widget.set_margin_top(margin)
widget.set_margin_start(margin)
widget.set_margin_end(margin)
+61
View File
@@ -0,0 +1,61 @@
import os
import shlex
import subprocess
from dzgui.const.constants import APPID_DAYZ
from dzgui.const.enum import RowType
from dzgui.const import endpoints
from dzgui.util.bash import concat_bash_args
links = {
RowType.DOCS: endpoints.GITHUB_PAGES,
RowType.DOCS_FALLBACK: endpoints.CODEBERG_PAGES,
RowType.BUGS: endpoints.GITHUB_ISSUES,
RowType.FORUM: endpoints.FORUM,
RowType.SPONSOR: endpoints.SPONSORS,
RowType.CHNG_STEAM_API: endpoints.STEAM_API_SETUP,
RowType.CHNG_BM_API: endpoints.BM_API_SETUP,
RowType.CONN_BY_ID: endpoints.BM_BROWSE,
RowType.ADD_BY_ID: endpoints.BM_BROWSE,
}
def get_browser() -> str:
if (browser := os.getenv("BROWSER")) is not None:
command = browser
else:
command = "xdg-open"
return command
def open_user_workshop(uid: str, client: str) -> None:
base = f"https://steamcommunity.com/profiles/{uid}/myworkshopfiles/?appid={APPID_DAYZ}&browsefilter=mysubscriptions"
uri = endpoints.OPEN_URL + base
args = concat_bash_args(client)
args.append(uri)
subprocess.Popen([*args])
def open_link_by_url(url: str) -> None:
command = get_browser()
args = concat_bash_args(command)
args.append(url)
subprocess.Popen([*args])
def open_link_by_rowtype(enum: RowType) -> None:
command = get_browser()
args = concat_bash_args(command)
try:
url = links[enum]
except KeyError:
return
args.append(url)
subprocess.Popen([*args])
def open_workshop_page(uid: str, client: str) -> None:
uri = endpoints.WORKSHOP + uid
args = concat_bash_args(client)
args.append(uri)
subprocess.Popen([*args])
+6
View File
@@ -0,0 +1,6 @@
import re
def redact(text: str) -> str:
r = r"(/home/)([^/])*"
cleaned = re.sub(r, r"/home/REDACTED", text)
return cleaned
+479
View File
@@ -0,0 +1,479 @@
from dataclasses import dataclass
from typing import List, Dict
from dzgui.const.constants import SYSTEM_LOG
def build_missing(build: str) -> str:
msg = (
f"This server is running {build}. You can install "
f"{build} by searching for it in your Steam library. "
f"If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again."
)
return msg
def build_path_invalid(build: str) -> str:
msg = (
f"Steam is reporting that {build} is installed at a non-existent location. "
f"If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again."
)
return msg
# General
dialog_header = "DZGUI - Dialog"
dz = "DayZ"
dz_exp = "DayZ Experimental"
delimiter = "␞"
# GenericDialog
main_menu = "Return to main menu"
exit_app = "Exit"
server_details = "Server details"
modlist = "Modlist"
input_required = "User input required"
confirm = "Confirmation"
notice = "Notice"
wait = "Please wait"
# Wait dialogs
ping = "Ping"
mod = "Mod"
_id = "ID"
# ContextMenu
add_note = "Add note"
edit_note = "Edit note"
show_mods = "Show server-side mods"
show_details = "Server details"
refresh_players = "Refresh player count"
open_workshop = "Open in Steam Workshop"
delete_mod = "Delete mod"
copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard"
add = "Add to my servers"
remove = "Remove from my servers"
remove_history = "Remove from history"
# Columns
server_mod_cols = ["Mod", "ID", "Installed"]
mod_cols = ["Mod", "Symlink", "Dir", "Size (MiB)", "Color"]
log_cols = ["Timestamp", "Flag", "Traceback", "Message"]
browser_cols = [
"Name",
"Map",
"Perspective",
"Gametime",
"Players",
"Maximum",
"Queue",
"IP",
"Qport",
"Ping",
]
# DetailsDialog
server_message = "Server message"
workshop = "Enter/double click a row to open in Steam Workshop."
# LanDialog
default_port = "Use default query port (27016)"
custom_port = "Enter custom query port"
scan_servers = "Scan LAN servers"
select_port = "Select the query port"
# Errors
config_not_found = "DZGUI configuration file not found. Please exit and restart."
cannot_acquire_lock = "DZGUI is already open."
something_wrong = "Something went wrong."
steam_key_missing = "No Steam API key is set."
malformed_mods = "Found mods on system, but was unable to parse results."
steam_missing = "Local Steam installation is not set, possibly malformed config file."
build_corrupted = "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
api_warn_msg = """No servers returned. Possible causes:
no servers in favorites/history, local network issue, or API key on cooldown.
Return to the main menu, wait 30s, and try again.
If this issue persists, your API key may be defunct.
"""
server_timeout = "Timed out when querying server, check IP or try again later"
server_error = "Error while contacting server, possibly timed out. Please wait and try again."
server_protected = (
"This server is password-protected and you will be "
"prompted when connecting. Do you want to proceed?"
)
# KeybindingsDialog
navigation = {
"Enter/space/double click": "select row item",
"Down arrow": "move down a row/scroll down",
"Up arrow": "move up a row/scroll up",
"Right arrow": "jump to sidebar from main area",
"Left arrow": "jump to main area from sidebar",
"Tab": "cycle forward through elements",
"Shift-tab": "cycle backward through elements",
"ESC/Enter": "close dialogs",
"?": "show/hide this dialog",
"Ctrl-q": "Quit",
}
servers = {
"Enter/space/double-click": "connect to server",
"Right-click/Ctrl-l": "additional context menus",
"Ctrl-r": "refresh players",
"Ctrl-p": "refresh ping",
"Ctrl-f": "jump to keyword search field",
"Ctrl-m": "jump to maps field",
"Ctrl-d": "toggle dry run (debug) mode",
"ESC": "return to table",
"1-9": "toggle filter 1-9 on/off",
"0": "toggle filter 10",
"Minus": "toggle filter 11",
"Backslash": "toggle filter 12",
}
vim = {
"j": "Move down a row/scroll up",
"k": "Move up a row/scroll down",
"l": "Jump to main area from sidebar",
"h": "Jump to sidebar from main area",
"gg": "Jump to first row/top of page",
"G": "Jump to last row/bottom of page",
}
key_contexts = ["Servers", "Navigation", "Vim-style keys"]
key_header = "Keybindings"
# ModSelectionPanel
# labels
label_main_menu = "Main menu"
# buttons
ping_servers = "Ping servers"
debug_mode = "Debug mode"
keys_button = "Keys"
keys_tooltip = "Opens the keybindings dialog"
debug_tooltip = (
"Used to perform a dry run without\n"
"actually connecting to a server"
)
ping_tooltip = (
"Refresh the ping for visible servers.\n"
"Available once per unique filter context"
)
statusbar_helptext = "Select a row to see its detailed description"
# use e.g. filters.1pp
filter_1pp = "1PP"
filter_3pp = "3PP"
filter_day = "Day"
filter_night = "Night"
filter_empty = "Empty"
filter_full = "Full"
filter_lowpop = "Low pop"
filter_nonascii = "Non-ASCII"
filter_duplicate = "Duplicate"
filter_official = "Official"
filter_unofficial = "Unoffic."
filter_modded = "Modded"
# maps
all_maps = "All maps"
# state
unknown = "Unknown"
none_provided = "None provided"
disabled = "Disabled"
enabled = "Enabled"
null = "-"
none = "None"
unspecified = "not specified"
# DLC
dlc_frostline = "Frostline"
# platform
windows = "Windows"
linux = "Linux"
# misc
self_workshop = "Open Steam workshop"
settings_updated = "Settings updated!"
@dataclass(slots=True, frozen=True)
class ModPanelStrings:
header: str
unhighlight_stale: str
unhighlight_stale_tooltip: str
highlight_stale: str
highlight_stale_tooltip: str
delete_selected: str
delete_selected_tooltip: str
unselect_all: str
unselect_all_tooltip: str
select_all: str
select_all_tooltip: str
bulk_select: str
clear_highlights: str
select_stale: str
select_stale_tooltip: str
@dataclass(slots=True, frozen=True)
class Dialog:
querying: str
fetching: str
filtering: str
refreshing: str
details: str
modlist: str
working: str
updating_mods: str
@dataclass(slots=True, frozen=True)
class Init:
is_steam_running: str
is_dayz_running: str
requires_steam: str
@dataclass(slots=True, frozen=True)
class Button:
main_label: str
main_tooltip: str
mods_label: str
mods_tooltip: str
options_label: str
options_tooltip: str
help_label: str
help_tooltip: str
exit_label: str
exit_tooltip: str
@dataclass(slots=True, frozen=True)
class Crumbs:
changelog: str
history: str
keys: str
lan: str
log: str
main: str
mods: str
my_servers: str
options: str
servers: str
thanks: str
@dataclass(slots=True, frozen=True)
class Thanks:
header: str
description: str
users: list[str]
@dataclass(slots=True, frozen=True)
class Options:
header: str
steam_web: str
bm_web: str
enter_steam: str
enter_bm: str
steam_placeholder: str
bm_placeholder: str
name_placeholder: str
last_used: str
always_fs: str
steam: str
flatpak: str
km: str
mi: str
client: str
window_size: str
distance: str
name: str
manual_dl: str
auto_dl: str
update: str
install_mode: str
force_update: str
dl_eventbox: str
force_eventbox: str
self_update: str
no_self_update: str
api_keys: str
prefs: str
mods: str
version: str
branch: str
stable: str
testing: str
manual_sub_msg: str
init = Init(
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.",
requires_steam = "DZGUI requires that Steam or Flatpak Steam be installed on the system.",
)
mod_panel = ModPanelStrings(
header = "Mod actions",
unhighlight_stale = "Unhighlight stale",
unhighlight_stale_tooltip = "Clears highlight from stale mods",
highlight_stale = "Highlight stale",
highlight_stale_tooltip = (
"Shows locally-installed mods which are not used by any server "
"in your Saved Servers"
),
delete_selected = "Delete selected",
delete_selected_tooltip = "Deletes selected mods from the system",
unselect_all = "Unselect all",
unselect_all_tooltip = "Bulk unselects all mods",
select_all = "Select all",
select_all_tooltip = "Bulk selects all mods",
bulk_select = "Bulk selects all currently highlighted mods",
clear_highlights = "Clears highlights and reverts the table to a default state",
select_stale = "Select stale",
select_stale_tooltip = "Only selects highlighted stale mods",
)
thanks = Thanks(
header = "# Special thanks",
description = (
"This page recognizes beta testers, collaborators, code "
"contributors, and sponsors of the project in alphabetical order."
),
users = [
"bongjutsu",
"Deku",
"dj3hac",
"GaryBlackbourne",
"jiriks74",
"Johnofwrong",
"MatheusLasserr",
"nolan-perez",
"scandalouss",
"StevelDusa",
"Thoughtduck216",
],
)
options = Options(
header="Options",
steam_web="Steam API page",
bm_web="Battlemetrics API page",
enter_steam="Enter your Steam API key",
enter_bm="Enter your Battlemetrics API key",
steam_placeholder="Steam API key",
bm_placeholder="Battlemetrics API key",
name_placeholder="Identifies you to other players in-game",
last_used="Last used dimensions",
always_fs="Always fullscreen",
steam="Steam",
flatpak="Flatpak (experimental)",
km="km (kilometers)",
mi="mi (miles)",
client="Steam client",
window_size="Window size at boot",
distance="Distance display",
name="Player name",
manual_dl="Manual",
auto_dl="Auto",
update="Update",
install_mode = "Mod install mode",
force_update = "Force update local mods",
dl_eventbox = (
"Manual: prompt to subscribe to mods in Steam. "
"Auto: unmanned downloads."
),
force_eventbox = "Synchronize all local mods. Automatic mode must be enabled.",
self_update = (
"Stable: only contains stable features. "
"Testing: pre-release beta, contains new features."
),
no_self_update = (
"In-app updates are disabled when DZGUI is "
"installed globally (e.g., via package manager)."
),
api_keys = "API Keys",
prefs = "Preferences",
mods = "Mods",
version = "Version",
branch = "DZGUI branch",
stable = "Stable",
testing = "Testing",
manual_sub_msg = """When switching from MANUAL to AUTO mod install mode,
DZGUI will manage mod installation and deletion for you.
To prevent conflicts with Steam Workshop subscriptions and old mods from being downloaded
when Steam updates, you should unsubscribe from any existing Workshop mods you manually subscribed to.
Open your Profile > Workshop Items and select 'Unsubscribe from all'
on the right-hand side.
""",
)
dialog = Dialog(
querying = "Querying server",
fetching = "Fetching server metadata",
filtering = "Filtering servers",
refreshing = "Refreshing player count",
details = "Fetching details",
modlist = "Fetching modlist",
working = "Working",
updating_mods = "Updating mods",
)
buttons = Button(
main_label="Main menu",
main_tooltip="Search for and connect to servers",
mods_label="Mods",
mods_tooltip="Manage local mods",
options_label="Options",
options_tooltip="Advanced options",
help_label="Help",
help_tooltip="Troubleshooting and documentation",
exit_label="Exit",
exit_tooltip="Quits the application",
)
crumbs = Crumbs(
changelog="Help > Changelog",
history="Main menu > Recent servers",
keys="Keybindings",
lan="Main menu > Local servers",
log="Help > Debug log",
main="Main menu",
mods="Mods",
my_servers="Main menu > Saved servers",
options="Options",
servers="Main menu > Server browser",
thanks="Help > Special thanks"
)
checkmark = "✓"
no_mods = "No local mods found."
@dataclass(slots=True, frozen=True)
class Flags:
description: str
version: str
uninstall: str
flags = Flags(
description="DayZ server browser and mod manager",
version="Print version information",
uninstall="Uninstall data files (use prior to 'pip uninstall')"
)
@dataclass(slots=True, frozen=True)
class FilePicker:
title: str
placeholder: str
picker = FilePicker(
title = "Save diagnostic log to file",
placeholder = SYSTEM_LOG,
)
api_error = "API key validation error or timeout. See 'Help > Show debug log' for details."
+50
View File
@@ -0,0 +1,50 @@
import hashlib
import logging
from pathlib import Path
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP
from dzgui.const.enum import Preferences
from dzgui.config.query import lookup
import dzgui.api.pefile as PeFile
logger = logging.getLogger(__name__)
def rebuild_symlinks(config: Path) -> None:
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
for file in dayz_path.iterdir():
if file.is_symlink():
file.unlink()
workshop = get_local_mod_path(steam_path)
for uid in get_local_mod_ids(steam_path):
uid = str(uid)
md5sum = _hash(str(uid))
Path(dayz_path / md5sum).symlink_to(workshop / uid)
def clone_symlinks(config: Path) -> None:
"""
Used after any symlink operation
"""
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
try:
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
exp_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP)
except Exception as e:
logger.critical(e)
return
# TODO: test these two operations
for file in exp_path.iterdir():
if file.is_symlink():
file.unlink()
for file in dayz_path.iterdir():
if file.is_symlink():
uid = file.name
Path(exp_path / uid).symlink_to(dayz_path / uid)