Compare commits

...
41 Commits
Author SHA1 Message Date
aclist 2963c4e01c Merge pull request #299 from aclist/chore/move-updates
chore: move updates
2026-05-25 16:18:07 +09:00
aclist f849cbc183 chore: update tests 2026-05-25 16:07:44 +09:00
aclist 77505db621 fix: collision with native method name 2026-05-25 15:13:08 +09:00
aclist ba7a2b7ab4 fix: fallback for missing entries in columns file 2026-05-25 15:11:21 +09:00
aclist 6d7ed80996 Merge branch 'chore/move-updates' of github.com:aclist/dztui into chore/move-updates 2026-05-25 00:04:14 +09:00
aclist 72024c5d43 fix: refresh increment overwriting when changing tabs 2026-05-25 00:04:06 +09:00
aclist 0426d50d3e fix: refresh increment ov refresh decrement resetting when changing tabs 2026-05-25 00:03:21 +09:00
aclist 26ee560cb4 chore: test build version after export 2026-05-24 18:56:41 +09:00
aclist 6ec77f6542 fix: pass update args to manager 2026-05-24 01:53:16 +09:00
aclist 59959ae3a1 chore: clear typehinting errors 2026-05-24 01:49:08 +09:00
aclist 05fd95cd8e chore: bump version 2026-05-24 01:34:22 +09:00
aclist 86ba8d9187 chore: move update logic to manager 2026-05-24 01:34:16 +09:00
aclist e715e6da1c fix: use LC_ALL var in test 2026-05-23 22:23:44 +09:00
aclist 18ba851b7e chore: drop unused code 2026-05-23 22:23:29 +09:00
aclist d2643d08e9 Merge pull request #296 from aclist/fix/empty-statusbar
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: statusbar being wiped when calcdist fails on empty row
2026-05-21 21:07:23 +09:00
aclist 6274f62c93 chore: remove print statement
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-05-21 21:06:40 +09:00
aclist 24609a1c71 docs: update changelog 2026-05-21 20:58:32 +09:00
aclist 1b344c1260 chore: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-21 20:55:01 +09:00
aclist e72e09eec1 feat: save filters on exit 2026-05-21 20:42:24 +09:00
aclist 00e78b1228 fix: set filter panel to insensitive on lan load 2026-05-21 20:42:00 +09:00
aclist efddd61ea1 chore: bump version 2026-05-21 01:49:55 +09:00
aclist d006908bbc fix: statusbar being wiped when calcdist fails on empty row 2026-05-21 01:47:05 +09:00
aclist 5ba6b6c040 Merge pull request #295 from aclist/fix/lan-server
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: break out of loop when sek out of loop on first server
2026-05-20 00:36:58 +09:00
aclist d3b9ba031d Merge branch 'fix/lan-server' of github.com:aclist/dztui into fix/lan-server 2026-05-20 00:35:49 +09:00
aclist 8bfcb6c8fe fix: break out of loop when scanning LAN 2026-05-20 00:35:35 +09:00
aclist 353b10ebfc fix: break out of loop when sek out of loop on first server 2026-05-20 00:25:10 +09:00
aclist 698150645d Merge pull request #290 from aclist/feat/airgapped-installer
feat: airgapped installer
2026-05-20 00:09:31 +09:00
aclist 87f347870f chore: clean up try/except block
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-05-19 23:41:51 +09:00
aclist 74bf4b5cfc se str, not Path
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-19 18:35:47 +09:00
aclist 896799f904 chore: update string 2026-05-19 18:34:35 +09:00
aclist 0f591e658c fix: call proper workshop method 2026-05-19 18:31:59 +09:00
aclist f9083b33ef fix: change lan panel tate on map/unmap 2026-05-19 18:28:45 +09:00
aclist 89fcfcb5d6 fix: use hostname.local 2026-05-19 18:26:09 +09:00
aclist fc7f1d7815 chore: update build script paths 2026-05-19 18:25:29 +09:00
aclist fb36425083 chore: bump version 2026-05-19 06:23:53 +09:00
aclist c5d8afbc4d chore: build dependencies into distribution tarball 2026-05-19 06:08:55 +09:00
aclist 17f732fafe feat: continue to main app after config wizard closes 2026-05-19 06:05:22 +09:00
aclist 676a7ea9eb fix: notes file may not be present 2026-05-19 06:04:45 +09:00
aclist d3f64c1ba5 chore: trigger backup workflow
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
39 changed files with 496 additions and 133 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: aclist
+57
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
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+47
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
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
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 }}
+1
View File
@@ -34,6 +34,7 @@
- Early load alerts button - Early load alerts button
- Preconnect dialog - Preconnect dialog
- Preconnect warnings/failsafes like filesize - Preconnect warnings/failsafes like filesize
- Save filters per server context between sessions
## Changed ## Changed
+10 -5
View File
@@ -1,18 +1,23 @@
# TODO: link to here from sphinx docs # TODO: link to here from sphinx docs
# Installing dev dependencies
uv pip install -e .[dev]
# Setting up precommit hooks # Setting up precommit hooks
pre-commit install pre-commit install
# Fetching submodules # Fetching submodules
git submodule update --recursive --init git submodule update --recursive --init
# Installing dev dependencies
uv pip install -e .[dev]
# Building documentation # Building documentation
sphinx-build -M html source build -a sphinx-build -M html source build -a
# Building standalone DZGUI release binaries # Building standalone DZGUI release binaries
1. Run `./scripts/dl_pyapp.sh` 1. Run `./scripts/dl_pyapp.sh`
2. Run `python3 scripts/release.py` # TODO: secondary script that packs this archive
3. Generate a release and tag against the bundled `dzgui` executable 2. Download cpython 3.13, linux, gnu, v3
https://github.com/astral-sh/python-build-standalone/releases/download/20251014/cpython-3.13.9%2B20251014-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz
3. Explicitly install dependencies into site-packages
4. Pack back into a tarball
5. Run `python3 scripts/release.py`
6. Generate a release and tag against the bundled `dzgui` executable
+1 -1
View File
@@ -34,7 +34,7 @@ DZG-004: On Steam Deck, some mods in the Workshop show a black screen when DZGUI
--------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------
This is a bug in the Steam client that is being tracked at Valve's Steam for Linux issue tracker here: https://github.com/ValveSoftware/steam-for-linux/issues/9598. This is a bug in the Steam client that is being tracked at Valve's Steam for Linux issue tracker here: https://github.com/ValveSoftware/steam-for-linux/issues/9598.
To resolve this issue, manually intervene in the Steam client by selecting a different context (e.g., Store, Library), waiting for it to load, then navigating back to the Workshop context. This should To resolve this issue, manually intervene in the Steam client by selecting a different context (e.g., Store, Library), waiting for it to load, then navigating back to the Workshop context. This should
clear the blockage and allow the contents to render. clear the blockage and allow the contents to render.
.. _DZG-005: .. _DZG-005:
+2 -1
View File
@@ -106,7 +106,7 @@ class A2SInfo:
def get_netmask() -> str: def get_netmask() -> str:
hostname = os.uname()[1] hostname = os.uname()[1]
i = socket.gethostbyname(hostname) i = socket.gethostbyname(f"{hostname}.local")
netmask = i.rsplit(".", 1)[0] netmask = i.rsplit(".", 1)[0]
return netmask return netmask
@@ -472,6 +472,7 @@ def query_by_ip(addr: str) -> A2SInfo:
return A2SInfo(Record("0", 0, 0), None) return A2SInfo(Record("0", 0, 0), None)
return query_by_record(record, update_gameport=True) return query_by_record(record, update_gameport=True)
def query_by_id(server_id: int, key: str) -> A2SInfo: def query_by_id(server_id: int, key: str) -> A2SInfo:
""" """
Used with numeric Battlemetrics IDs Used with numeric Battlemetrics IDs
+3 -3
View File
@@ -17,6 +17,7 @@ class Xdg:
system: Path system: Path
debug: Path debug: Path
ips: Path ips: Path
filters: Path
def is_writeable(path_str: str) -> bool: def is_writeable(path_str: str) -> bool:
@@ -63,9 +64,6 @@ def get_xdg_paths() -> dict:
def parse_filepaths(xdg: dict) -> Xdg: def parse_filepaths(xdg: dict) -> Xdg:
# cache = xdg["XDG_CACHE_HOME"]
# data = xdg["XDG_DATA_HOME"]
config = xdg["XDG_CONFIG_HOME"] config = xdg["XDG_CONFIG_HOME"]
state = xdg["XDG_STATE_HOME"] state = xdg["XDG_STATE_HOME"]
@@ -80,6 +78,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
history = state / "dzg.history" history = state / "dzg.history"
versions = state / "dzg.versions" versions = state / "dzg.versions"
ips = state / "ips.csv" ips = state / "ips.csv"
filters = state / "dzg.filters.json"
return Xdg( return Xdg(
config, config,
@@ -91,4 +90,5 @@ def parse_filepaths(xdg: dict) -> Xdg:
system, system,
debug, debug,
ips, ips,
filters
) )
+4
View File
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal # TODO: if servers fail to load, may leave dangling widgets waiting for a signal
class Emitter(GObject.GObject): class Emitter(GObject.GObject):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -150,3 +151,6 @@ class Emitter(GObject.GObject):
def keyword_set(self, keyword: str) -> None: def keyword_set(self, keyword: str) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def lan_page_initialized(self) -> None:
pass
+30 -11
View File
@@ -84,7 +84,6 @@ class Controller(GObject.GObject):
self.emitter = Emitter() self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled) self.emitter.connect("check_toggled", self._on_check_toggled)
self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init)
# NOTE: suppress requests until entire UI is loaded # NOTE: suppress requests until entire UI is loaded
self.loaded = False self.loaded = False
@@ -260,6 +259,21 @@ class Controller(GObject.GObject):
def select_colorized(self) -> None: def select_colorized(self) -> None:
self.mod_man.select_colorized() self.mod_man.select_colorized()
def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
servers = self.get_servers()
all_filters: dict[str, dict[str, bool]] = {}
trees = (
servers.get_browser(),
servers.get_saved(),
servers.get_recent(),
servers.get_lan(),
)
for tree in trees:
enum = str(tree.get_enum())
filters = tree.get_filter_man().get_filters()
all_filters[enum] = filters
return all_filters
def get_filter_man(self) -> "FilterManager": def get_filter_man(self) -> "FilterManager":
"""Each ServerTreeView has an atomic FilterManager""" """Each ServerTreeView has an atomic FilterManager"""
return self.get_active_treeview().get_filter_man() return self.get_active_treeview().get_filter_man()
@@ -303,8 +317,22 @@ class Controller(GObject.GObject):
self.emitter.emit("servers_loaded", tv.get_enum()) self.emitter.emit("servers_loaded", tv.get_enum())
return return
self.mediator.statusbar.set_by_context(tv.get_enum(), "") self.mediator.statusbar.set_by_context(tv.get_enum(), "")
self.mediator.filters.button_grid.block_toggles(True)
self.populate_filter_prefs()
self.mediator.filters.button_grid.block_toggles(False)
self.emitter.emit("lan_page_initialized")
ServerModelManager(self, tv).load() ServerModelManager(self, tv).load()
def populate_filter_prefs(self) -> None:
tv = self.get_active_treeview()
filters = self.config_man.get_filters(tv)
if filters is None:
return
filter_man = self.get_filter_man()
for filt, state in filters.items():
filter_man.set_filter(filt, state)
self.mediator.filters.set_filters(filters)
def get_dist_cache(self) -> dict[str, "Haversine"]: def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache return self.dist_cache
@@ -319,16 +347,7 @@ class Controller(GObject.GObject):
filter_man = self.get_filter_man() filter_man = self.get_filter_man()
return filter_man.get_filters() return filter_man.get_filters()
# TODO: rename # FIXME: wipe maps store when changing tabs if model is none
def _on_servers_loaded_init(self, emitter: "Emitter") -> None:
"""Triggered after servers load but prior to maps loading"""
# FIXME: wipe maps store when changing tabs if model is none
# e.g. select recent, toggle map, then select lan -> not wiped
tv = self.get_active_treeview()
if tv.loaded is False:
return
store = self.get_map_store()
self.emitter.emit("load_maps", store)
def has_server_model(self) -> bool: def has_server_model(self) -> bool:
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
+1
View File
@@ -34,6 +34,7 @@
- Early load alerts button - Early load alerts button
- Preconnect dialog - Preconnect dialog
- Preconnect warnings/failsafes like filesize - Preconnect warnings/failsafes like filesize
- Save filters per server context between sessions
## Changed ## Changed
+20 -9
View File
@@ -17,17 +17,28 @@ def has_new_config(config: Path) -> bool:
return config.exists() return config.exists()
def migrate_cols_file(res: Path) -> None: def convert_cols_file(res: Path) -> dict[str, int] | None:
# NOTE: dzg.columns.json is API 7 spec j = read_json(res)
old_res = Path.home() / LEGACY_COLS_PATH cols = j["cols"]
if old_res.is_file(): # NOTE: implies prior conversion
j = read_json(old_res) if "View" in cols:
cols = j["cols"] return None
if "View" in cols: # NOTE: user may not have changed these widths in API 6
return try:
cols["View"] = cols.pop("Perspective") cols["View"] = cols.pop("Perspective")
cols["Max"] = cols.pop("Maximum") cols["Max"] = cols.pop("Maximum")
write_json(j, res) except Exception:
pass
return j
def migrate_cols_file(res: Path) -> None:
# NOTE: filename "dzg.columns.json" is API 7 spec
old_res = Path.home() / LEGACY_COLS_PATH
if old_res.is_file():
j = convert_cols_file(old_res)
if j is not None:
write_json(j, res)
def copy_state_files(state_path: Path) -> None: def copy_state_files(state_path: Path) -> None:
+4 -1
View File
@@ -112,6 +112,9 @@ def main() -> None:
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
SetupWizard(_is_steam_deck, XDG.config) SetupWizard(_is_steam_deck, XDG.config)
# NOTE: implies that setup wizard failed or was closed
if has_new_config(XDG.config) is False:
return return
setup_logger(XDG.debug) setup_logger(XDG.debug)
@@ -127,7 +130,7 @@ def main() -> None:
if has_steam_client() is False: if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam) EarlyAlertDialog(init.requires_steam)
# NOTE: clear versions file of unlinked mods # NOTE: clears versions file of unlinked mods
rebuild_symlinks(XDG.config) rebuild_symlinks(XDG.config)
remove_stale_signatures(XDG.config, XDG.version) remove_stale_signatures(XDG.config, XDG.version)
+13 -1
View File
@@ -22,7 +22,6 @@ import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository.Gtk import main_quit # noqa E402 from gi.repository.Gtk import main_quit # noqa E402
# import dzgui.api.servers as Servers
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@@ -160,6 +159,9 @@ class ConfigManager:
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None: def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
columns = tv.get_columns() columns = tv.get_columns()
filters = self.controller.get_all_tree_filters()
filters_file = self.prefs.paths.filters
columns_file = self.prefs.paths.columns columns_file = self.prefs.paths.columns
try: try:
data = read_json(columns_file) data = read_json(columns_file)
@@ -174,6 +176,7 @@ class ConfigManager:
try: try:
write_json(data, columns_file) write_json(data, columns_file)
write_json(filters, filters_file)
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
@@ -217,3 +220,12 @@ class ConfigManager:
h = WINDOW_DEFAULT_Y h = WINDOW_DEFAULT_Y
logger.info(f"Using default window size {w},{h}") logger.info(f"Using default window size {w},{h}")
window.set_default_size(w, h) window.set_default_size(w, h)
def get_filters(self, tv: "ServerTreeView") -> Any | None:
filters = self.prefs.paths.filters
try:
enum = tv.get_enum()
filters = read_json(filters)
return filters[str(enum)]
except Exception:
return None
+4 -1
View File
@@ -20,7 +20,10 @@ logger = logging.getLogger(APP_NAME)
class NoteManager: class NoteManager:
def __init__(self, controller: "Controller", path: "Path") -> None: def __init__(self, controller: "Controller", path: "Path") -> None:
self.notes_path = path self.notes_path = path
self.cache: dict[str, str] = read_json(self.notes_path) try:
self.cache: dict[str, str] = read_json(self.notes_path)
except Exception:
self.cache = {}
def add_note(self, server: str, note: str) -> None: def add_note(self, server: str, note: str) -> None:
self.cache[server] = note self.cache[server] = note
+67
View File
@@ -0,0 +1,67 @@
import logging
import requests
import shutil
import subprocess
import tarfile
from typing import TYPE_CHECKING
from dzgui.const.constants import (
APP_NAME,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
)
from dzgui.strings import dialogs
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
logger = logging.getLogger(APP_NAME)
class UpdateManager:
def __init__(self, controller: "Controller") -> None:
self.thread_man = ThreadingManager(controller)
self.controller = controller
@call_on_thread(dialogs.fetching_update)
def update_version(self, exe_path: str, 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)
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_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()
+18 -12
View File
@@ -73,6 +73,7 @@ class ServerModelManager:
self._dump_history() self._dump_history()
case ServerTab.LAN: case ServerTab.LAN:
# NOTE: LAN tab is only loaded on demand # NOTE: LAN tab is only loaded on demand
self.emitter.emit("lan_page_initialized")
pass pass
case _: case _:
pass pass
@@ -123,8 +124,10 @@ class ServerModelManager:
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data(parsed) self._push_data(parsed)
@call_on_thread(dialog.scanning) @call_on_thread(dialog.scanning, show_cancel=True)
def dump_lan(self, port: int, early_abort: bool) -> None: def dump_lan(self, port: int, early_abort: bool) -> None:
self.first_iteration = True
servers = [] servers = []
ports = range(1, 256) ports = range(1, 256)
failure_func = StoredFunc(self._cleanup_on_failure) failure_func = StoredFunc(self._cleanup_on_failure)
@@ -135,19 +138,20 @@ class ServerModelManager:
for future in as_completed(futures): for future in as_completed(futures):
try: try:
if self.controller.get_exit_event().is_set(): if self.controller.get_exit_event().is_set():
event.set()
return
if self.controller.get_cancel_event().is_set():
event.set()
self.controller.clear_cancel_event()
return return
res = future.result(timeout=LAN_TIMEOUT) res = future.result(timeout=LAN_TIMEOUT)
if res is None: if res is None:
continue continue
servers.append(res)
if early_abort is True: if early_abort is True:
# NOTE: on first non-empty hit, flag pending threads to close # NOTE: on first non-empty hit, flag pending threads to close
event.set() event.set()
servers.append(res) break
self.thread_man.set_cleanup_func(
StoredFunc(self._cleanup_on_success)
)
return
servers.append(res)
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
self.thread_man.set_cleanup_func(failure_func, destroy_first=True) self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
@@ -239,9 +243,11 @@ class ServerModelManager:
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
maps = self._get_new_maps() maps = self._get_new_maps()
filter_man.set_unique_maps(maps) filter_man.set_unique_maps(maps)
store = filter_man.get_map_store()
self.first_iteration = False self.first_iteration = False
self.emitter.emit("servers_loaded_init") self.emitter.emit("load_maps", store)
# self.emitter.emit("servers_loaded_init")
def add_to_history(self, record: dict[str, Any]) -> None: def add_to_history(self, record: dict[str, Any]) -> None:
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
@@ -374,24 +380,24 @@ class ServerModelManager:
# NOTE: maps are set outside of thread because it triggers map changed signals # NOTE: maps are set outside of thread because it triggers map changed signals
maps = self._get_new_maps() maps = self._get_new_maps()
filter_man.set_unique_maps(maps) filter_man.set_unique_maps(maps)
store = filter_man.get_map_store()
self.emitter.emit("load_maps", store)
self.first_iteration = False self.first_iteration = False
self.emitter.emit("servers_loaded_init")
self.emitter.emit("saved_servers_changed") self.emitter.emit("saved_servers_changed")
def _update_maps(self) -> None: def _update_maps(self) -> None:
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
filter_man.set_unique_maps(self._get_new_maps()) filter_man.set_unique_maps(self._get_new_maps())
self.emitter.emit("servers_loaded_init") store = filter_man.get_map_store()
self.emitter.emit("load_maps", store)
self.first_iteration = False self.first_iteration = False
def _cleanup_on_success(self) -> None: def _cleanup_on_success(self) -> None:
proxy = self._get_proxy_man().get_proxy_model() proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
# TODO: servers_loaded vs servers_reloaded
self.emitter.emit("servers_loaded", self.enum) self.emitter.emit("servers_loaded", self.enum)
if self.first_iteration: if self.first_iteration:
self._update_maps() self._update_maps()
+1 -1
View File
@@ -67,4 +67,4 @@ label_client = "Steam client"
### Completion page ### Completion page
heading_completion = "Setup complete" heading_completion = "Setup complete"
blurb_completion = "Configuration completed successfully. Please exit and restart DZGUI to apply changes." blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI."
+4 -6
View File
@@ -2,15 +2,13 @@ import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
def read_json(path: Path) -> Any: def read_json(path: Path) -> Any:
try: try:
with open(path, "r") as infile: with open(path, "r") as infile:
try: data = json.load(infile)
data = json.load(infile) return data
return data except Exception as e:
except json.decoder.JSONDecodeError as e:
raise e
except OSError as e:
raise e raise e
+1 -1
View File
@@ -101,7 +101,7 @@ build_corrupted = (
"Steam settings or DayZ installation may be corrupted. Try restarting Steam." "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
) )
api_warn_msg = """No servers returned. Please wait and try again. api_warn_msg = """No servers returned. Please wait and try again.
If this issue persists, your API key may be defunct or your network is blocking requests. This usually indicates a transitory network timeout. If this issue persists, your API key or network may be malfunctioning.
""" """
server_timeout = "Timed out when querying server, check IP or try again later." server_timeout = "Timed out when querying server, check IP or try again later."
server_error = ( server_error = (
+1 -1
View File
@@ -262,8 +262,8 @@ class Grid(Gtk.Grid):
self.emitter.connect("server_page_toggled", self.toggle_filter_panels) self.emitter.connect("server_page_toggled", self.toggle_filter_panels)
# TODO: drop, use map/signal
def hide_widgets_on_init(self) -> None: def hide_widgets_on_init(self) -> None:
self.conpan.set_lan_visible(False)
self.right_panel.sel_panel.hide() self.right_panel.sel_panel.hide()
def toggle_filter_panels(self, emitter: "Emitter", state: bool) -> None: def toggle_filter_panels(self, emitter: "Emitter", state: bool) -> None:
+12 -5
View File
@@ -94,9 +94,11 @@ class RefreshButton(IconTextButton):
icon=REFRESH_ICON, icon=REFRESH_ICON,
label=atomic_buttons.refresh, label=atomic_buttons.refresh,
) )
self.controller = controller self.controller = controller
emitter = self.controller.get_emitter() emitter = self.controller.get_emitter()
self.loading = False self.loading = False
self.is_clicked = False
self.time = 30 self.time = 30
@@ -110,17 +112,21 @@ class RefreshButton(IconTextButton):
def _on_refresh_clicked(self, button: Self) -> None: def _on_refresh_clicked(self, button: Self) -> None:
"""Spawned in a thread""" """Spawned in a thread"""
self.loading = True self.is_clicked = True
self.controller.refresh_tree() self.controller.refresh_tree()
# TODO: get server tab enum # TODO: get server tab enum
# if LAN tab, reload existing entries in place # if LAN tab, reload existing entries in place
def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None: def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None:
if self.is_clicked is False:
return
if self.loading: if self.loading:
self.set_sensitive(False) return
self.loading = False self.set_sensitive(False)
self.show_time(True) self.loading = True
GLib.timeout_add_seconds(1, self.decrement) self.is_clicked = False
self.show_time(True)
GLib.timeout_add_seconds(1, self.decrement)
def decrement(self) -> bool: def decrement(self) -> bool:
self.time -= 1 self.time -= 1
@@ -128,6 +134,7 @@ class RefreshButton(IconTextButton):
self.time = 30 self.time = 30
self.show_time(False) self.show_time(False)
self.set_sensitive(True) self.set_sensitive(True)
self.loading = False
return False return False
self.show_time(True) self.show_time(True)
return True return True
+2 -4
View File
@@ -278,9 +278,7 @@ class ConnectPanel(Gtk.Box):
for el in self.lan, self.fav, self.add_panel: for el in self.lan, self.fav, self.add_panel:
self.add(el) self.add(el)
self.lan.set_visible(False)
def set_lan_visible(self, state: bool) -> None:
self.lan.set_visible(state)
def _on_lan_tab_toggled(self, emitter: "Emitter", state: bool) -> None: def _on_lan_tab_toggled(self, emitter: "Emitter", state: bool) -> None:
self.set_lan_visible(state) self.lan.set_visible(state)
+6
View File
@@ -68,6 +68,7 @@ class ButtonGrid(Gtk.Grid):
) )
def reload_filters(self) -> None: def reload_filters(self) -> None:
# TODO: may be superfluous
checkboxes = self.checks checkboxes = self.checks
self.block_toggles(True) self.block_toggles(True)
filters = self.controller.get_enabled_filters() filters = self.controller.get_enabled_filters()
@@ -330,3 +331,8 @@ class FilterPanel(Gtk.Box):
self.block_map_change_propagation = False self.block_map_change_propagation = False
return return
self.emitter.emit("map_selection_changed", name) self.emitter.emit("map_selection_changed", name)
def set_filters(self, filters: dict[str, bool]) -> None:
for check in self.button_grid.checks:
label = check.get_label()
check.set_active(filters[label])
+14 -53
View File
@@ -1,9 +1,5 @@
import logging import logging
import os import os
import requests
import shutil
import subprocess
import tarfile
from typing import Literal, TYPE_CHECKING from typing import Literal, TYPE_CHECKING
@@ -13,20 +9,14 @@ from dzgui.const.constants import (
NO_FILL, NO_FILL,
FILL, FILL,
NO_PADDING, NO_PADDING,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
) )
from dzgui.const.update import ALLOW_UPDATES
from dzgui.const.enum import ServerTab from dzgui.const.enum import ServerTab
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.update import UpdateManager
from dzgui.strings import dialogs
from dzgui.util.clip import copy_clipboard from dzgui.util.clip import copy_clipboard
from dzgui.views.components.buttonbox import ButtonBox from dzgui.views.components.buttonbox import ButtonBox
from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.mod_panel import ModSelectionPanel from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi import gi
@@ -45,13 +35,13 @@ class RightPanel(Gtk.Box):
def __init__(self, controller: "Controller"): def __init__(self, controller: "Controller"):
super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL) super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL)
self.thread_man = ThreadingManager(controller)
self.controller = controller self.controller = controller
self.controller.register_widget("right_panel", self) self.controller.register_widget("right_panel", self)
emitter = controller.get_emitter() emitter = controller.get_emitter()
emitter.connect("servers_loaded", self._on_servers_loaded) emitter.connect("servers_loaded", self._on_servers_loaded)
emitter.connect("server_page_changed", self._on_server_page_changed) emitter.connect("server_page_changed", self._on_server_page_changed)
emitter.connect("lan_page_initialized", self._on_lan_page_init)
self.button_vbox = ButtonBox(controller) self.button_vbox = ButtonBox(controller)
self.filters_vbox = FilterPanel(controller) self.filters_vbox = FilterPanel(controller)
@@ -95,54 +85,20 @@ class RightPanel(Gtk.Box):
valign=Gtk.Align.END, valign=Gtk.Align.END,
spacing=10, spacing=10,
) )
if ALLOW_UPDATES and update is not None: exe_path = os.getenv("PYAPP")
if exe_path and update is not None:
self.update_button.set_halign(Gtk.Align.END) self.update_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.update_button) self.gutter_box.add(self.update_button)
self.update_button.connect( self.update_button.connect(
"clicked", self._on_update_button_clicked, update "clicked", self._on_update_button_clicked, exe_path, update
) )
self.gutter_box.add(eb) self.gutter_box.add(eb)
self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING) self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING)
def _on_update_success(self) -> None: def _on_update_button_clicked(
msg = dialogs.update_success self, button: Gtk.Button, exe_path: str, url: str
dialog = QuitDialog(self.controller, msg) ) -> None:
dialog.run() UpdateManager(self.controller).update_version(exe_path, url)
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( def _on_server_page_changed(
self, emitter: "Emitter", page: "ServerTreeView" self, emitter: "Emitter", page: "ServerTreeView"
@@ -151,6 +107,11 @@ class RightPanel(Gtk.Box):
if page.loaded is True: if page.loaded is True:
return return
self.filters_vbox.set_sensitive(False) self.filters_vbox.set_sensitive(False)
# TODO: unless it is lan page
#self.refresh_button.set_sensitive(False)
def _on_lan_page_init(self, emitter: "Emitter") -> None:
self.filters_vbox.set_sensitive(False)
def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None: def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None:
# TODO: similar logic on notebook page change # TODO: similar logic on notebook page change
+2 -2
View File
@@ -111,9 +111,9 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage], context: Union["ServerTab", NotebookPage],
) -> None: ) -> None:
self.spinner.stop() self.spinner.stop()
# TODO: CalcDist is being called when table is not loaded # FIXME: CalcDist is being called when table is not loaded
if dist is None: if dist is None:
self.set_by_context(context, "") return
else: else:
pretty = self.append_distance(dist) pretty = self.append_distance(dist)
self.set_by_context(context, pretty) self.set_by_context(context, pretty)
+2 -2
View File
@@ -11,5 +11,5 @@ class ModsMixin:
self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn
) -> None: ) -> None:
path = self.get_focused_row_path() # type: ignore mod = self.get_value_at_index(2) # type: ignore
self.controller.open_mod_page(path) # type: ignore self.controller.open_workshop_page(mod) # type: ignore
+1 -1
View File
@@ -17,7 +17,7 @@ from dzgui.views.trees.tree_server_mods import ServerModTreeView
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk # type: ignore # noqa E402 from gi.repository import Gdk, Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
+6 -6
View File
@@ -72,6 +72,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
logger.critical(e) logger.critical(e)
valid_json = False valid_json = False
# NOTE: reasonable defaults for long columns
width_map = { width_map = {
"Name": 800, "Name": 800,
"Map": 300, "Map": 300,
@@ -96,8 +97,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
column.set_fixed_width(saved_size) column.set_fixed_width(saved_size)
column.set_expand(True) column.set_expand(True)
else: else:
w = width_map[column_title] if column_title in width_map:
column.set_fixed_width(w) w = width_map[column_title]
column.set_fixed_width(w)
if column_title == "Ping": if column_title == "Ping":
column.set_cell_data_func(renderer, self._get_ping) column.set_cell_data_func(renderer, self._get_ping)
@@ -162,8 +164,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
# TODO: disable filter panel if current model is None # TODO: disable filter panel if current model is None
if self.get_enum() is ServerTab.LAN: state = self.get_enum() is ServerTab.LAN
self.emitter.emit("lan_tab_toggled", True) self.emitter.emit("lan_tab_toggled", state)
store = self.filter_man.get_map_store() store = self.filter_man.get_map_store()
@@ -178,8 +180,6 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
# NOTE: removes queue checker for this tab # NOTE: removes queue checker for this tab
GLib.Source.remove(self.queue_id) GLib.Source.remove(self.queue_id)
self.emitter.disconnect(self.handler_id) self.emitter.disconnect(self.handler_id)
if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", False)
def _on_col_width_changed( def _on_col_width_changed(
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b2" version = "7.0.0b5"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
+14 -2
View File
@@ -9,6 +9,7 @@ root = Path(__file__).resolve().parents[1]
output = root.joinpath("dist") output = root.joinpath("dist")
pyapp_dir = root.joinpath("pyapp-latest") pyapp_dir = root.joinpath("pyapp-latest")
cpython = root.joinpath("build/cpython/airgapped.tar.gz")
builder = build.ProjectBuilder(root) builder = build.ProjectBuilder(root)
wheel = builder.build("wheel", output_directory=output) wheel = builder.build("wheel", output_directory=output)
@@ -26,10 +27,15 @@ env["PYAPP_PROJECT_VERSION"] = version
env["PYAPP_PROJECT_NAME"] = appname env["PYAPP_PROJECT_NAME"] = appname
env["PYAPP_EXEC_SPEC"] = entrypoint env["PYAPP_EXEC_SPEC"] = entrypoint
env["PYAPP_PROJECT_PATH"] = wheel env["PYAPP_PROJECT_PATH"] = wheel
env["PYAPP_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_PYTHON_VERSION"] = "3.13"
env["PYAPP_PASS_LOCATION"] = "true" env["PYAPP_PASS_LOCATION"] = "true"
# NOTE: explicitly install all dependencies into distribution
env["PYAPP_SKIP_INSTALL"] = "true"
env["PYAPP_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_FULL_ISOLATION"] = "true"
env["PYAPP_DISTRIBUTION_PATH"] = str(cpython)
env["PYAPP_DISTRIBUTION_PYTHON_PATH"] = "python/bin/python3"
proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir) proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir)
if proc.returncode == 0: if proc.returncode == 0:
output_exe = root.joinpath("pyapp-latest/target/release/pyapp") output_exe = root.joinpath("pyapp-latest/target/release/pyapp")
@@ -39,3 +45,9 @@ if proc.returncode == 0:
with tarfile.open(tarpath, "w:gz") as tar: with tarfile.open(tarpath, "w:gz") as tar:
tar.add(release_exe, arcname=appname) tar.add(release_exe, arcname=appname)
print(f"Wrote tarfile to '{tarpath}'") print(f"Wrote tarfile to '{tarpath}'")
proc = subprocess.run([release_exe, "-v"], capture_output=True, text=True)
assert proc.stdout.rstrip() == version
release_exe.unlink()
Path(wheel).unlink()
+14
View File
@@ -0,0 +1,14 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"Maximum": 1,
"Perspective": 1
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"cols": {
"Name": 398,
"Map": 181,
"Gametime": 151,
"Players": 119,
"Queue": 96,
"IP": 247,
"Qport": 94,
"Ping": 253
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"View": 1
}
}
+33
View File
@@ -0,0 +1,33 @@
import pytest
from dzgui.init.migrate import convert_cols_file
from tests.fixtures import fixture_path
@pytest.fixture
def columns_with_perspective():
return fixture_path("columns_1")
@pytest.fixture
def columns_without_perspective():
return fixture_path("columns_2")
@pytest.fixture
def columns_with_view():
return fixture_path("columns_3")
@pytest.mark.config
def test_columns_with_perspective(columns_with_perspective):
j = convert_cols_file(columns_with_perspective)
assert "View" in j["cols"]
assert "Max" in j["cols"]
@pytest.mark.config
def test_columns_without_perspective(columns_without_perspective):
j = convert_cols_file(columns_without_perspective)
assert "View" not in j["cols"]
assert "Max" not in j["cols"]
@pytest.mark.config
def test_columns_with_view(columns_with_view):
j = convert_cols_file(columns_with_view)
assert j is None
+3 -3
View File
@@ -2,14 +2,14 @@ from dzgui.util import localize
def test_de(monkeypatch): def test_de(monkeypatch):
monkeypatch.setenv("LC_CTYPE", "de_DE.UTF-8") monkeypatch.setenv("LC_ALL", "de_DE.UTF-8")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9.900" assert localize.number(9900) == "9.900"
assert localize.number(99.88) == "99,88" assert localize.number(99.88) == "99,88"
def test_en(monkeypatch): def test_en(monkeypatch):
monkeypatch.setenv("LC_CTYPE", "en_US.UTF-8") monkeypatch.setenv("LC_ALL", "en_US.UTF-8")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9,900" assert localize.number(9900) == "9,900"
assert localize.number(99.88) == "99.88" assert localize.number(99.88) == "99.88"
@@ -22,7 +22,7 @@ def test_none(monkeypatch):
def test_empty(monkeypatch): def test_empty(monkeypatch):
monkeypatch.setenv("LC_CTYPE", "") monkeypatch.setenv("LC_ALL", "")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9,900" assert localize.number(9900) == "9,900"
assert localize.number(99.88) == "99.88" assert localize.number(99.88) == "99.88"