mirror of
https://github.com/aclist/dztui.git
synced 2026-10-30 22:52:42 +01:00
feat: dzgui 7 alpha
This commit is contained in:
1 parent
1acdbd58c5
commit
2e5b771137
150 files changed
+10359
No files matched your search
Whitespace-only changes.
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
import shlex
|
||||
|
||||
def concat_bash_args(command: str) -> list[str]:
|
||||
return shlex.split(command)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -0,0 +1,6 @@
|
||||
import re
|
||||
|
||||
def redact(text: str) -> str:
|
||||
r = r"(/home/)([^/])*"
|
||||
cleaned = re.sub(r, r"/home/REDACTED", text)
|
||||
return cleaned
|
||||
@@ -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."
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user