Compare commits

...

14 Commits

Author SHA1 Message Date
aclist
d3f64c1ba5 chore: trigger backup workflow
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-18 22:05:46 +09:00
aclist
5da3397a29 Merge branch 'dzgui7' of github.com:aclist/dztui into dzgui7 2026-05-18 22:03:02 +09:00
aclist
eca6df90d4 chore: add .github directory 2026-05-18 22:02:16 +09:00
aclist
2d58df1c89
Merge pull request #288 from aclist/feat/manage-updates
feat: manage updates
2026-05-18 21:46:47 +09:00
aclist
de4521a366 chore: use string file 2026-05-18 21:45:08 +09:00
aclist
fe9f488e10 fix: source metadata directly from wheel 2026-05-18 21:19:18 +09:00
aclist
7b5d99c34b feat: print version number when loading 2026-05-18 21:18:54 +09:00
aclist
0a84c20d1d fix: explicitly set arcname in tarball 2026-05-18 20:48:23 +09:00
aclist
d5309c1ddf chore: include version number in tarball 2026-05-18 20:37:42 +09:00
aclist
418c0f026d chore: restore ALLOW_UPDATES flag 2026-05-18 20:30:02 +09:00
aclist
af3a9cfa77 docs: update changelog 2026-05-18 20:25:31 +09:00
aclist
1b20969c30 chore: clear typehinting errors 2026-05-18 20:24:59 +09:00
aclist
7f1125432e chore: bump version 2026-05-18 20:12:20 +09:00
aclist
c063059c19 feat: auto update 2026-05-18 20:11:51 +09:00
20 changed files with 300 additions and 43 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: aclist

57
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Bug report
description: Submit bugs that affect functionality here
title: "[BUG] Your title here"
labels: ["bug"]
assignees:
- aclist
body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: checkboxes
id: kb
attributes:
label: Did you check if this issue already exists?
options:
- label: I have checked the [Knowledge Base](https://aclist.github.io/dzgui/kb.html) and searched through [past issues](https://github.com/aclist/dztui/issues?q=is%3Aissue+is%3Aclosed) and did not find my issue there.
required: true
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the bug
description: What did you expect to happen?
placeholder: "E.g., When doing X, Y occurred. Include as much detail as possible."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Additional log output
description: Attach the file `$HOME/.local/state/dzgui/DZGUI_DEBUG.log` by clicking this area to highlight it and then dragging the file in.
validations:
required: false

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

47
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Feature suggestion
description: Submit requests for new features here
title: "[FEAT] Your title here"
labels: ["enhancement"]
assignees:
- aclist
body:
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the feature
description: A clear and detailed description
placeholder: "E.g., Add a dialog to the Options menu that automatically lets the user do Y"
validations:
required: true
- type: textarea
id: why
attributes:
label: Describe the benefit
description: How will this feature benefit large numbers of users?
placeholder: "E.g., Currently, users have to manually do X to achieve Y. Implementing this feature would achieve A, B, and C."
validations:
required: true

50
.github/ISSUE_TEMPLATE/trouble.yml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Troubleshooting
description: Submit user error/troubleshooting questions here
title: "[HELP] Your title here"
labels: ["troubleshooting"]
assignees:
- aclist
body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: A detailed description. What did you expect to happen?
placeholder: "Include as much detail as possible and state the steps leading up to the problem. E.g., First I do this, then I do this, then this happens."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Additional log output
description: Attach the file `$HOME/.local/state/dzgui/DZGUI_DEBUG.log` by clicking this area to highlight it and then dragging the file in.
validations:
required: false

18
.github/workflows/mirror.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Mirror to Codeberg
on:
push:
workflow_dispatch:
jobs:
mirror-to-codeberg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
if: ${{ vars.GIT_REMOTE != '' }}
with:
target_repo_url: ${{ vars.GIT_REMOTE }}
ssh_private_key: ${{ secrets.GIT_SSH_PRIVATE_KEY }}

View File

@ -65,7 +65,6 @@
## Unreleased
- Load offline mods
- Choose to jump into splash screen instead of server
- Setup wizard
- Local documentation
- Raw debug command in context menu

View File

@ -15,5 +15,5 @@ class UserPrefs:
coords: Union["Coords", None]
version: str
paths: "Xdg"
update_available: bool
latest_release: str | None
use_miles: bool

View File

@ -73,3 +73,7 @@ UBUNTU_STEAM_PATH = ".steam/steam"
DEBIAN_STEAM_PATH = ".steam/debian-installation"
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
TMP_PATH = "/tmp"
TMP_TARBALL = "/tmp/dzgui.tar.gz"
TMP_EXE = "/tmp/dzgui"

13
dzgui/const/update.py Normal file
View File

@ -0,0 +1,13 @@
"""
This file is intended to be patched by package maintainers
repackaging DZGUI for use in different distributions.
If DZGUI is going to be installed globally via a package manager
or other means and will reside in an immutable location, or needs to be
explicitly bound to a certain version, set the flag below to False.
This will disable the ability for the application to self-manage and
will suppress the "Updates available" button in the gutter.
"""
ALLOW_UPDATES = True

View File

@ -65,7 +65,6 @@
## Unreleased
- Load offline mods
- Choose to jump into splash screen instead of server
- Setup wizard
- Local documentation
- Raw debug command in context menu

View File

@ -2,6 +2,7 @@ import logging
import requests
from packaging.version import Version
from typing import Any
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES
@ -9,28 +10,30 @@ from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES
logger = logging.getLogger(APP_NAME)
def get_latest_release() -> str | None:
def get_latest_release() -> tuple[Any, Any] | tuple[None, None]:
tag = None
url = None
# TODO: check order; github often has gateway errors
for url in [GITHUB_RELEASES, CODEBERG_RELEASES]:
try:
res = requests.get(url, timeout=REQUEST_TIMEOUT)
if res.status_code == 200:
tag = res.json()["tag_name"]
url = res.json()["assets"][0]["browser_download_url"]
break
except Exception as e:
logger.critical(e)
continue
return tag
return tag, url
def check_updates(version: str) -> bool:
def check_updates(version: str) -> str | None:
try:
latest = get_latest_release()
latest, url = get_latest_release()
if latest is None:
return False
return None
if Version(version) >= Version(latest):
return False
return True
return None
return url
except Exception:
return False
return None

View File

@ -89,6 +89,8 @@ def main() -> None:
version = get_version()
set_locale()
print(f"{APP_NAME} {version}")
# NOTE: consider aborting this check if steam deck
xdg_paths = get_xdg_paths()
XDG = parse_filepaths(xdg_paths)
@ -116,7 +118,7 @@ def main() -> None:
with open(XDG.debug, "w") as f:
f.truncate(0)
update_available = check_updates(version)
latest_release = check_updates(version)
if _is_steam_deck is False:
# TODO: sudo escalation dialog
@ -142,7 +144,7 @@ def main() -> None:
coords=local_coords,
version=version,
paths=XDG,
update_available=update_available,
latest_release=latest_release,
use_miles=use_miles,
)
print(boot.all_ok)

View File

@ -29,7 +29,7 @@ def call_on_thread(
self = args[0]
stored = StoredFunc(func, *args, **kwargs)
if not hasattr(self, "thread_man"):
raise AttributeError
raise AttributeError(f"Object '{self}' has no attribute 'thread_man'")
if type(self.thread_man) is not ThreadingManager:
raise TypeError(
"Attribute 'thread_man' must be of type 'ThreadingManager'"

View File

@ -1,2 +1,6 @@
waiting_for_launch = "Waiting for DayZ to launch"
waiting_for_mods = "Waiting for Steam to update mods"
fetching_update = "Fetching update"
failed_to_update = "Failed to update DZGUI executable"
update_success = "Updated successfully. Please exit and relaunch."

View File

@ -38,6 +38,7 @@ input_required = "User input required"
confirm = "Confirmation"
notice = "Notice"
wait = "Please wait"
restart_required = "App relaunch required"
# Wait dialogs
ping = "Ping"

View File

@ -1,14 +1,32 @@
import logging
import os
import requests
import shutil
import subprocess
import tarfile
from typing import Literal, TYPE_CHECKING
from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING
from dzgui.const.endpoints import GITHUB_USER_RELEASES
from dzgui.const.constants import (
APP_NAME,
NO_EXPAND,
NO_FILL,
FILL,
NO_PADDING,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
)
from dzgui.const.update import ALLOW_UPDATES
from dzgui.const.enum import ServerTab
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import dialogs
from dzgui.util.clip import copy_clipboard
from dzgui.util.open_links import open_link_by_url
from dzgui.views.components.buttonbox import ButtonBox
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi
@ -20,11 +38,14 @@ if TYPE_CHECKING:
from dzgui.controllers.emitter import Emitter
from dzgui.views.trees.tree_servers import ServerTreeView
logger = logging.getLogger(APP_NAME)
class RightPanel(Gtk.Box):
def __init__(self, controller: "Controller"):
super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL)
self.thread_man = ThreadingManager(controller)
self.controller = controller
self.controller.register_widget("right_panel", self)
@ -44,7 +65,7 @@ class RightPanel(Gtk.Box):
prefs = self.controller.get_prefs()
version = prefs.version
update = prefs.update_available
update = prefs.latest_release
self.version_label = Gtk.Label(
label=version,
@ -64,7 +85,7 @@ class RightPanel(Gtk.Box):
self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING)
self.version_button = IconTextButton(
self.update_button = IconTextButton(
"dialog-information-symbolic", label="Updates available"
)
@ -74,15 +95,54 @@ class RightPanel(Gtk.Box):
valign=Gtk.Align.END,
spacing=10,
)
if update:
self.version_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.version_button)
self.version_button.connect("clicked", self._on_version_button_clicked)
if ALLOW_UPDATES and update is not None:
self.update_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.update_button)
self.update_button.connect(
"clicked", self._on_update_button_clicked, update
)
self.gutter_box.add(eb)
self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING)
def _on_version_button_clicked(self, button: Gtk.Button) -> None:
open_link_by_url(GITHUB_USER_RELEASES)
def _on_update_success(self) -> None:
msg = dialogs.update_success
dialog = QuitDialog(self.controller, msg)
dialog.run()
def _on_update_failure(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg)
dialog.run()
@call_on_thread(dialogs.fetching_update)
def _on_update_button_clicked(self, button: Gtk.Button, url: str) -> None:
try:
res = requests.get(url)
if res.status_code == 200:
with open(TMP_TARBALL, "wb") as file:
file.write(res.content)
with tarfile.open(TMP_TARBALL) as tar:
tar.extractall(TMP_PATH)
exe_path = os.getenv("PYAPP")
if exe_path is None:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
return
shutil.move(TMP_EXE, exe_path)
proc = subprocess.run([exe_path, "self", "restore"])
if proc.returncode == 0:
func = StoredFunc(self._on_update_success)
self.thread_man.set_cleanup_func(func, destroy_first=True)
else:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
except Exception as e:
func = StoredFunc(self._on_update_failure, e)
self.thread_man.set_cleanup_func(func, destroy_first=True)
logger.warning(e)
def _on_server_page_changed(
self, emitter: "Emitter", page: "ServerTreeView"

View File

@ -81,7 +81,7 @@ class NotifyDialog(GenericDialog):
controller=controller,
text=strings.notice,
mtype=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.OK_CANCEL,
buttons=Gtk.ButtonsType.OK,
secondary=secondary,
)
@ -143,7 +143,7 @@ class QuitDialog(GenericDialog):
def __init__(self, controller: "Controller", secondary: str):
super().__init__(
controller=controller,
text=strings.wait,
text=strings.restart_required,
mtype=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.NONE,
secondary=secondary,

View File

@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b1"
version = "7.0.0b2"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"

View File

@ -3,43 +3,39 @@ import os
import subprocess
import tarfile
from importlib import metadata
from pathlib import Path
from dzgui.const.constants import APP_NAME_LOWER
root = Path(__file__).resolve().parents[1]
output = root.joinpath("dist")
pyapp_dir = root.joinpath("pyapp-latest")
builder = build.ProjectBuilder(root)
builder.build("wheel", output_directory=output)
wheel = builder.build("wheel", output_directory=output)
stem = Path(wheel).stem
tarname = f"{stem}.tar.gz"
interpreter = "py3"
arch = "none"
target = "any"
version = metadata.version(APP_NAME_LOWER)
filename = f"{APP_NAME_LOWER}-{version}-{interpreter}-{arch}-{target}.whl"
metadata = stem.split("-")
appname = metadata[0]
version = metadata[1]
wheel = str(output.joinpath(filename))
entrypoint = "dzgui.main:main"
env = os.environ
env["PYAPP_PROJECT_VERSION"] = version
env["PYAPP_PROJECT_NAME"] = APP_NAME_LOWER
env["PYAPP_PROJECT_NAME"] = appname
env["PYAPP_EXEC_SPEC"] = entrypoint
env["PYAPP_PROJECT_PATH"] = wheel
env["PYAPP_DISTRIBUTION_EMBED"] = "1"
env["PYAPP_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_PYTHON_VERSION"] = "3.13"
env["PYAPP_PASS_LOCATION"] = "true"
proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir)
if proc.returncode == 0:
output_exe = root.joinpath("pyapp-latest/target/release/pyapp")
release_exe = output.joinpath(APP_NAME_LOWER)
release_exe = output.joinpath(appname)
output_exe.rename(release_exe)
tarname = APP_NAME_LOWER + ".tar.gz"
tarpath = output.joinpath(tarname)
with tarfile.open(tarpath, "w:gz") as tar:
tar.add(release_exe)
tar.add(release_exe, arcname=appname)
print(f"Wrote tarfile to '{tarpath}'")