Compare commits

...
Author SHA1 Message Date
aclist e935276e32 Merge pull request #542 from aclist/chore/test-typehints
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: typehint tests
2026-11-12 07:47:03 +09:00
aclist 047b9dbbf6 Merge branch 'chore/test-typehints' of github.com:aclist/dzgui into chore/test-typehints 2026-11-12 07:45:36 +09:00
aclist d5c9f24ad5 chore: clear typehinting errors 2026-11-12 07:45:24 +09:00
aclist f486cc506f chore: clear typehinting errors 2026-11-12 07:44:34 +09:00
aclist 307792ba5b chore: drop unused import 2026-11-12 07:11:20 +09:00
aclist f7d95d5407 chore: clear typehinting errors 2026-11-12 07:10:27 +09:00
aclist 8b09622fe5 Merge branch 'dzgui7' into chore/test-typehints 2026-11-12 06:21:45 +09:00
aclist 7f6d61083c Merge pull request #541 from aclist/docs/7.0.0-spec
docs: update to DZGUI 7 spec
2026-11-12 06:20:56 +09:00
aclist 3e7b49203a chore: add mark to test 2026-11-12 06:20:16 +09:00
aclist 269ef18a48 Merge pull request #540 from aclist/drop/unused-imports-2
drop: unused imports-2
2026-11-12 06:18:37 +09:00
aclist 8aaa806cd3 chore: drop unused docs pages
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-12 06:11:48 +09:00
aclist e4dc545038 fix: URLs 2026-11-12 06:10:00 +09:00
aclist 35623b6456 change: use ServerRow classmethod
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-12 06:06:27 +09:00
aclist b9cb7ec973 chore: update repo URL 2026-11-12 06:03:40 +09:00
aclist e29433b388 chore: typehint tests 2026-11-12 05:10:02 +09:00
aclist 170990ee1d chore: downgrade FIXME tags 2026-11-12 05:06:51 +09:00
aclist 1dbbd5977d chore: drop Optional annotation 2026-11-12 05:05:59 +09:00
aclist c267b1bbc7 docs: update changelog
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-12 02:12:40 +09:00
aclist 439109d11d docs: update to DZGUI 7 spec 2026-11-12 02:07:49 +09:00
aclist d0b3a723c6 drop: unused comment 2026-11-12 02:06:36 +09:00
aclist 535bc505f7 chore: update comments 2026-11-12 02:06:20 +09:00
aclist f2375feeec feat: add get_index() method to ServerRow 2026-11-12 02:04:55 +09:00
aclist 1b9448735d chore: drop old comments 2026-11-12 02:03:59 +09:00
aclist d5693b07fb chore: update comments 2026-11-12 02:03:31 +09:00
aclist 7a6f4a2bb9 feat: immutable decorator 2026-11-11 07:52:05 +09:00
aclist ba92d15085 drop: unused signal logic 2026-11-11 07:50:54 +09:00
aclist 65e04c2fc4 fix: multiple signals being stacked 2026-11-11 07:50:38 +09:00
aclist 0907f5a474 change: immutable dataclass 2026-11-11 07:50:12 +09:00
aclist bfa83c3f94 change: import sort order 2026-11-11 07:15:53 +09:00
aclist aef48e515b fix: failing test 2026-11-11 07:15:28 +09:00
aclist e6ebeaeb5e chore: immutable dataclass 2026-11-11 07:04:25 +09:00
aclist 88c740105a chore: drop unused dataclasses 2026-11-11 07:02:23 +09:00
aclist b98a6f95a6 drop: unused imports 2026-11-11 07:00:17 +09:00
aclist 87d76e83ba chore: update DEVELOPERS.md 2026-11-11 06:57:56 +09:00
aclist a5374d1f1a Merge pull request #526 from aclist/fix/legacy-version-file
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
fix: legacy version file
2026-11-11 05:08:14 +09:00
aclist dcc4cb494b Merge branch 'dzgui7' into fix/legacy-version-file
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
2026-11-11 05:07:35 +09:00
aclist ff1e150eb1 change: suppress logger message 2026-11-11 05:04:50 +09:00
aclist c7ef91f02e Merge pull request #539 from aclist/fix/remove-from-history
fix: restore missing route
2026-11-11 05:00:34 +09:00
aclist 95bc05eab5 Merge pull request #536 from aclist/change/server-row-dataclass
change: marshal server row data
2026-11-11 04:59:58 +09:00
aclist 89dc6aa8a8 chore: drop comment
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
2026-11-11 04:52:50 +09:00
aclist 9e8e3730c7 fix: drop unused imports 2026-11-11 04:30:31 +09:00
aclist e5c8f2a69c chore: drop stale comment 2026-11-11 04:11:23 +09:00
aclist 63316a01a9 fix: extraneous newlines in log 2026-11-11 03:57:51 +09:00
aclist 0729273503 change: drop Union 2026-11-11 03:51:09 +09:00
aclist 91d164d666 chore: drop unused method 2026-11-11 03:49:43 +09:00
aclist 0d7f022c43 Merge branch 'dzgui7' into change/server-row-dataclass 2026-11-11 03:49:05 +09:00
aclist 9b948271e1 chore: add test 2026-11-11 00:57:12 +09:00
aclist 7322970c54 fix: define constants outside of FiltrationManager 2026-11-11 00:56:51 +09:00
aclist 504a2a724c fix: typo 2026-11-11 00:48:37 +09:00
aclist c66511b005 change: marshal server row data 2026-11-11 00:41:40 +09:00
aclist 3677b01fdc fix: exception handling 2026-11-09 22:25:13 +09:00
aclist 4f6223f90b fix: strip invalid values from versions file 2026-11-09 22:24:26 +09:00
aclist 441066a1a0 chore: update comments 2026-11-09 22:23:37 +09:00
aclist fa1ac8fa31 chore: update imports 2026-11-09 22:23:27 +09:00
85 changed files with 778 additions and 758 deletions

No files matched your search

+3 -7
View File
@@ -57,6 +57,7 @@
- List timestamp of last DayZ client update - List timestamp of last DayZ client update
- Validate mods via Mod page to pull in updates - Validate mods via Mod page to pull in updates
- Support for Nasdara (Badlands) DLC - Support for Nasdara (Badlands) DLC
- Load offline mods
### Changed ### Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
@@ -99,16 +100,11 @@
- Current keyword filter not being applied when refreshing table - Current keyword filter not being applied when refreshing table
- Steam Flatpak cannot run DZGUI as a non-Steam game - Steam Flatpak cannot run DZGUI as a non-Steam game
- Collisions between native Steam and Steam Flatpak clients - Collisions between native Steam and Steam Flatpak clients
- Sticky toggle signal on radio buttons when reloading Options
### Unreleased
- Load offline mods
- Local documentation
- Raw debug command in context menu
### Developer-facing ### Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page - Options -> Dev page
- Raw debug command in preconnect menu
## [6.0.5] 2026-05-17 ## [6.0.5] 2026-05-17
### Changed ### Changed
+2 -3
View File
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
# Requesting help # Requesting help
If you encounter a problem with DZGUI, you can submit tickets on the GitHub If you encounter a problem with DZGUI, you can submit tickets on the GitHub
[issue tracker](https://github.com/aclist/dztui/issues) under the [issue tracker](https://github.com/aclist/dzgui/issues) under the
`troubleshooting` tag. `troubleshooting` tag.
# How can I help the project? # How can I help the project?
@@ -24,7 +24,7 @@ accordingly.
## Submitting a ticket ## Submitting a ticket
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues). Navigate to the GitHub [issue tracker](https://github.com/aclist/dzgui/issues).
From there, follow the onscreen prompts. You will be asked questions such as: From there, follow the onscreen prompts. You will be asked questions such as:
- What version are you using? - What version are you using?
@@ -55,7 +55,6 @@ The following naming conventions apply for PRs:
- fix/<your-fix> - patch/hotfix branches - fix/<your-fix> - patch/hotfix branches
- feat/<your-feature> - feature branches - feat/<your-feature> - feature branches
- doc/<your-doc-branch> - documentation branches - doc/<your-doc-branch> - documentation branches
- infra/<your-infra-branch> - infrastructure branches
Implement your changes and test them locally. If they work, you may Implement your changes and test them locally. If they work, you may
open a merge request, and then we review your changes. If everything is OK, it open a merge request, and then we review your changes. If everything is OK, it
+6 -5
View File
@@ -5,7 +5,7 @@
### Fetch submodules ### Fetch submodules
git submodule update --recursive --init git submodule update --recursive --init
### Install with dev dependencies ### Install into editable venv with dev dependencies
uv pip install -e '.[dev]' uv pip install -e '.[dev]'
This includes all dependencies for building, linting, testing, and checking in code. This includes all dependencies for building, linting, testing, and checking in code.
@@ -16,7 +16,7 @@ docs: documentation tools
lint: code linting, fixing, and type checking lint: code linting, fixing, and type checking
test: unit and integration tests test: unit and integration tests
## Miscellaneous ## Build process
### Setting up precommit hooks ### Setting up precommit hooks
pre-commit install pre-commit install
@@ -25,6 +25,7 @@ pre-commit install
sphinx-build -M html source build -a sphinx-build -M html source build -a
### Building standalone release binaries ### Building standalone release binaries
1. Set up the build target: `rustup target add x86_64-unknown-linux-musl` 1. Install the applicable system package containing the 'musl-gcc' wrapper
2. Run `python3 scripts/release.py` 2. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
3. Generate a release and tag against the resulting tarfile generated in `dist` 3. Run `python3 scripts/release.py`
4. Generate a release and tag against the resulting tarfile generated in `dist`
+7 -8
View File
@@ -1,11 +1,11 @@
Submitting a ticket Submitting a ticket
========================== ==========================
Tickets are tracked at the `DZGUI issue tracker <https://github.com/aclist/dztui/issues>`_. Tickets are tracked at the `DZGUI issue tracker <https://github.com/aclist/dzgui/issues>`_.
You will need to `register for a GitHub account <https://github.com/signup>`_ in order to submit one. You will need to `register for a GitHub account <https://github.com/signup>`_ in order to submit one.
If have checked the :doc:`kb` and `closed issues <https://github.com/aclist/dztui/issues?q=is%3Aissue+is%3Aclosed>`_ area and did not find a solution, three ticket types are provided for your If have checked the :doc:`kb` and `closed issues <https://github.com/aclist/dzgui/issues?q=is%3Aissue+is%3Aclosed>`_ area and did not find a solution, three ticket types are provided for your
convenience: convenience:
- **Bug report**: for unexpected, breaking behavior in the application - **Bug report**: for unexpected, breaking behavior in the application
@@ -20,18 +20,17 @@ Below are some examples of how to format a constructive report that will ensure
A non-actionable report A non-actionable report
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Subject: add button Subject: BUG: add button
hi, you must add a button to show how long I have to wait to join a full server, the app is useless without this...do your job, guys!!! ;-) hi, you must add a button to show how long I have to wait to join a full server, the app is useless without this...do your job, guys!!! ;-)
This report exhibits a number of problems: This report exhibits a number of problems:
- Vague title and summary: in addition to being unclear, this will slow down filing the incoming report.
- Problem space is poorly defined: it is not clear where the issue lies, where the feature is supposed to be implemented, nor why.
- Lack of context: the feature may already exist, but there is not enough information to go on. Alternatively, the feature may be technically impossible to implement, but there is not enough
information to go on.
- Incorrect report type: this is a feature request filed as a bug report. - Incorrect report type: this is a feature request filed as a bug report.
- Vague title and summary: this will slow down filing the incoming report.
- Problem space is poorly defined: it is not clear where the issue lies, where the feature is supposed to be implemented, nor why.
- Lack of context: the feature may already exist, but there is not enough information to go on. Alternatively, the feature may be technically impossible to implement.
- Unprofessional tone: the message is disrespectful towards project members and inserts the user's subjective opinion. - Unprofessional tone: the message is disrespectful towards project members and inserts the user's subjective opinion.
An actionable report An actionable report
@@ -56,5 +55,5 @@ This report is actionable because it is objective, specific, and detailed:
- The subject is clearly defined: specific details ensure it will be filed in a timely manner. - The subject is clearly defined: specific details ensure it will be filed in a timely manner.
- Scope is explicit: the specific version, settings, and conditions under which the problem occur were given. - Scope is explicit: the specific version, settings, and conditions under which the problem occur were given.
- Ample context: the user gave steps to reproduce the problem and clarified each one. - Ample context: the user gave steps to reproduce the problem and clarified each one.
- Courteous tone: the user interacted with project members in a professional manner.
- Supporting information: the user provided a crash dump to expedite resolution of the issue. - Supporting information: the user provided a crash dump to expedite resolution of the issue.
- Courteous tone: the user interacted with project members in a professional manner.
+2 -2
View File
@@ -36,5 +36,5 @@ html_show_copyright = False
html_show_sourcelink = False html_show_sourcelink = False
today_fmt = "%Y-%m-%d" today_fmt = "%Y-%m-%d"
release = "6.x.x - 7.x.x" release = "7.x.x"
version = "6.x.x - 7.x.x" version = "7.x.x"
+5 -10
View File
@@ -1,19 +1,14 @@
Developers Developers
=========== ===========
This project is open source under the GPLv3 license. You can find the source code on This project is open source under the GPLv3 license. You can find the source code on
`GitHub <https://github.com/aclist/dztui>`_. If you wish to contribute to `GitHub <https://github.com/aclist/dzgui>`_. If you wish to contribute to
development, you will find further help in the development, you will find further help in the
`CONTRIBUTING.md <https://github.com/aclist/dztui/blob/main/CONTRIBUTING.md>`_ `CONTRIBUTING.md <https://github.com/aclist/dzgui/blob/dzgui7/CONTRIBUTING.md>`_
file. file.
All contributors are expected to act in accordance with the project's All contributors are expected to act in accordance with the project's
`code of conduct <https://github.com/aclist/dztui/blob/main/CODE_OF_CONDUCT.md>`_, `code of conduct <https://github.com/aclist/dzgui/blob/dzgui7/CODE_OF_CONDUCT.md>`_,
and follow the project's standards and guidelines. and follow the project's standards and guidelines.
To install DZGUI and related dependencies for linting, testing, and version control, Detailed instructions on setting up a build environment and installing DZGUI from source code
invoke the command ``uv pip install .[dev]``. can be found in the `DEVELOPERS.md <https://github.com/aclist/dzgui/blob/dzgui7/DEVELOPERS.md>`_ file.
After this process, install the applicable pre-commit hooks by invoking the command ``pre-commit install``.
These pre-commit hooks enforce code checks that prevent making commits unless the relevant tests pass.
-110
View File
@@ -1,110 +0,0 @@
Installation (DZGUI 6 legacy)
==================
Basic runtime dependencies
---------------------------------
If not already installed, the below can be found in your system’s package manager.
If any dependencies are missing when the application starts, it will warn you, so you need not take any preemptive measures here.
- curl
- jq
- steam
- wmctrl or xdotool
- zenity
.. note::
All dependencies are installed out of the box on Steam Deck.
UI dependencies (Python and PyGObject)
---------------------------------------
.. important::
As of version 6.0.0, you must have a valid Python 3.13 and matching PyGObject environment.
Depending on your distribution, this may require additional manual intervention.
Arch Linux (vanilla variants)
^^^^^^^^^^^^^^^^^^^^^^
You will need the ``python3.13`` package, if it is not already installed.
Install the following build dependencies:
.. code:: console
sudo pacman -S cmake gcc pkgconf python-cairo
.. include:: venv.rst
CachyOS (Arch)
^^^^^^^^^^^^^^^^^^^^^^^
Refer to the build instructions for Arch Linux first.
The ``python3.13`` package is not available on CachyOS, so it is recommended to
use ``uv`` to manage Python versions.
.. code:: console
sudo pacman -S uv
.. include:: venv.rst
Manjaro 26 (Arch)
^^^^^^^^^^^^^^^^^^^^^^^
If Python 3.13 is not already available out of the box, refer to the build instructions for :ref:`Arch Linux (vanilla variants)`.
Debian 13 variants
^^^^^^^^^^^^^^^^^^^^^^^
Python 3.13 and PyGObject are available out of the box. No additional steps are necessary.
You can proceed to the :ref:`Installscript`.
Fedora 43 variants
^^^^^^^^^^^^^^^^^^^^^^^
Fedora 43 ships with a newer version of Python (3.14), so you must set up Python 3.13 in its own
virtual environment (venv) to prevent it from conflicting with the system package.
Install the following build dependencies:
.. code:: console
sudo dnf install cairo cairo-devel cairo-gobject-devel gcc python3.13 python3.13-devel
.. include:: venv.rst
Ubuntu 24 variants
^^^^^^^^^^^^^^^^^^^^^^
Ubuntu ships with an older version of Python (3.12). You must install additional dependencies.
Add the PPA below to add Python 3.13 to your sources:
.. code:: console
sudo add-apt-repository ppa:deadsnakes/ppa
Install the following build dependencies:
.. code:: console
sudo apt install python3.13 python3.13-venv python3.13-dev build-essential python3-gi-cairo pkg-config libgirepository-2.0-dev libcairo2-dev
.. include:: venv.rst
Installscript
-----------------------
Invoke the command below from a terminal. This script fetches the main DZGUI script from the repository,
makes it executable, and launches DZGUI's first-time setup. After running this installscript, you will
have the file ``dzgui.sh`` wherever you invoked the command.
.. code:: console
curl -s "https://codeberg.org/aclist/dztui/raw/branch/dzgui/install.sh" | bash
nixOS installation
----------------------
Two packages are independently maintained by other contributors. Each takes a slightly different approach.
nixOS packages are standalone and do not require cloning the git repository.
- https://github.com/lelgenio/dzgui-nix
- https://github.com/jiriks74/dzgui.flake
Next steps
---------------
Proceed to the :doc:`setup` section for setup instructions.
-33
View File
@@ -1,33 +0,0 @@
Installation (DZGUI 7 beta)
==================
In order to make the first-time setup experience easier for end-users, DZGUI 7 no longer requires
the tool to be installed from source code into a working Python environment. All necessary dependencies and the
runtime environment are built-in.
Turnkey installer
------------------------
- Visit the project's `Releases page <https://github.com/aclist/dztui/releases/latest>`_.
- Extract the DZGUI tarball from the top of the **Assets** list.
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
a file explorer.
- Once the setup wizard opens, follow the steps at the bottom of this page.
UI dependencies (Python and PyGObject)
---------------------------------------
On most major distributions, GObject introspection libraries should be available by default.
If you receive an error about a missing ``libgirepository-2.0`` dependency, install it via your system's package manager.
On Debian-based distributions (Ubuntu, etc.), this package is not explicitly installed on the system.
You can install it via the following set of commands:
.. code:: console
sudo apt update
sudo apt install libgirepository-2.0-0
Next steps
---------------
Proceed to the :doc:`setup` section for setup instructions.
+12
View File
@@ -0,0 +1,12 @@
Command line launch flags
==========================
When run from the terminal, DZGUI provides several flags:
``-d, --debug``: enable developer debugging features. This does not add hidden or extra functionality; it merely enables developers to track deprecation warnings and see values stored in memory.
End-users will gain no benefit from using this flag.
``-m, --map``: check and update system map count value. Unnecessary on most modern systems. If your system does not meet the threshold, the DZGUI setup wizard will prompt you to use this flag.
``-u, --uninstall``: uninstall: granular selection of which state/configuration files to remove.
``-v, --version``: displays the DZGUI version.
+3 -3
View File
@@ -15,8 +15,8 @@ A DayZ server browser/mod manager for Linux. Used to list official and community
Source code: Source code:
- `GitHub <https://www.github.com/aclist/dztui>`_ - `GitHub <https://www.github.com/aclist/dzgui>`_
- `Codeberg (mirror) <https://codeberg.org/aclist/dztui>`_ - `Codeberg (mirror) <https://codeberg.org/aclist/dzgui>`_
Installation Installation
------------- -------------
@@ -56,8 +56,8 @@ See: :doc:`kb` or :ref:`Submitting a ticket`.
:maxdepth: 3 :maxdepth: 3
:hidden: :hidden:
steam
locale locale
flags
.. toctree:: .. toctree::
:caption: Help :caption: Help
+25 -16
View File
@@ -1,28 +1,37 @@
Installation Installation
================== ==================
As of this writing, you have a choice of two DZGUI versions: .. note::
Version 6 of DZGUI 6 has been fully replaced with DZGUI 7.
Users on existing DZGUI 6 installations should migrate to the DZGUI 7 build.
- DZGUI 6: Legacy stable version, more complicated to install In order to make the first-time setup experience easier for end-users, DZGUI 7 no longer requires
- DZGUI 7: Unstable beta version, much easier to install the tool to be installed from source code into a working Python environment. All necessary dependencies and the
runtime environment are built-in.
DZGUI 6 Turnkey installer
-------------------- ------------------------
This version is no longer receiving feature updates, but will receive security updates until it is replaced by DZGUI 7.
DZGUI 6 must be installed from the source code and has many more package dependencies and prerequisites for it to function correctly. - Visit the project's `Releases page <https://github.com/aclist/dzgui/releases/latest>`_.
If you are looking for a one-click, self-contained installer, use :ref:`DZGUI 7` instead. - Download the DZGUI archive from the top of the **Assets** list. This file will be labeled ``dzgui-<VERSION>-x86_64-unknown-linux-musl.tar.gz``.
- Extract the archive's contents.
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in a file explorer.
- Once the setup wizard opens, follow the steps at the bottom of this page.
To install **DZGUI 6**, visit :doc:`dzgui6`. UI dependencies (Python and PyGObject)
---------------------------------------
On most major distributions, GObject introspection libraries should be available by default.
If you receive an error about a missing ``libgirepository-2.0`` dependency, install it via your system's package manager.
DZGUI 7 On Debian-based distributions (Ubuntu, etc.), this package is not explicitly installed on the system.
-------------------- You can install it via the following set of commands:
This version is currently in beta testing and has all of the features found in DZGUI 6, but may be unstable.
DZGUI 7 has an all-in-one installer and setup wizard, and many new features not found in DZGUI 6. .. code:: console
However, this codebase is newer and is provided as a pre-release beta candidate.
If you want the simplest installer and don't mind experiencing the occasional bug, use this version. sudo apt update
sudo apt install libgirepository-2.0-0
To install **DZGUI 7**, visit :doc:`dzgui7`. Next steps
---------------
Proceed to the :doc:`setup` section for setup instructions.
+15 -2
View File
@@ -98,15 +98,28 @@ tend to have less headroom for simultaneous connections than their wired counter
DZG-009: Floating dialogs appear maximized on tiling window managers DZG-009: Floating dialogs appear maximized on tiling window managers
------------------------------------------------------------------- -------------------------------------------------------------------
The main DZGUI window and its child dialogs are expected to be rendered as floating by your window manager. The main DZGUI window and its child dialogs are expected to be rendered as floating by your window manager.
DZGUI sends window manager hints to this effect, but tiling window managers designed to bisect the screen into quadrants (e.g., i3 window manager) DZGUI sends window manager hints to this effect, but tiling window managers designed to bisect the screen into quadrants (e.g., i3 window manager)
may try to always launch applications in fullscreen. may try to always launch applications in fullscreen.
To resolve this, set specific exclusions or window hints in your WM's configuration file. To resolve this, set specific exclusions or window hints in your WM's configuration file.
For example, for i3, add the following to your `XDG_CONFIG_HOME/i3/config` file (defaults to `$HOME/.config/i3/config`): For example, for i3, add the following to your ``XDG_CONFIG_HOME/i3/config`` file (defaults to ``$HOME/.config/i3/config``):
.. code:: console .. code:: console
for_window [instance="DZGUI"] floating enable, move position center for_window [instance="DZGUI"] floating enable, move position center
for_window [instance="DZGUI - Dialog"] floating enable, move position center for_window [instance="DZGUI - Dialog"] floating enable, move position center
DZG-010: Maps combobox extends to full height of window when there are many maps to choose from
---------------------------------------------------------------------------------------------------
This is a limitation of GTK 3. If and when DZGUI is refactored to support the GTK 4 API, this visual glitch is expected to be resolved.
DZG-011: GTK reports "Failed to load module 'xapp-gtk3-module' when starting DZGUI
---------------------------------------------------------------------------------------------------
This is a purely cosmetic message that appears when Cinnamon (Xapp) desktop integations are not present.
While you can suppress this message by installing the package ``xapp``, if you are not actually using Cinnamon as
part of your desktop environment, it will pull in a large number of unnecessary dependencies.
DZG-012: After adding the DZGUI Steam shortcut via the installer, shortcut icons do not appear in the small library list
-------------------------------------------------------------------------------------------------------------------------
It can take 1-2 restarts of the Steam client for these icons to synchronize.
+14 -5
View File
@@ -19,14 +19,14 @@ You will need at a minimum a :ref:`Steam Web API key<steam_api>` in order to bro
Steam Web API key Steam Web API key
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
**This step is required**. **This step is required**.
Register for a `Steam Web API key <https://steamcommunity.com/dev/apikey>`_ (free) using your Steam account. You will be asked for a unique URL for your app when registering. Register for a `Steam Web API key <https://steamcommunity.com/dev/apikey>`_ (free) using your Steam account.
Since this key is for a personal use application and does not actually call back anywhere, set a generic local identifier here like "127.0.0.1" or some other name that is meaningful to you. You will be asked for a unique URL for your app when registering. Typically, an API would send logs back to your URL in the form of callbacks.
However, since these API queries will be used for local consumption by a single user only, you can set a generic local identifier here like ``127.0.0.1`` to have the server discard them.
Once configured, you can insert this key in the app when launching it for the first time. Once configured, you can insert this API key in the app when launching it for the first time.
.. note:: .. note::
If you are confused about this requirement, please refer to DZGUI Knowledge Base article :ref:`DZG-007<DZG-007>` for additional information. If you are confused about this requirement, please refer to DZGUI Knowledge Base article :ref:`DZG-007<DZG-007>` for additional information.
@@ -36,7 +36,16 @@ Battlemetrics API key
.. warning:: .. warning::
This feature is no longer supported in DZGUI. Battlemetrics has switched to a paid service. This feature is no longer supported in DZGUI. Battlemetrics has switched to a paid service.
Battlemetrics queries are expected to fail in DZGUI 6. In DZGUI 7, this feature has been removed entirely.
Choosing a Steam client
^^^^^^^^^^^^^^^^^^^^^^^^^
If multiple Steam clients are available (native Steam client/Flatpak Steam client), you will be
asked to choose which you want to use. Due to the way Steam's inter-process communication and internal URIs work,
if you choose one or the other, you should not run both clients simultaneously. This can lead DZGUI to send requests to the wrong one.
.. note::
If you decide to switch to using a different Steam client with DZGUI, please uninstall fully (``dzgui -u``) and re-run the first time setup.
Next steps Next steps
-------------- --------------
-94
View File
@@ -1,94 +0,0 @@
Steam integration
====================
.. note::
In :doc:`DZGUI 7 <dzgui7>`, Steam shortcut creation and cover art setup are handled automatically during setup. You can skip this step unless you are using DZGUI 6.
DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments.
First, launch Steam in the **Large** (default) view.
.. attention::
Steam Deck users: Gaming Mode has limited support for configuration of custom games. You will need to switch to **Desktop Mode** in order to add a non-Steam game.
.. figure:: images/01.png
:scale: 70%
:alt: Adding a non-Steam game
Select **Add a Game** > **Add a Non-Steam Game** from the bottom left corner of the Steam client.
Navigate to ``$HOME/.local/share/applications`` and select ``dzgui.desktop``.
Select **Add Selected Programs** to add a shortcut to DZGUI.
Artwork
^^^^^^^^^^
DZGUI ships with custom artwork that provides various cover/splash images to allow it to integrate with the rest of your games library.
.. figure:: images/02.png
:scale: 70%
:alt: Adding a non-Steam game
From the main library view, navigate to the app’s details page and right-click the blank image header at the top.
.. figure:: images/03.png
:scale: 70%
:alt: Adding a non-Steam game
Select **Set Custom Background**
Select to display **All Files** from the **File type** dropdown
Navigate to ``$HOME/.local/share/dzgui`` and select ``hero.png``.
.. figure:: images/04.png
:scale: 70%
:alt: Adding a non-Steam game
Next, right-click the image background and select **Set Custom Logo**.
Navigate to ``$HOME/.local/share/dzgui`` and select ``logo.png``.
.. figure:: images/05.png
:scale: 70%
:alt: Adding a non-Steam game
Notice that this removes the redundant app name that occluded the image, as seen above.
.. figure:: images/06.png
:scale: 70%
:alt: Adding a non-Steam game
Next, navigate to your Library index (looks like a bookshelf of cover art) and find the DZGUI app.
Right-click its cover and select **Manage** > **Set custom artwork**.
Navigate to ``$HOME/.local/share/dzgui`` and select ``grid.png``.
.. figure:: images/07.png
:scale: 70%
:alt: Adding a non-Steam game
The final result will create box art looking like the above.
.. figure:: images/08.png
:scale: 100%
:alt: Adding a non-Steam game
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
Steam shows a collection of box art based on categories like "Play Next", "Recent Games", etc. Look for a **downward-pointing caret icon** and click it, then select the **Recent Games** category.
.. figure:: images/09.png
:scale: 100%
:alt: Adding a non-Steam game
If DZGUI was the last item played, it will be shown with a landscape, rather than portrait, orientation cover, which is initially blank.
To customize this cover, right click it and select **Manage** > **Set custom artwork**, navigate to ``$HOME/.local/share/dzgui``, then select ``hero.png``.
+3 -7
View File
@@ -1,12 +1,8 @@
Uninstallation Uninstallation
================== ==================
To uninstall DZGUI and its associated files, invoke ``./dzgui`` from the terminal with the ``-u`` or ``--uninstall`` flag.
To uninstall DZGUI and its associated files, invoke ``./dzgui.sh`` with the ``-u`` or ``--uninstall`` flag. You will be prompted to select atomic files you wish to remove.
Follow the prompts to choose from:
- **Full uninstall** -- removes the application and all user-defined configuration files
- **Partial uninstall** -- removes the application, but keeps user-defined configuration files
.. tip:: .. tip::
If you intend to reinstall DZGUI again, keeping your configuration files is recommended. If you intend to reinstall DZGUI again, keeping your configuration files at a minimum is recommended.
+12 -8
View File
@@ -1,25 +1,29 @@
Usage Usage
========= =========
It is always advised to have Steam running in the background. DZGUI is meant to run "on top of" Steam. It is always advised to have Steam running in the background. DZGUI is meant to run "on top of" Steam.
DZGUI can be launched in one of three ways. DZGUI can be launched in one of three ways.
From a terminal From a terminal
------------------ ------------------
Invoke the command ``./dzgui`` from wherever you downloaded DZGUI to.
Invoke the command ``./dzgui.sh`` from wherever you downloaded the DZGUI script to.
Launching from a terminal may give more verbose information in the event of a crash, and can be a good way of troubleshooting problems. Launching from a terminal may give more verbose information in the event of a crash, and can be a good way of troubleshooting problems.
From the shortcut shipped with the application From the shortcut shipped with the application
-------------------------------------------------- --------------------------------------------------
When completing the first-time setup, you will be asked if you want to create additional system shortcuts.
If applicable for yourDesktop Environment, these shortcuts can be created.
On most distributions, if you are using a desktop environment based on the Freedesktop specification, shortcuts will be installed for you. - One shortcut will be located under the ``Games`` category of your system’s applications list/start menu.
- The other is accessed via the ``DZGUI`` shortcut on the desktop
- One shortcut is located under the ``Games`` category of your system’s applications list/start menu.
- The other is accessed via the ``DZGUI`` desktop icon (Steam Deck only)
From Steam From Steam
-------------- --------------
See :doc:`steam`. DZGUI will also ask you if you want to add a "non-Steam game" shortcut to Steam.
This shortcut will launch DZGUI directly and comes with integrated grid and cover artwork.
You must restart Steam for this shortcut to take effect in your library.
.. note::
You can also pass DZGUI launch parameter's to the Steam shortcut via Steam's right-click menu. See :doc:`flags`.
+6 -6
View File
@@ -2,10 +2,9 @@ import hashlib
import logging import logging
import re import re
from concurrent.futures import wait from concurrent.futures import ThreadPoolExecutor, wait
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any
from dzgui.api.steam import get_app_path from dzgui.api.steam import get_app_path
from dzgui.api.servers import get_rules, fqip_to_record from dzgui.api.servers import get_rules, fqip_to_record
@@ -18,8 +17,7 @@ from dzgui.const.constants import (
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.init.migrate import clean_legacy_versions_file
from typing import Any
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -117,10 +115,12 @@ def remove_stale_signatures(
config: Path, versions: Path, ids: list[int] | None = None config: Path, versions: Path, ids: list[int] | None = None
) -> None: ) -> None:
if versions.is_file() is False: if versions.is_file() is False:
logger.warning("Creating new version signatures file") logger.debug("Creating new version signatures file")
versions.touch() versions.touch()
return return
clean_legacy_versions_file(versions)
path = lookup(config, Preferences.DEFAULT) path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path) steam_path = Path(path)
if ids is None: if ids is None:
+6 -5
View File
@@ -9,10 +9,11 @@ import subprocess
import threading import threading
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING, Union from typing import Any, TYPE_CHECKING, Union
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import STEAM_SERVERS from dzgui.const.endpoints import STEAM_SERVERS
from dzgui.model.proxy_model import ServerRow
from dzgui.util.format import format_exception from dzgui.util.format import format_exception
from dzgui.util import strings from dzgui.util import strings
@@ -141,7 +142,7 @@ def sanitize(name: str) -> str:
def parse_json( def parse_json(
json: list[dict[str, Any]], json: list[dict[str, Any]],
) -> list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]: ) -> list[ServerRow]:
""" """
Server metadata is underspecified and server operators Server metadata is underspecified and server operators
tend to insert random garbage in the headers. In case tend to insert random garbage in the headers. In case
@@ -226,7 +227,7 @@ def parse_json(
mapname = row["map"].lower() mapname = row["map"].lower()
players = row["players"] players = row["players"]
max_players = row["max_players"] max_players = row["max_players"]
raw = ( raw = ServerRow(
name, name,
mapname, mapname,
view, view,
@@ -490,7 +491,7 @@ def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
return A2SInfo(record, None) return A2SInfo(record, None)
def short_ip_to_record(addr: str) -> Optional[Record]: def short_ip_to_record(addr: str) -> Record | None:
r = addr.split(":") r = addr.split(":")
# TODO: raise exceptions instead of Nonetype # TODO: raise exceptions instead of Nonetype
if len(r) != 2: if len(r) != 2:
@@ -498,7 +499,7 @@ def short_ip_to_record(addr: str) -> Optional[Record]:
return Record(r[0], 0, int(r[1])) return Record(r[0], 0, int(r[1]))
def fqip_to_record(addr: str) -> Optional[Record]: def fqip_to_record(addr: str) -> Record | None:
r = addr.split(":") r = addr.split(":")
if len(r) != 3: if len(r) != 3:
return None return None
+2 -3
View File
@@ -6,7 +6,6 @@ from importlib import resources
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.const.constants import ( from dzgui.const.constants import (
APP_NAME, APP_NAME,
APP_NAME_LOWER, APP_NAME_LOWER,
@@ -18,8 +17,8 @@ from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.migrate import ( from dzgui.init.migrate import (
has_new_config,
copy_state_files, copy_state_files,
has_new_config,
migrate_cols_file, migrate_cols_file,
) )
from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.init.prereqs import has_flatpak_client, has_steam_client
@@ -107,7 +106,7 @@ def load_steam_icon() -> None:
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
try: try:
traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH) traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH)
+2 -1
View File
@@ -6,7 +6,8 @@ if TYPE_CHECKING:
from dzgui.util.ip import Coords from dzgui.util.ip import Coords
# NOTE: mutable dataclass, 'use_miles' key changes on demand # NOTE: mutable dataclass: 'use_miles' key changes on demand
# 'frozen' cannot be True
@dataclass(slots=True) @dataclass(slots=True)
class UserPrefs: class UserPrefs:
is_steam_deck: bool is_steam_deck: bool
+1 -1
View File
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
from dzgui.init.proc import is_sandboxed from dzgui.init.proc import is_sandboxed
@dataclass @dataclass(slots=True, frozen=True)
class Xdg: class Xdg:
config: Path config: Path
columns: Path columns: Path
+2 -2
View File
@@ -8,9 +8,9 @@ STEAM_UNSUB = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1
STEAM_APP_DETAILS = "https://store.steampowered.com/api/appdetails?" STEAM_APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
GITHUB = "https://github.com/aclist" GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dzgui/releases/latest"
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest" CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
GITHUB_USER_RELEASES = "https://github.com/aclist/dztui/releases" GITHUB_USER_RELEASES = "https://github.com/aclist/dzgui/releases"
DB_IP = "https://db-ip.com/db/download/ip-to-city-lite" DB_IP = "https://db-ip.com/db/download/ip-to-city-lite"
IP_ECHO = "https://ipecho.net/plain" IP_ECHO = "https://ipecho.net/plain"
COORDS_API = "http://ip-api.com/json" COORDS_API = "http://ip-api.com/json"
+3 -9
View File
@@ -1,11 +1,11 @@
import gi
import logging import logging
import threading import threading
from typing import Any, TYPE_CHECKING, Union from typing import Any, TYPE_CHECKING, Union
import dzgui.util._json as JSON # noqa from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import ( from dzgui.const.enum import (
ButtonType, ButtonType,
ContextMenuItem, ContextMenuItem,
@@ -14,9 +14,6 @@ from dzgui.const.enum import (
Preferences, Preferences,
ServerTab, ServerTab,
) )
from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.controllers.emitter import Emitter from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager from dzgui.managers.connection import ConnectionManager
@@ -29,8 +26,6 @@ from dzgui.util.open_links import open_user_workshop, open_workshop_page
from dzgui.views.dialogs.filepicker import FilePicker from dzgui.views.dialogs.filepicker import FilePicker
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GObject from gi.repository import Gtk, Gdk, GLib, GObject
@@ -371,7 +366,6 @@ 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()
# FIXME: wipe maps store when changing tabs if model is none
def has_server_model(self) -> bool: def has_server_model(self) -> bool:
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
proxy_man = treeview.get_proxy_man() proxy_man = treeview.get_proxy_man()
+10 -7
View File
@@ -53,6 +53,11 @@
- Expand/collapse changelog versions - Expand/collapse changelog versions
- Support local light/dark mode changes on system theme - Support local light/dark mode changes on system theme
- Server time calculator - Server time calculator
- Add "Unmodded" filter to servers
- List timestamp of last DayZ client update
- Validate mods via Mod page to pull in updates
- Support for Nasdara (Badlands) DLC
- Load offline mods
### Changed ### Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
@@ -74,6 +79,7 @@
- Enable LAN page Empty/Full filters on first run of app - Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client - Propagate subscribed mods to Steam client
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow - Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
- Use SQLite DB for geolocation records
### Dropped ### Dropped
- Battlemetrics API support - Battlemetrics API support
@@ -92,16 +98,13 @@
- Moved dialogs out of threads - Moved dialogs out of threads
- History table not being in lock-step with state file - History table not being in lock-step with state file
- Current keyword filter not being applied when refreshing table - Current keyword filter not being applied when refreshing table
- Steam Flatpak cannot run DZGUI as a non-Steam game
### Unreleased - Collisions between native Steam and Steam Flatpak clients
- Load offline mods - Sticky toggle signal on radio buttons when reloading Options
- Local documentation
- Raw debug command in context menu
### Developer-facing ### Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page - Options -> Dev page
- Raw debug command in preconnect menu
## [6.0.5] 2026-05-17 ## [6.0.5] 2026-05-17
### Changed ### Changed
+22 -1
View File
@@ -1,12 +1,21 @@
import json import json
import logging
import re
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH from dzgui.const.constants import (
APP_NAME,
LEGACY_CONFIG_PATH,
LEGACY_COLS_PATH,
LEGACY_IPS_PATH,
)
from dzgui.config.convert import rc2json from dzgui.config.convert import rc2json
from dzgui.util._json import read_json, write_json from dzgui.util._json import read_json, write_json
logger = logging.getLogger(APP_NAME)
def migrate_legacy_conf(config: Path) -> Any: def migrate_legacy_conf(config: Path) -> Any:
old_conf = Path.home() / LEGACY_CONFIG_PATH old_conf = Path.home() / LEGACY_CONFIG_PATH
@@ -70,3 +79,15 @@ def copy_ipdb(ips_path: Path) -> None:
return return
# TODO: ensure that month file is copied # TODO: ensure that month file is copied
shutil.copy(legacy, ips_path) shutil.copy(legacy, ips_path)
def clean_legacy_versions_file(versions: Path) -> None:
# NOTE: handling for #523
try:
pat = r"^(\d+),(\d+)$"
lines = versions.read_text().splitlines()
clean = [line for line in lines if re.match(pat, line)]
concat = "\n".join(clean)
versions.write_text(concat)
except Exception as e:
logger.warning(e)
-1
View File
@@ -330,7 +330,6 @@ class ConnectionManager:
self.thread_man.update_dialog(waiting_for_launch) self.thread_man.update_dialog(waiting_for_launch)
while True: while True:
# FIXME: cancel should not be visible here per setting above
if self.controller.is_cancel_pending(): if self.controller.is_cancel_pending():
# TODO: some facility to also close spawned steam process # TODO: some facility to also close spawned steam process
return return
+153 -75
View File
@@ -1,7 +1,8 @@
import re import re
from dataclasses import dataclass from dataclasses import dataclass, fields
from typing import Any, TYPE_CHECKING, Union from operator import attrgetter
from typing import TYPE_CHECKING
from dzgui.const.enum import FilterMode from dzgui.const.enum import FilterMode
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
@@ -33,6 +34,105 @@ class ServerRow:
provider: str provider: str
modded: bool modded: bool
# NOTE: construct getter once for fast lookup in lieu of field introspection
_to_tuple = attrgetter(
"name",
"mapname",
"view",
"time",
"players",
"max_players",
"queue",
"ip",
"qport",
"ping",
"provider",
"modded",
)
@classmethod
def marshal_rows(
cls, rows: list["ServerRow"]
) -> list[tuple[str, str, str, str, int, int, int, str, int, str, str, bool]]:
"""Marshals data before passing into FastInsertListStore's extend() method"""
getter = cls._to_tuple
return [getter(row) for row in rows]
@classmethod
def get_index(cls, field: str) -> int:
_fields = [f.name for f in fields(cls)]
return _fields.index(field)
class FiltrationManager:
def __init__(self) -> None:
self.filter_map = {
strings.filter_3pp: self.filter_3pp,
strings.filter_1pp: self.filter_1pp,
strings.filter_official: self.filter_official,
strings.filter_unofficial: self.filter_unofficial,
strings.filter_empty: self.filter_empty,
strings.filter_full: self.filter_full,
strings.filter_duplicate: self.filter_duplicate,
strings.filter_day: self.filter_day,
strings.filter_night: self.filter_night,
strings.filter_nonascii: self.filter_nonascii,
strings.filter_lowpop: self.filter_lowpop,
strings.filter_modded: self.filter_modded,
strings.filter_unmodded: self.filter_unmodded,
}
def filter_3pp(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.view != strings.filter_3pp]
def filter_1pp(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.view != strings.filter_1pp]
def filter_official(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.provider != strings.filter_official]
def filter_unofficial(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.provider != strings.filter_unofficial]
def filter_empty(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.players != 0]
def filter_full(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.players != row.max_players]
def filter_duplicate(self, rows: list[ServerRow]) -> list[ServerRow]:
seen = set()
final = []
for row in rows:
if row.name not in seen:
seen.add(row.name)
final.append(row)
rows = final
return rows
def filter_day(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if not re.match(DAY_REG, row.time)]
def filter_night(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if not re.match(NIGHT_REG, row.time)]
def filter_nonascii(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.name.isascii()]
def filter_lowpop(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if (row.players / row.max_players * 100) > 30]
def filter_modded(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if not row.modded]
def filter_unmodded(self, rows: list[ServerRow]) -> list[ServerRow]:
return [row for row in rows if row.modded]
def filter(self, filter_type: str, rows: list[ServerRow]) -> list[ServerRow]:
func = self.filter_map[filter_type]
return func(rows)
class ProxyModelManager: class ProxyModelManager:
""" """
@@ -49,19 +149,15 @@ class ProxyModelManager:
def __init__(self, filter_man: "FilterManager") -> None: def __init__(self, filter_man: "FilterManager") -> None:
self.filter_cache: dict[ self.filter_cache: dict[
tuple[str, ...], tuple[str, ...],
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]], list[ServerRow],
] = {} ] = {}
self.proxy_model: "FastInsertListStore" self.proxy_model: "FastInsertListStore"
self.filter_man = filter_man self.filter_man = filter_man
self.filtration_manager = FiltrationManager()
self.control_model: ( self.control_model: list[ServerRow] | None = None
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]] self.filtered: list[ServerRow] = []
| None
) = None
self.filtered: list[
tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
] = []
self.success = True self.success = True
def has_control_model(self) -> bool: def has_control_model(self) -> bool:
@@ -69,9 +165,7 @@ class ProxyModelManager:
return False return False
return True return True
def append_row_to_control( def append_row_to_control(self, row: ServerRow) -> None:
self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
) -> None:
if self.control_model is None: if self.control_model is None:
raise AttributeError("Trying to add rows to a non-existent model") raise AttributeError("Trying to add rows to a non-existent model")
self.control_model.append(row) self.control_model.append(row)
@@ -79,21 +173,24 @@ class ProxyModelManager:
def update_playercount(self, playercount: "NewPlayerCount") -> None: def update_playercount(self, playercount: "NewPlayerCount") -> None:
treeiter = playercount.treeiter treeiter = playercount.treeiter
self.proxy_model[treeiter][4] = playercount.players field = [f.name for f in fields(ServerRow)]
self.proxy_model[treeiter][6] = playercount.queue index_players = field.index("players")
index_queue = field.index("queue")
self.proxy_model[treeiter][index_players] = playercount.players
self.proxy_model[treeiter][index_queue] = playercount.queue
def append_row_to_history( def append_row_to_history(
self, self,
history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool], history: ServerRow,
) -> None: ) -> None:
addr = history[7] addr = history.ip
qport = history[8] qport = history.qport
if self.control_model is None: if self.control_model is None:
raise AttributeError("Trying to append row to empty model") raise AttributeError("Trying to append row to empty model")
found = False found = False
for i, row in enumerate(self.control_model): for i, row in enumerate(self.control_model):
if addr == row[7] and qport == row[8]: if addr == row.ip and qport == row.qport:
item = self.control_model.pop(i) item = self.control_model.pop(i)
self.control_model.append(item) self.control_model.append(item)
found = True found = True
@@ -102,6 +199,7 @@ class ProxyModelManager:
if found is False: if found is False:
self.control_model.append(history) self.control_model.append(history)
# NOTE: truncate history to 10 records max
if len(self.control_model) == 11: if len(self.control_model) == 11:
del self.control_model[0] del self.control_model[0]
@@ -113,7 +211,7 @@ class ProxyModelManager:
addr = f"{record.ip}:{record.gameport}" addr = f"{record.ip}:{record.gameport}"
qport = record.qport qport = record.qport
for row in self.control_model: for row in self.control_model:
if addr == row[7] and qport == row[8]: if addr == row.ip and qport == row.qport:
self.control_model.remove(row) self.control_model.remove(row)
break break
self.wipe_cache() self.wipe_cache()
@@ -122,19 +220,22 @@ class ProxyModelManager:
def clear_proxy_model(self) -> None: def clear_proxy_model(self) -> None:
self.proxy_model.clear() self.proxy_model.clear()
def get_proxy_model(self) -> Union["FastInsertListStore", None]: def get_proxy_model(self) -> "FastInsertListStore | None":
if hasattr(self, "proxy_model"): if hasattr(self, "proxy_model"):
return self.proxy_model return self.proxy_model
return None return None
def filter( def filter(
self, mode: FilterMode, skip_cache: bool = False self, mode: FilterMode, skip_cache: bool = False
) -> Union["FastInsertListStore", None]: ) -> "FastInsertListStore | None":
""" """
Native Gtk.TreeView.refilter() method was not performant enough Native Gtk.TreeView.refilter() method was not performant enough
when running in the main loop with 40k+ records when running in the main loop with 40k+ records
skip_cache: used when updating a record on Saved Servers/History and forcing a refilter skip_cache: used when updating a record on Saved Servers/History and forcing a refilter
NOTE: data must be marshalled from ServerRow format before constructing list stores
The "clone" is the proxy model (list of tuples) actually fed into Gtk.TreeView
""" """
# TODO: filter cache: return a dataclass object with clearly enumerated map, keyword, and filter values # TODO: filter cache: return a dataclass object with clearly enumerated map, keyword, and filter values
# instead of just a serial list of strings # instead of just a serial list of strings
@@ -143,8 +244,9 @@ class ProxyModelManager:
if skip_cache is False: if skip_cache is False:
if filters in self.filter_cache: if filters in self.filter_cache:
cached_rows = self.filter_cache[filters] cached_rows = self.filter_cache[filters]
marshalled_rows = ServerRow.marshal_rows(cached_rows)
clone = ModelFactory().make_server_store() clone = ModelFactory().make_server_store()
clone.extend(cached_rows) clone.extend(marshalled_rows)
self.set_proxy_model(clone) self.set_proxy_model(clone)
self.set_filtered(cached_rows) self.set_filtered(cached_rows)
return None return None
@@ -163,17 +265,18 @@ class ProxyModelManager:
clone = ModelFactory().make_server_store() clone = ModelFactory().make_server_store()
if len(rows) > 0: if len(rows) > 0:
rows = self.sort_rows(rows) rows = self.sort_rows(rows)
clone.extend(rows) marshalled_rows = ServerRow.marshal_rows(rows)
clone.extend(marshalled_rows)
self.set_cache(filters, rows) self.set_cache(filters, rows)
self.set_proxy_model(clone) self.set_proxy_model(clone)
return clone return clone
def sort_rows(self, rows: list) -> list: def sort_rows(self, rows: list[ServerRow]) -> list[ServerRow]:
rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x[0].lower())) rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x.name.lower()))
return rows return rows
def filter_initial(self, filters: tuple) -> list: def filter_initial(self, filters: tuple[str, ...]) -> list[ServerRow]:
""" """
Simply culls the control model of any disabled filters Simply culls the control model of any disabled filters
""" """
@@ -182,7 +285,7 @@ class ProxyModelManager:
self.set_filtered(self.filter_toggle_off(filters, f)) self.set_filtered(self.filter_toggle_off(filters, f))
return self.filtered return self.filtered
def filter_map(self, filters: tuple) -> list: def filter_map(self, filters: tuple[str, ...]) -> list[ServerRow]:
""" """
Multi-filtration for any context starts by narrowing by map Multi-filtration for any context starts by narrowing by map
""" """
@@ -192,10 +295,10 @@ class ProxyModelManager:
if sel_map == str_filters.all_maps: if sel_map == str_filters.all_maps:
return rows return rows
rows = [row for row in rows if row[1] == sel_map] rows = [row for row in rows if row.mapname == sel_map]
return rows return rows
def filter_keyword(self, filters: tuple) -> list: def filter_keyword(self, filters: tuple[str, ...]) -> list[ServerRow]:
keyword = self.filter_man.get_active_keyword() keyword = self.filter_man.get_active_keyword()
rows = self.filtered rows = self.filtered
@@ -205,13 +308,15 @@ class ProxyModelManager:
filtered = [ filtered = [
row row
for row in rows for row in rows
if keyword in row[0].lower() if keyword in row.name.lower()
or keyword in row[1].lower() or keyword in row.mapname.lower()
or keyword in row[7].lower() or keyword in row.ip.lower()
] ]
return filtered return filtered
def filter_toggle_off(self, filters: tuple, filter_type: str) -> list: def filter_toggle_off(
self, filters: tuple[str, ...], filter_type: str
) -> list[ServerRow]:
""" """
Sub-filtration of the current model Sub-filtration of the current model
""" """
@@ -225,43 +330,11 @@ class ProxyModelManager:
self.set_filtered(None) self.set_filtered(None)
return [] return []
rows = self.filtered # TODO: pack filter type enum in filter checkbox, rather than stringwise comparison
match filter_type: rows = self.filtration_manager.filter(filter_type, self.filtered)
case strings.filter_3pp:
rows = [row for row in rows if row[2] != strings.filter_3pp]
case strings.filter_1pp:
rows = [row for row in rows if row[2] != strings.filter_1pp]
case strings.filter_official:
rows = [row for row in rows if row[10] != strings.filter_official]
case strings.filter_unofficial:
rows = [row for row in rows if row[10] != strings.filter_unofficial]
case strings.filter_empty:
rows = [row for row in rows if row[4] != 0]
case strings.filter_full:
rows = [row for row in rows if row[4] != row[5]]
case strings.filter_duplicate:
seen = set()
final = []
for row in rows:
if row[0] not in seen:
seen.add(row[0])
final.append(row)
rows = final
case strings.filter_day:
rows = [row for row in rows if not re.match(DAY_REG, row[3])]
case strings.filter_night:
rows = [row for row in rows if not re.match(NIGHT_REG, row[3])]
case strings.filter_nonascii:
rows = [row for row in rows if row[0].isascii()]
case strings.filter_lowpop:
rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
case strings.filter_modded:
rows = [row for row in rows if not row[11]]
case strings.filter_unmodded:
rows = [row for row in rows if row[11]]
return rows return rows
def filter_toggle_on(self, filters: tuple[str, ...]) -> list: def filter_toggle_on(self, filters: tuple[str, ...]) -> list[ServerRow]:
"""Effectively applies all filters""" """Effectively applies all filters"""
self.set_filtered(self.control_model) self.set_filtered(self.control_model)
self.set_filtered(self.filter_map(filters)) self.set_filtered(self.filter_map(filters))
@@ -272,31 +345,36 @@ class ProxyModelManager:
self.set_filtered(self.filter_toggle_off(filters, f)) self.set_filtered(self.filter_toggle_off(filters, f))
return self.filtered return self.filtered
def set_cache(self, filters: tuple[str, ...], filtered_rows: list[tuple]) -> None: def set_cache(
self, filters: tuple[str, ...], filtered_rows: list[ServerRow]
) -> None:
self.filter_cache[filters] = filtered_rows self.filter_cache[filters] = filtered_rows
def set_filtered(self, rows: list | None) -> None: def set_filtered(self, rows: list[ServerRow] | None) -> None:
if rows is None: if rows is None:
self.filtered = [] self.filtered = []
else: else:
self.filtered = rows self.filtered = rows
def get_filtered(self) -> list: def get_filtered(self) -> list[ServerRow]:
return self.filtered return self.filtered
def set_proxy_model(self, model: "FastInsertListStore") -> None: def set_proxy_model(self, model: "FastInsertListStore") -> None:
""" """
FastInsertListStore representation of the raw model after filtration FastInsertListStore representation of the raw model after filtration
This attribute is saved from within a thread so that it later be retrieved
and safely packed into the TreeView in the main loop, cf. get_proxy_model()
""" """
self.proxy_model = model self.proxy_model = model
def set_control(self, rows: list) -> None: def set_control(self, rows: list[ServerRow]) -> None:
""" """
Raw data model prior to filtration Raw data model prior to filtration
""" """
self.control_model = rows self.control_model = rows
def get_control(self) -> list[Any]: def get_control(self) -> list[ServerRow]:
if self.control_model is None: if self.control_model is None:
raise AttributeError( raise AttributeError(
"Expected a populated control model, but it is Nonetype" "Expected a populated control model, but it is Nonetype"
@@ -307,7 +385,7 @@ class ProxyModelManager:
self.filtered = [] self.filtered = []
self.filter_cache = {} self.filter_cache = {}
def push(self, data: list[Any]) -> None: def push(self, data: list[ServerRow]) -> None:
self.wipe_cache() self.wipe_cache()
self.set_control(data) self.set_control(data)
cur_keyword = self.filter_man.get_active_keyword() cur_keyword = self.filter_man.get_active_keyword()
+7 -12
View File
@@ -26,7 +26,7 @@ from gi.repository import Gtk, GLib
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import A2SInfo, Record from dzgui.api.servers import A2SInfo, Record
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.model.proxy_model import ProxyModelManager from dzgui.model.proxy_model import ProxyModelManager, ServerRow
from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.views.trees.tree_servers import ServerTreeView
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -34,7 +34,7 @@ LAN_TIMEOUT = 0.5
API_TIMEOUT = 3 API_TIMEOUT = 3
@dataclass @dataclass(slots=True, frozen=True)
class NewPlayerCount: class NewPlayerCount:
treeiter: Gtk.TreeIter treeiter: Gtk.TreeIter
players: int players: int
@@ -241,10 +241,7 @@ class ServerModelManager:
def update_history( def update_history(
self, self,
rows: ( rows: list["ServerRow"] | None = None,
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]
| None
) = None,
) -> None: ) -> None:
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
if rows is not None: if rows is not None:
@@ -267,7 +264,6 @@ class ServerModelManager:
self.first_iteration = False self.first_iteration = False
self.emitter.emit("load_maps", store) self.emitter.emit("load_maps", store)
# TODO: dataclass for record rows; check for other dict annotations
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None: def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
row, record = data row, record = data
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
@@ -345,8 +341,8 @@ class ServerModelManager:
return return
config_man.add_saved_server(fqip) config_man.add_saved_server(fqip)
# FIXME: this is valid if saved servers tab is already open, # TODO: this is valid if saved servers tab is already open,
# but not if app was just booted; causes single row to appear prematurely # but not if tab is unloaded; causes single row to appear prematurely
if proxy_man.has_control_model() is False: if proxy_man.has_control_model() is False:
proxy_man.push(records) proxy_man.push(records)
else: else:
@@ -457,13 +453,13 @@ class ServerModelManager:
dialog.show_all() dialog.show_all()
dialog.run() # type: ignore[no-untyped-call] dialog.run() # type: ignore[no-untyped-call]
def _push_data(self, data: list[Any]) -> None: def _push_data(self, data: list["ServerRow"]) -> None:
self._get_proxy_man().push(data) self._get_proxy_man().push(data)
self._sort_unique_maps(data) self._sort_unique_maps(data)
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success)) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
def _sort_unique_maps(self, data: list[Any]) -> None: def _sort_unique_maps(self, data: list[Any]) -> None:
u_maps = set([row[1] for row in data]) u_maps = set([row.mapname for row in data])
self._set_new_maps(sorted(u_maps)) self._set_new_maps(sorted(u_maps))
def _set_new_maps(self, maps: list[str]) -> None: def _set_new_maps(self, maps: list[str]) -> None:
@@ -477,7 +473,6 @@ class ServerModelManager:
@call_on_thread(dialog.filtering) @call_on_thread(dialog.filtering)
def refilter(self, mode: FilterMode) -> None: def refilter(self, mode: FilterMode) -> None:
# FIXME: causes two wait dialogs when map selection change signal emits after loading servers
self.first_iteration = False self.first_iteration = False
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
proxy_man.filter(mode) proxy_man.filter(mode)
+6 -11
View File
@@ -11,6 +11,7 @@ from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.init.prefix import get_version from dzgui.init.prefix import get_version
from dzgui.util.redact import redact_home from dzgui.util.redact import redact_home
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -27,28 +28,22 @@ def get_cpu_model() -> str:
def print_servers(servers: list[str]) -> str: def print_servers(servers: list[str]) -> str:
s = "" return "\n".join(servers)
for server in servers:
s += server + "\n"
return s
def print_mods(mods: list[int]) -> str: def print_mods(mods: list[int]) -> str:
s = "" return "\n".join(map(str, mods))
for mod in mods:
s += str(mod) + "\n"
return s
def write_diagnostic(config: Path, outfile: Path) -> None: def write_diagnostic(config: Path, outfile: Path) -> None:
date = datetime.now().isoformat() date = datetime.now().isoformat()
try: try:
# FIXME: ID_LIKE not available on CachyOS # NOTE: ID_LIKE not available on CachyOS
distro = platform.freedesktop_os_release()["ID_LIKE"] distro = platform.freedesktop_os_release()["ID_LIKE"]
except Exception as e: except Exception as e:
logger.warn(e) logger.warn(e)
distro = "Unknown" distro = unknown
kernel = os.uname().release kernel = os.uname().release
cpu = get_cpu_model() cpu = get_cpu_model()
@@ -68,7 +63,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
servers = lookup(config, Preferences.IP_LIST) servers = lookup(config, Preferences.IP_LIST)
servers_pretty = print_servers(servers) servers_pretty = print_servers(servers)
# FIXME: extraneous newlines in lists of mods
template = f"""\ template = f"""\
{APP_NAME} version {version} {APP_NAME} version {version}
Date: {date} Date: {date}
@@ -82,6 +76,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
Mods: Mods:
{mods_pretty} {mods_pretty}
Servers: Servers:
{servers_pretty} {servers_pretty}
""" """
+9
View File
@@ -0,0 +1,9 @@
from dataclasses import dataclass
from typing import Callable
def immutable_dataclass(cls: type | None = None, **kwargs: bool) -> type | Callable[[type], type]:
kwargs["slots"] = True
kwargs["frozen"] = True
if cls is None:
return lambda c: dataclass(c, **kwargs)
return dataclass(cls, **kwargs)
+2 -3
View File
@@ -1,10 +1,10 @@
import a2s import a2s
from ipaddress import ip_address
import requests import requests
import shutil import shutil
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from ipaddress import ip_address
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.servers import Record from dzgui.api.servers import Record
@@ -12,12 +12,11 @@ from dzgui.config.ipdb import read_from_table
from dzgui.const.constants import REQUEST_TIMEOUT from dzgui.const.constants import REQUEST_TIMEOUT
from dzgui.const.endpoints import IP_ECHO from dzgui.const.endpoints import IP_ECHO
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@dataclass @dataclass(slots=True, frozen=True)
class Coords: class Coords:
lat: float lat: float
lon: float lon: float
+5 -3
View File
@@ -1,3 +1,4 @@
import gi
import logging import logging
import signal import signal
@@ -34,8 +35,6 @@ from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook from dzgui.views.pages.servers import ServerNotebook
from dzgui.views.pages.thanks import Thanks from dzgui.views.pages.thanks import Thanks
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
gi.require_version("GLibUnix", "2.0") gi.require_version("GLibUnix", "2.0")
from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore
@@ -49,7 +48,10 @@ logger = logging.getLogger(APP_NAME)
class OuterWindow(Gtk.ApplicationWindow): class OuterWindow(Gtk.ApplicationWindow):
def __init__(self, app: "App") -> None: def __init__(self, app: "App") -> None:
super().__init__( super().__init__(
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER application=app,
title=APP_NAME,
border_width=10,
icon_name=APP_NAME_LOWER,
) )
self.hb = AppHeaderBar() self.hb = AppHeaderBar()
+3 -3
View File
@@ -1,9 +1,9 @@
from typing import Sequence
import gi import gi
from typing import Sequence
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
class GenericBox(Gtk.Box): class GenericBox(Gtk.Box):
+1 -1
View File
@@ -1,4 +1,4 @@
from typing import Callable, Literal, Self, TYPE_CHECKING, Union from typing import Callable, Literal, Self, TYPE_CHECKING
from dzgui.util.clip import copy_clipboard from dzgui.util.clip import copy_clipboard
from dzgui.strings import buttons from dzgui.strings import buttons
+5 -4
View File
@@ -1,10 +1,11 @@
from typing import Self
from dzgui.util import css
import gi import gi
from typing import Self
from dzgui.util import css
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
# TODO: subclass Gtk.Frame directly # TODO: subclass Gtk.Frame directly
+2 -3
View File
@@ -1,3 +1,4 @@
import gi
from typing import Self from typing import Self
from warnings import deprecated from warnings import deprecated
@@ -8,10 +9,8 @@ from dzgui.strings import options
from dzgui.views.components.box import HBox from dzgui.views.components.box import HBox
from dzgui.views.components.buttons import Icon from dzgui.views.components.buttons import Icon
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
@deprecated("Client choice is determined at setup time") @deprecated("Client choice is determined at setup time")
+3 -2
View File
@@ -106,14 +106,15 @@ 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)
# self.refresh_button.set_sensitive(False)
def _on_lan_page_init(self, emitter: "Emitter") -> None: def _on_lan_page_init(self, emitter: "Emitter") -> None:
self.filters_vbox.set_sensitive(False) 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
# TODO: wipe maps store when changing tabs if model is None
state = self.controller.has_server_model() state = self.controller.has_server_model()
self.filters_vbox.set_sensitive(state) self.filters_vbox.set_sensitive(state)
+1 -1
View File
@@ -1,7 +1,7 @@
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
class NoOverlayScrolledWindow(Gtk.ScrolledWindow): class NoOverlayScrolledWindow(Gtk.ScrolledWindow):
+1 -1
View File
@@ -101,7 +101,7 @@ class Statusbar(Gtk.Grid):
if type(context) is NotebookPage: if type(context) is NotebookPage:
self.prior_enum = context self.prior_enum = context
self.prior_status = "" self.prior_status = ""
# FIXME: CalcDist is being called when table is not loaded
if dist is None: if dist is None:
self.set_by_context(context, "") self.set_by_context(context, "")
return return
+5 -11
View File
@@ -1,13 +1,9 @@
import gi
import sys import sys
from enum import Enum
from typing import Any, Literal, Self, TYPE_CHECKING from typing import Any, Literal, Self, TYPE_CHECKING
# import time
from enum import Enum
from dzgui.views.mixins.colorscheme import ColorAwareApp
# TODO: import dialog titles
from dzgui.api.mods import remove_stale_signatures as remove_stale from dzgui.api.mods import remove_stale_signatures as remove_stale
from dzgui.config.ipdb import get_ipdb from dzgui.config.ipdb import get_ipdb
from dzgui.const.constants import HEX_GREEN, HEX_RED from dzgui.const.constants import HEX_GREEN, HEX_RED
@@ -20,12 +16,10 @@ from dzgui.util.format import format_exception
from dzgui.util.strings import dialog_header from dzgui.util.strings import dialog_header
from dzgui.util.symlink import rebuild_symlinks from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.components.buttons import ClipboardButton from dzgui.views.components.buttons import ClipboardButton
from dzgui.views.mixins.colorscheme import ColorAwareApp
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk # noqa from gi.repository import Gtk, GLib, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.config.xdg import Xdg from dzgui.config.xdg import Xdg
+2 -1
View File
@@ -54,6 +54,7 @@ class FolderPicker(Gtk.FileChooserDialog):
return None return None
if res == Gtk.ResponseType.OK: if res == Gtk.ResponseType.OK:
folder = self.get_current_folder() folder = self.get_current_folder()
# TODO: this should never be possible
if folder is None: if folder is None:
try: try:
uri = self.get_uri() uri = self.get_uri()
@@ -61,7 +62,7 @@ class FolderPicker(Gtk.FileChooserDialog):
return None return None
return Path.from_uri(uri) return Path.from_uri(uri)
except Exception: except Exception:
# FIXME: edge cases # TODO: edge cases
return Path("/") return Path("/")
self.destroy() self.destroy()
return Path(folder) return Path(folder)
+1 -1
View File
@@ -15,7 +15,7 @@ from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, Pango # noqa from gi.repository import Gtk, Gdk, Pango
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import Details from dzgui.api.servers import Details
+3 -3
View File
@@ -1,3 +1,5 @@
import gi
from typing import Any from typing import Any
from dzgui.const.constants import ( from dzgui.const.constants import (
@@ -7,10 +9,8 @@ from dzgui.const.constants import (
GIO_SETTINGS_VAL, GIO_SETTINGS_VAL,
) )
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio # noqa from gi.repository import Gtk, Gio
class ColorAwareApp: class ColorAwareApp:
+1 -1
View File
@@ -1,7 +1,7 @@
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk, Gdk
class HelpMenuMixin: class HelpMenuMixin:
+3 -3
View File
@@ -1,10 +1,10 @@
import gi
from dzgui.const.enum import VAdjustment from dzgui.const.enum import VAdjustment
from dzgui.const.constants import SCROLL_INCREMENT from dzgui.const.constants import SCROLL_INCREMENT
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk, Gdk
class ScrollableMixin: class ScrollableMixin:
+2 -3
View File
@@ -1,3 +1,4 @@
import gi
from pathlib import Path from pathlib import Path
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
@@ -8,10 +9,8 @@ from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.scrollable import NoOverlayScrolledWindow from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
+6 -7
View File
@@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import gi
from enum import Enum from enum import Enum
from typing import Self, TYPE_CHECKING, Union from typing import Self, TYPE_CHECKING, Union
from dzgui.util import css
from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
@@ -11,8 +12,10 @@ from dzgui.const.constants import (
APPNAME_DAYZ_EXP_HUMAN, APPNAME_DAYZ_EXP_HUMAN,
) )
from dzgui.const.enum import ContextMenuGroup, NotebookPage from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
from dzgui.managers.offline import OfflineManager from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline from dzgui.strings import generic, offline
from dzgui.util import css
from dzgui.views.components.box import HBox, VBox from dzgui.views.components.box import HBox, VBox
from dzgui.views.components.buttons import Icon, IconTextButton from dzgui.views.components.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.eventbox import InfoEventBox
@@ -21,11 +24,8 @@ from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.components.misc import ErrorPopover from dzgui.views.components.misc import ErrorPopover
from dzgui.views.trees.tree_mods import OfflineModTreeView from dzgui.views.trees.tree_mods import OfflineModTreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GObject # noqa from gi.repository import Gtk, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@@ -136,7 +136,6 @@ class FolderHBox(HBox):
self.folder = "" self.folder = ""
self.label.set_label("") self.label.set_label("")
self.unset_button.hide() self.unset_button.hide()
# FIXME: rename signal
self.emitter.emit("custom_mods_unloaded", self) self.emitter.emit("custom_mods_unloaded", self)
def get_button(self) -> Gtk.Button: def get_button(self) -> Gtk.Button:
+14 -17
View File
@@ -1,9 +1,10 @@
import gi
from pathlib import Path from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING from typing import Any, Callable, TYPE_CHECKING
from dzgui.api import pefile as PeFile from dzgui.api import pefile as PeFile
from dzgui.api.steam import get_last_updated from dzgui.api.steam import get_last_updated
from dzgui.config import query from dzgui.config import query
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
@@ -14,12 +15,11 @@ from dzgui.const.constants import (
FLATPAK_SANDBOX, FLATPAK_SANDBOX,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
) )
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
from dzgui.const.endpoints import STEAM_API_SETUP from dzgui.const.endpoints import STEAM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab from dzgui.const.enum import Preferences, ServerTab
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
from dzgui.strings import options from dzgui.strings import options
from dzgui.util import strings, css, open_links from dzgui.util import strings, css, open_links
from dzgui.views.components.box import ShortHBox, VBox from dzgui.views.components.box import ShortHBox, VBox
from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.labels import LeftLabel from dzgui.views.components.labels import LeftLabel
@@ -29,11 +29,8 @@ from dzgui.views.components.misc import ErrorPopover
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@@ -233,10 +230,19 @@ class ToggleField(Gtk.Box):
self.pack_start(self.radio1, expand=False, fill=False, padding=0) self.pack_start(self.radio1, expand=False, fill=False, padding=0)
self.pack_start(self.radio2, expand=False, fill=False, padding=0) self.pack_start(self.radio2, expand=False, fill=False, padding=0)
self.signal_id: int | None = None
def disconnect_signal(self) -> None:
if self.signal_id is not None:
self.radio1.disconnect(self.signal_id)
def set_suboption_active(self, state: bool) -> None: def set_suboption_active(self, state: bool) -> None:
# NOTE: defer connection of signal until after state is set # NOTE: defer connection of signal until after state is set
self.radio2.set_active(state) self.radio2.set_active(state)
self.radio1.connect("toggled", self._on_radio_toggled, self.context) self.disconnect_signal()
self.signal_id = self.radio1.connect(
"toggled", self._on_radio_toggled, self.context
)
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None: def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
self.controller.toggle_config(context) self.controller.toggle_config(context)
@@ -486,14 +492,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
start_tab = self.controller.query_config(Preferences.START_TAB) start_tab = self.controller.query_config(Preferences.START_TAB)
self.start_tab_combo.set_active(start_tab) self.start_tab_combo.set_active(start_tab)
def _suppress_toggles(self, state: bool) -> None:
for toggle in [
self.fullscreen_toggle,
self.distance_toggle,
]:
self.controller.suppress_signal(
self, toggle.get_children()[0], "_on_radio_toggled", state
)
def grab_content_area(self) -> None: def grab_content_area(self) -> None:
self.grab_focus() self.grab_focus()
+3 -20
View File
@@ -1,11 +1,12 @@
from dataclasses import dataclass import gi
from typing import Self, Sequence, TYPE_CHECKING from typing import Self, Sequence, TYPE_CHECKING
from dzgui.const.enum import NotebookPage
from dzgui.const.icons import ( from dzgui.const.icons import (
ICON_ERROR, ICON_ERROR,
ICON_WARNING, ICON_WARNING,
) )
from dzgui.const.enum import NotebookPage
from dzgui.util.css import add_class from dzgui.util.css import add_class
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.util.localize import number from dzgui.util.localize import number
@@ -15,33 +16,15 @@ from dzgui.views.dialogs.generic import DebugDialog
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.trees.tree_server_mods import ServerModTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk from gi.repository import Gdk, Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.managers.connection import Prerequisites from dzgui.managers.connection import Prerequisites
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter from dzgui.controllers.emitter import Emitter
@dataclass
class Warnings:
passworded: bool
dayz_running: bool
no_steam: bool
wrong_version: bool
@dataclass
class Errors:
no_space: bool
no_dayz_exp: bool
no_dayz: bool
class Placeholder(Gtk.Label): class Placeholder(Gtk.Label):
def __init__(self, text: str) -> None: def __init__(self, text: str) -> None:
super().__init__( super().__init__(
+5 -6
View File
@@ -1,15 +1,14 @@
import gi
import logging import logging
from typing import Any, Optional, TYPE_CHECKING from typing import Any, TYPE_CHECKING
from dzgui.const.constants import APP_NAME, SEPARATOR from dzgui.const.constants import APP_NAME, SEPARATOR
from dzgui.util.keys import is_ctrl_mask, is_navkey from dzgui.util.keys import is_ctrl_mask, is_navkey
from dzgui.views.mixins.cursor_mixin import CursorMixin from dzgui.views.mixins.cursor_mixin import CursorMixin
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, Gdk, GObject
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -20,7 +19,7 @@ if TYPE_CHECKING:
class TreeView(CursorMixin, Gtk.TreeView): # type: ignore class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
def __init__( def __init__(
self, controller: "Controller", menu: Optional["ContextMenuGroup"] self, controller: "Controller", menu: "ContextMenuGroup | None"
) -> None: ) -> None:
super().__init__( super().__init__(
enable_search=False, enable_search=False,
@@ -59,7 +58,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
return True return True
return False return False
def get_current_iter(self) -> Optional[Gtk.TreeIter]: def get_current_iter(self) -> Gtk.TreeIter | None:
it = self.get_selection().get_selected()[1] it = self.get_selection().get_selected()[1]
return it return it
+1 -1
View File
@@ -10,7 +10,7 @@ from dzgui.views.trees.tree_base import TreeView
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
+3 -3
View File
@@ -1,4 +1,6 @@
import gi
import logging import logging
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
from dzgui.const.constants import APP_NAME, HEX_RED from dzgui.const.constants import APP_NAME, HEX_RED
@@ -9,10 +11,8 @@ from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.mixins.mods_mixin import ModsMixin from dzgui.views.mixins.mods_mixin import ModsMixin
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
+5 -5
View File
@@ -1,15 +1,15 @@
import gi
from typing import TYPE_CHECKING
from dzgui.const.enum import ContextMenuGroup from dzgui.const.enum import ContextMenuGroup
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
from dzgui.strings import server_mods from dzgui.strings import server_mods
from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
from typing import TYPE_CHECKING
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, Pango # noqa from gi.repository import Gtk, Pango
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
+32 -37
View File
@@ -1,30 +1,25 @@
import gi
import logging import logging
import threading import threading
from queue import Queue from queue import Queue
from typing import Any, Self, Union from typing import Any, Literal, Self, TYPE_CHECKING, Union
from warnings import deprecated from warnings import deprecated
from dzgui.api.servers import ping, Record from dzgui.api.servers import ping, Record
from dzgui.const.constants import APP_NAME from dzgui.const.constants import APP_NAME
from dzgui.const.enum import ContextMenuItem, ContextMenuGroup, ServerTab from dzgui.const.enum import ContextMenuItem, ContextMenuGroup, ServerTab
from dzgui.managers.filter import FilterManager from dzgui.managers.filter import FilterManager
from dzgui.model.proxy_model import ProxyModelManager from dzgui.model.proxy_model import ProxyModelManager, ServerRow
from dzgui.util import strings from dzgui.util import strings
from dzgui.util.dist import CalcDist from dzgui.util.dist import CalcDist
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.context_mixin import ContextMixin
from typing import Literal, TYPE_CHECKING
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
import dzgui.util._json as JSON # noqa import dzgui.util._json as JSON # noqa
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, GLib, Gdk, GObject
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -34,8 +29,15 @@ if TYPE_CHECKING:
from dzgui.model.model_factory import FastInsertListStore from dzgui.model.model_factory import FastInsertListStore
from dzgui.util.dist import Haversine from dzgui.util.dist import Haversine
# TODO: consts
QUEUE_CHECK_DELAY = 200 QUEUE_CHECK_DELAY = 200
IND_NAME = ServerRow.get_index("name")
IND_ADDR = ServerRow.get_index("ip")
IND_QPORT = ServerRow.get_index("qport")
IND_PING = ServerRow.get_index("ping")
IND_MODDED = ServerRow.get_index("modded")
class ServerTreeView(ContextMixin, TreeView): # type: ignore class ServerTreeView(ContextMixin, TreeView): # type: ignore
def __init__( def __init__(
@@ -106,7 +108,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
if column_title in width_map: if column_title in width_map:
w = width_map[column_title] w = width_map[column_title]
column.set_fixed_width(w) column.set_fixed_width(w)
if column_title == "Ping": if i == IND_PING:
column.set_cell_data_func(renderer, self._get_ping) column.set_cell_data_func(renderer, self._get_ping)
column.connect("notify::fixed-width", self._on_col_width_changed) column.connect("notify::fixed-width", self._on_col_width_changed)
@@ -145,8 +147,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
if model is None: if model is None:
return False return False
tree_iter = model.get_iter(path[0]) tree_iter = model.get_iter(path[0])
ip = model.get_value(tree_iter, 7) ip = model.get_value(tree_iter, IND_ADDR)
qport = model.get_value(tree_iter, 8) qport = model.get_value(tree_iter, IND_QPORT)
addr = ip + ":" + str(qport) addr = ip + ":" + str(qport)
note = self.controller.get_note_by_record(addr) note = self.controller.get_note_by_record(addr)
@@ -169,14 +171,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
return self.enum return self.enum
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
# TODO: disable filter panel if current model is None
state = self.get_enum() is ServerTab.LAN state = self.get_enum() is ServerTab.LAN
self.emitter.emit("lan_tab_toggled", state) self.emitter.emit("lan_tab_toggled", state)
store = self.filter_man.get_map_store() store = self.filter_man.get_map_store()
# FIXME: if model is none, wipe maps # TODO: distinguish this signal from changing map combo in-situ
# distinguish this signal from changing map combo in-situ
self.emitter.emit("load_maps", store) self.emitter.emit("load_maps", store)
self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc) self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc)
self.start_queue_checker() self.start_queue_checker()
@@ -221,11 +221,11 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _check_result_queue(self) -> Literal[True]: def _check_result_queue(self) -> Literal[True]:
latest_result = None latest_result = None
# FIXME: empty results cause statusbar to be emptied # TODO: Nonetype results cause statusbar to be blank
while not self.queue.empty(): while not self.queue.empty():
latest_result = self.queue.get() latest_result = self.queue.get()
# FIXME: cache is being checked in two passes # TODO: cache is being checked in two passes
cache = self.controller.get_dist_cache() cache = self.controller.get_dist_cache()
if latest_result: if latest_result:
@@ -267,15 +267,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
(model, pathlist) = sels (model, pathlist) = sels
path = pathlist[0] path = pathlist[0]
tree_iter = model.get_iter(path) tree_iter = model.get_iter(path)
has_mods = model.get_value(tree_iter, 11) has_mods = model.get_value(tree_iter, IND_MODDED)
return bool(has_mods) return bool(has_mods)
# def get_selected_row(self) -> Gtk.TreeModelRow:
# sel = self.get_selection()
# sels = sel.get_selected_rows()
# print(type(sels[0]))
# return sels[0]
def is_in_favs(self) -> bool: def is_in_favs(self) -> bool:
record = self.get_record_string() record = self.get_record_string()
if self.controller.get_config_man().is_in_favs(record): if self.controller.get_config_man().is_in_favs(record):
@@ -304,17 +298,17 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
self.start_distcalc() self.start_distcalc()
def get_name(self) -> str: def get_name(self) -> str:
return str(self.get_value_at_index(0)) return str(self.get_value_at_index(IND_NAME))
def get_simplified_ip(self) -> str: def get_simplified_ip(self) -> str:
addr = self.get_value_at_index(7) addr = self.get_value_at_index(IND_ADDR)
qport = self.get_value_at_index(8) qport = self.get_value_at_index(IND_QPORT)
ip = addr.split(":")[0] ip = addr.split(":")[0]
return f"{ip}:{qport}" return f"{ip}:{qport}"
def get_record_string(self) -> str: def get_record_string(self) -> str:
addr = self.get_value_at_index(7) addr = self.get_value_at_index(IND_ADDR)
qport = self.get_value_at_index(8) qport = self.get_value_at_index(IND_QPORT)
return f"{addr}:{qport}" return f"{addr}:{qport}"
def get_record(self) -> Record | None: def get_record(self) -> Record | None:
@@ -353,8 +347,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _redraw( def _redraw(
self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int
) -> None: ) -> None:
ping_column = 9 model.set(_iter, IND_PING, _ping)
model.set(_iter, ping_column, _ping)
self.queue_draw() self.queue_draw()
def wipe_cache(self) -> None: def wipe_cache(self) -> None:
@@ -369,13 +362,10 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
data: Any, data: Any,
) -> None: ) -> None:
addr_column = 7 addr = model.get_value(_iter, IND_ADDR).split(":")
qport_column = 8
addr = model.get_value(_iter, addr_column).split(":")
ip = addr[0] ip = addr[0]
gameport = int(addr[1]) gameport = int(addr[1])
qport = model.get_value(_iter, qport_column) qport = model.get_value(_iter, IND_QPORT)
record = Record(ip, gameport, qport) record = Record(ip, gameport, qport)
if record in self.seen_cache: if record in self.seen_cache:
@@ -413,5 +403,10 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
if row_index >= start[0] <= end[0]: if row_index >= start[0] <= end[0]:
# NOTE: fetch raw data rows # NOTE: fetch raw data rows
real_model = self.proxy_man.get_control() real_model = self.proxy_man.get_control()
value = real_model[row_index][col_index] from dataclasses import fields
from dzgui.model.proxy_model import ServerRow
key = fields(ServerRow)[col_index].name
value = getattr(real_model[row_index], key)
cell.set_property("text", str(value)) cell.set_property("text", str(value))
+5 -5
View File
@@ -1,13 +1,13 @@
import gi
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.model.model_factory import FastInsertListStore from dzgui.model.model_factory import FastInsertListStore
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class SimpleTree(Gtk.ScrolledWindow): class SimpleTree(Gtk.ScrolledWindow):
"""Simple scrollable 1-column tree""" """Simple scrollable 1-column tree"""
+4 -2
View File
@@ -1,5 +1,7 @@
from pathlib import Path from pathlib import Path
CWD = Path(__file__).parent CWD = Path(__file__).parent
def fixture_path(fixture: str) -> str:
return str(CWD / fixture)
def fixture_path(fixture: str) -> Path:
return Path(CWD).joinpath(fixture)
+9 -6
View File
@@ -1,6 +1,7 @@
import pytest import pytest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from dzgui.api.steam import get_app_allows_downloads, get_last_updated from dzgui.api.steam import get_app_allows_downloads, get_last_updated
@@ -36,30 +37,32 @@ def steam_path() -> Path:
@pytest.fixture(scope="module", autouse=True) @pytest.fixture(scope="module", autouse=True)
def patch_api() -> None: def patch_api() -> Iterator[None]:
mp = MonkeyPatch() mp = MonkeyPatch()
mp.setattr("dzgui.api.steam.get_app_path", mock_path) mp.setattr("dzgui.api.steam.get_app_path", mock_path)
yield yield
mp.undo() mp.undo()
def test_no_last_update(steam_path) -> None: def test_no_last_update(steam_path: Path) -> None:
assert get_last_updated(steam_path, 555) is None assert get_last_updated(steam_path, 555) is None
def test_last_update(steam_path) -> None: def test_last_update(steam_path: Path) -> None:
assert get_last_updated(steam_path, 444) == "2026-10-22 00:09:33" assert get_last_updated(steam_path, 444) == "2026-10-22 00:09:33"
def test_app_allows_downloads(steam_path) -> None: def test_app_allows_downloads(steam_path: Path) -> None:
assert get_app_allows_downloads(steam_path, 111) is True assert get_app_allows_downloads(steam_path, 111) is True
def test_app_disallows_downloads(steam_path): def test_app_disallows_downloads(steam_path: Path) -> None:
assert get_app_allows_downloads(steam_path, 222) is False assert get_app_allows_downloads(steam_path, 222) is False
def test_app_delegates_downloads(monkeypatch, steam_path, client_allows): def test_app_delegates_downloads(
monkeypatch: MonkeyPatch, steam_path: Path, client_allows: Path
) -> None:
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_disallows) monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_disallows)
assert get_app_allows_downloads(steam_path, 333) is False assert get_app_allows_downloads(steam_path, 333) is False
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_allows) monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_allows)
+10 -4
View File
@@ -1,6 +1,8 @@
import pytest import pytest
import tempfile import tempfile
from typing import TYPE_CHECKING
from dzgui.app_init import copy_bare_configs from dzgui.app_init import copy_bare_configs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
@@ -8,9 +10,13 @@ from dzgui.config.xdg import get_xdg_paths, parse_filepaths
CONF_STRING = "DZGUI_CONF\n" CONF_STRING = "DZGUI_CONF\n"
STATE_STRING = "DZGUI_STATE\n" STATE_STRING = "DZGUI_STATE\n"
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
from dzgui.config.xdg import Xdg
@pytest.fixture @pytest.fixture
def state_files(): def state_files() -> list[str]:
state = [ state = [
"dzg.history", "dzg.history",
"dzg.versions", "dzg.versions",
@@ -25,7 +31,7 @@ def state_files():
@pytest.fixture @pytest.fixture
def xdg_paths(monkeypatch): def xdg_paths(monkeypatch: "MonkeyPatch") -> "Xdg":
routes = { routes = {
"XDG_CONFIG_HOME": "", "XDG_CONFIG_HOME": "",
"XDG_STATE_HOME": "", "XDG_STATE_HOME": "",
@@ -42,7 +48,7 @@ def xdg_paths(monkeypatch):
@pytest.mark.mods @pytest.mark.mods
def test_config_file_import(xdg_paths, state_files): def test_config_file_import(xdg_paths: "Xdg", state_files: list[str]) -> None:
# NOTE: write bare files in root # NOTE: write bare files in root
tmp_conf = xdg_paths.config.parent.parent tmp_conf = xdg_paths.config.parent.parent
@@ -67,7 +73,7 @@ def test_config_file_import(xdg_paths, state_files):
@pytest.mark.mods @pytest.mark.mods
def test_config_file_no_import(xdg_paths, state_files): def test_config_file_no_import(xdg_paths: "Xdg", state_files: list[str]) -> None:
for subdir in xdg_paths.config.parent, xdg_paths.resolution.parent: for subdir in xdg_paths.config.parent, xdg_paths.resolution.parent:
subdir.mkdir() subdir.mkdir()
+2 -2
View File
@@ -5,7 +5,7 @@ from pathlib import Path
@pytest.fixture @pytest.fixture
def changelog(request) -> None: def changelog(request: pytest.FixtureRequest) -> str:
root = request.config.rootpath root = request.config.rootpath
changelog = Path(root).joinpath("CHANGELOG.md").read_text() changelog = Path(root).joinpath("CHANGELOG.md").read_text()
return changelog return changelog
@@ -19,7 +19,7 @@ def count_hash(line: str) -> int:
return cnt return cnt
def test_changelog_prefix(changelog) -> None: def test_changelog_prefix(changelog: str) -> None:
r = r".*(\[.*\]).*" r = r".*(\[.*\]).*"
lines = changelog.splitlines() lines = changelog.splitlines()
sort = sorted(lines) sort = sorted(lines)
+7 -2
View File
@@ -1,14 +1,19 @@
import pytest import pytest
from typing import Any, TYPE_CHECKING
from dzgui.const.constants import FLATPAK_RUN_CMD, STEAM_CMD from dzgui.const.constants import FLATPAK_RUN_CMD, STEAM_CMD
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value, expected", [(FLATPAK_RUN_CMD, "Flatpak"), (STEAM_CMD, "Steam")] "value, expected", [(FLATPAK_RUN_CMD, "Flatpak"), (STEAM_CMD, "Steam")]
) )
def test_human_name(monkeypatch, value, expected) -> None: def test_human_name(monkeypatch: "MonkeyPatch", value: str, expected: str) -> None:
def mock_query_config(*args) -> str: def mock_query_config(*args: Any) -> str:
return value return value
monkeypatch.setattr( monkeypatch.setattr(
+4 -5
View File
@@ -1,16 +1,15 @@
import gi
import pytest import pytest
import subprocess import subprocess
from dzgui.const.constants import GIO_SETTINGS_KEY from dzgui.const.constants import GIO_SETTINGS_KEY
from dzgui.views.mixins.colorscheme import ColorAwareApp from dzgui.views.mixins.colorscheme import ColorAwareApp
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib # noqa from gi.repository import Gtk, GLib
class DummyWidget(ColorAwareApp, Gtk.Button): class DummyWidget(ColorAwareApp, Gtk.Button): # type: ignore
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -27,7 +26,7 @@ def widget() -> DummyWidget:
@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)]) @pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)])
def test_light_mode(widget, key: str, expect: bool) -> None: def test_light_mode(widget: DummyWidget, key: str, expect: bool) -> None:
local_pref = subprocess.run( local_pref = subprocess.run(
["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"], ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"],
capture_output=True, capture_output=True,
+21 -6
View File
@@ -1,33 +1,48 @@
import pytest import pytest
from typing import TYPE_CHECKING
from dzgui.init.migrate import convert_cols_file from dzgui.init.migrate import convert_cols_file
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
if TYPE_CHECKING:
from pathlib import Path
@pytest.fixture @pytest.fixture
def columns_with_perspective(): def columns_with_perspective() -> "Path":
return fixture_path("columns_1") return fixture_path("columns_1")
@pytest.fixture @pytest.fixture
def columns_without_perspective(): def columns_without_perspective() -> "Path":
return fixture_path("columns_2") return fixture_path("columns_2")
@pytest.fixture @pytest.fixture
def columns_with_view(): def columns_with_view() -> "Path":
return fixture_path("columns_3") return fixture_path("columns_3")
@pytest.mark.config @pytest.mark.config
def test_columns_with_perspective(columns_with_perspective): def test_columns_with_perspective(columns_with_perspective: "Path") -> None:
j = convert_cols_file(columns_with_perspective) j = convert_cols_file(columns_with_perspective)
if j is None:
return
assert "View" in j["cols"] assert "View" in j["cols"]
assert "Max" in j["cols"] assert "Max" in j["cols"]
@pytest.mark.config @pytest.mark.config
def test_columns_without_perspective(columns_without_perspective): def test_columns_without_perspective(columns_without_perspective: "Path") -> None:
j = convert_cols_file(columns_without_perspective) j = convert_cols_file(columns_without_perspective)
if j is None:
return
assert "View" not in j["cols"] assert "View" not in j["cols"]
assert "Max" not in j["cols"] assert "Max" not in j["cols"]
@pytest.mark.config @pytest.mark.config
def test_columns_with_view(columns_with_view): def test_columns_with_view(columns_with_view: "Path") -> None:
j = convert_cols_file(columns_with_view) j = convert_cols_file(columns_with_view)
assert j is None assert j is None
+17 -12
View File
@@ -1,25 +1,30 @@
import json import json
import pytest import pytest
from typing import Any, TYPE_CHECKING
from dzgui.const.boilerplate import config_boilerplate from dzgui.const.boilerplate import config_boilerplate
from dzgui.config.query import get_config from dzgui.config.query import get_config
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.config import convert from dzgui.config import convert
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
if TYPE_CHECKING:
from pathlib import Path
@pytest.fixture @pytest.fixture
def legacy_config(): def legacy_config() -> "Path":
return fixture_path("dztuirc_1") return fixture_path("dztuirc_1")
@pytest.fixture @pytest.fixture
def unset_values(): def unset_values() -> "Path":
return fixture_path("dztuirc_3") return fixture_path("dztuirc_3")
@pytest.fixture @pytest.fixture
def keys(): def keys() -> list[str]:
return [ return [
"fav_server", "fav_server",
"fav_label", "fav_label",
@@ -36,7 +41,7 @@ def keys():
# TODO: use a static fixture instead of system config # TODO: use a static fixture instead of system config
@pytest.fixture @pytest.fixture
def config(): def config() -> Any:
paths = get_xdg_paths() paths = get_xdg_paths()
xdg = parse_filepaths(paths) xdg = parse_filepaths(paths)
return get_config(xdg.config) return get_config(xdg.config)
@@ -44,21 +49,21 @@ def config():
@pytest.mark.post_install @pytest.mark.post_install
@pytest.mark.realconfig @pytest.mark.realconfig
def test_invalid_config_value(config): def test_invalid_config_value(config: Any) -> None:
with pytest.raises(Exception): with pytest.raises(Exception):
assert config["foo"] is None assert config["foo"] is None
@pytest.mark.post_install @pytest.mark.post_install
@pytest.mark.realconfig @pytest.mark.realconfig
def test_default_config_values(keys, config): def test_default_config_values(keys: list[str], config: Any) -> None:
for key in keys: for key in keys:
assert config[key] is not None assert config[key] is not None
@pytest.mark.post_install @pytest.mark.post_install
@pytest.mark.realconfig @pytest.mark.realconfig
def test_contains_invalid_values(keys, config): def test_contains_invalid_values(keys: list[str], config: Any) -> None:
for key in config: for key in config:
assert key in keys assert key in keys
@@ -70,15 +75,15 @@ def test_contains_invalid_values(keys, config):
("dztuirc_2", (False, False, False)), ("dztuirc_2", (False, False, False)),
], ],
) )
def test_bool_conversion(fixture, expect): def test_bool_conversion(fixture: str, expect: tuple[bool, bool, bool]) -> None:
fixture = fixture_path(fixture) fixture_p = fixture_path(fixture)
j = convert.rc2json(fixture) j = convert.rc2json(fixture_p)
j = json.loads(j) j = json.loads(j)
assert j["fullscreen"] == expect[0] assert j["fullscreen"] == expect[0]
assert not j["use_miles"] assert not j["use_miles"]
def test_key_conversion(legacy_config): def test_key_conversion(legacy_config: "Path") -> None:
j = convert.rc2json(legacy_config) j = convert.rc2json(legacy_config)
j = json.loads(j) j = json.loads(j)
keys = [ keys = [
@@ -92,7 +97,7 @@ def test_key_conversion(legacy_config):
assert key not in j assert key not in j
def test_missing_values(unset_values): def test_missing_values(unset_values: "Path") -> None:
j = convert.rc2json(unset_values) j = convert.rc2json(unset_values)
j = json.loads(j) j = json.loads(j)
assert j.keys() == config_boilerplate.keys() assert j.keys() == config_boilerplate.keys()
+11 -7
View File
@@ -1,6 +1,8 @@
import configparser import configparser
import pytest import pytest
from pathlib import Path
from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID, TMP_PATH from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID, TMP_PATH
from dzgui.config.xdg import get_xdg_paths from dzgui.config.xdg import get_xdg_paths
from dzgui.util.flatpak import apply_overrides from dzgui.util.flatpak import apply_overrides
@@ -13,17 +15,17 @@ pytestmark = pytest.mark.flatpak
@pytest.fixture @pytest.fixture
def xdg_paths() -> dict[str, str]: def xdg_paths() -> dict[str, Path]:
return get_xdg_paths() return get_xdg_paths()
@pytest.fixture @pytest.fixture
def override(xdg_paths) -> None: def override(xdg_paths: dict[str, Path]) -> None:
apply_overrides(xdg_paths) apply_overrides(xdg_paths)
@pytest.fixture @pytest.fixture
def config(xdg_paths) -> configparser.ConfigParser: def config(xdg_paths: dict[str, Path]) -> configparser.ConfigParser:
conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath( conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath(
f"flatpak/overrides/{FLATPAK_APPID}" f"flatpak/overrides/{FLATPAK_APPID}"
) )
@@ -32,14 +34,16 @@ def config(xdg_paths) -> configparser.ConfigParser:
return config return config
def test_session_bus(override, config) -> None: def test_session_bus(override: None, config: configparser.ConfigParser) -> None:
bus = config[BUS_KEY][GTK_APP_ID] bus = config[BUS_KEY][GTK_APP_ID]
assert bus == "own" assert bus == "own"
def test_paths(override, xdg_paths, config) -> None: def test_paths(
expect: list[str] = [] override: None, xdg_paths: dict[str, Path], config: configparser.ConfigParser
expect.append(TMP_PATH) ) -> None:
expect: list[Path] = []
expect.append(Path(TMP_PATH))
expect.extend([*xdg_paths.values()]) expect.extend([*xdg_paths.values()])
paths = config[PATHS_KEY][PATHS_SUBKEY].split(";") paths = config[PATHS_KEY][PATHS_SUBKEY].split(";")
assert all(str(path) in paths for path in expect) assert all(str(path) in paths for path in expect)
+2 -1
View File
@@ -2,10 +2,11 @@ import pytest
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.controllers.mc import Controller
from dzgui.views.pages.options import FlatpakToggleField from dzgui.views.pages.options import FlatpakToggleField
class MockController: class MockController(Controller):
def __init__(self) -> None: def __init__(self) -> None:
self.command: str self.command: str
+6 -3
View File
@@ -20,20 +20,23 @@ def get_db_len(path: Path) -> int:
c = conn.cursor() c = conn.cursor()
c.execute("SELECT COUNT(*) from ips") c.execute("SELECT COUNT(*) from ips")
db_len = c.fetchone()[0] db_len = c.fetchone()[0]
return db_len return int(db_len)
@pytest.fixture @pytest.fixture
def csv() -> Path: def csv() -> Path:
csv = Path(tempfile.NamedTemporaryFile().name) csv = Path(tempfile.NamedTemporaryFile().name)
url = find_download_url(DB_IP) url = find_download_url(DB_IP)
if url is None:
raise TypeError
tmp = serialize(url) tmp = serialize(url)
unzip(tmp, csv) unzip(tmp, csv)
strip_ipv6(csv) strip_ipv6(csv)
return csv return csv
@pytest.mark.slow @pytest.mark.slow
def test_stripping(csv): def test_stripping(csv: Path) -> None:
with open(csv, "rb") as f: with open(csv, "rb") as f:
first = f.readline().decode().rstrip() first = f.readline().decode().rstrip()
try: try:
@@ -52,7 +55,7 @@ def test_stripping(csv):
@pytest.mark.slow @pytest.mark.slow
def test_row_parity(csv) -> None: def test_row_parity(csv: Path) -> None:
rows = csv.read_text().splitlines() rows = csv.read_text().splitlines()
db = Path(tempfile.NamedTemporaryFile().name) db = Path(tempfile.NamedTemporaryFile().name)
+9 -7
View File
@@ -1,26 +1,28 @@
import gi
import pytest import pytest
from dzgui.api.servers import Details from dzgui.api.servers import Details
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
from dzgui.views.dialogs.servers import ServerDetailsDialog from dzgui.views.dialogs.servers import ServerDetailsDialog
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk from gi.repository import Gtk
class MockController: class MockController(Controller):
def __init__(self) -> None: def __init__(self) -> None:
self.window = Gtk.Window() pass
def get_window(self) -> Gtk.Window: def get_window(self) -> None: # type: ignore[override]
return self.window return None
def get_server_name(self) -> str: def get_server_name(self) -> str:
return "My server" return "My server"
def get_emitter(self) -> None: def get_emitter(self) -> Emitter:
return None return Emitter()
@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.filterwarnings("ignore::DeprecationWarning")
+1 -1
View File
@@ -15,7 +15,7 @@ from dzgui.views.components.entry import validate_ip_truthy
("192.168.1.101:27016", True), ("192.168.1.101:27016", True),
], ],
) )
def test_ip_entry(ip) -> None: def test_ip_entry(ip: tuple[str, bool]) -> None:
string, state = ip string, state = ip
assert validate_ip_truthy(string) is state assert validate_ip_truthy(string) is state
+25
View File
@@ -0,0 +1,25 @@
from typing import TYPE_CHECKING
from dzgui.init.migrate import clean_legacy_versions_file
if TYPE_CHECKING:
from pathlib import Path
def test_null_signature_stripping(tmp_path: "Path") -> None:
tmp = tmp_path.joinpath("dzg.versions")
lines = [
"1111,2222",
"3333,4444",
"5555,null",
"foo",
",4555",
"1111",
" ",
",,,",
"-1",
]
expect = ["1111,2222", "3333,4444"]
tmp.write_text("\n".join(lines))
clean_legacy_versions_file(tmp)
assert expect == tmp.read_text().splitlines()
+11 -7
View File
@@ -1,27 +1,29 @@
import pytest import pytest
from typing import Any
from dzgui.model.model_factory import FastInsertListStore from dzgui.model.model_factory import FastInsertListStore
@pytest.fixture @pytest.fixture
def heterogeneous_list() -> None: def heterogeneous_list() -> list[list[Any]]:
return [["a", 0, True], ["b", 1, False]] return [["a", 0, True], ["b", 1, False]]
@pytest.fixture @pytest.fixture
def mismatched_length_list() -> None: def mismatched_length_list() -> list[list[Any]]:
return [["a", 0, True], ["b", 1, False, 0]] return [["a", 0, True], ["b", 1, False, 0]]
@pytest.fixture @pytest.fixture
def extended_list(heterogeneous_list) -> None: def extended_list(heterogeneous_list: list[list[Any]]) -> FastInsertListStore:
fs = FastInsertListStore(str, int, bool) fs = FastInsertListStore(str, int, bool)
fs.extend(heterogeneous_list) fs.extend(heterogeneous_list)
return fs return fs
@pytest.fixture @pytest.fixture
def appended_list(heterogeneous_list) -> None: def appended_list(heterogeneous_list: list[list[Any]]) -> FastInsertListStore:
fs = FastInsertListStore(str, int, bool) fs = FastInsertListStore(str, int, bool)
for row in heterogeneous_list: for row in heterogeneous_list:
fs.append(row) fs.append(row)
@@ -29,19 +31,21 @@ def appended_list(heterogeneous_list) -> None:
@pytest.mark.parametrize("target", ["extended_list", "appended_list"]) @pytest.mark.parametrize("target", ["extended_list", "appended_list"])
def test_list_parity(heterogeneous_list, request, target) -> None: def test_list_parity(
heterogeneous_list: list[list[Any]], request: pytest.FixtureRequest, target: str
) -> None:
target = request.getfixturevalue(target) target = request.getfixturevalue(target)
extracted = [[el for el in row] for row in target] extracted = [[el for el in row] for row in target]
assert heterogeneous_list == extracted assert heterogeneous_list == extracted
def test_type_disparity(heterogeneous_list) -> None: def test_type_disparity(heterogeneous_list: list[list[Any]]) -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
fs = FastInsertListStore(str, bool, bool) fs = FastInsertListStore(str, bool, bool)
fs.extend(heterogeneous_list) fs.extend(heterogeneous_list)
def test_length_disparity(mismatched_length_list) -> None: def test_length_disparity(mismatched_length_list: list[list[Any]]) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
fs = FastInsertListStore(str, int, bool) fs = FastInsertListStore(str, int, bool)
fs.extend(mismatched_length_list) fs.extend(mismatched_length_list)
+9 -4
View File
@@ -1,27 +1,32 @@
from typing import TYPE_CHECKING
from dzgui.util import localize from dzgui.util import localize
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
def test_de(monkeypatch):
def test_de(monkeypatch: "MonkeyPatch") -> None:
monkeypatch.setenv("LC_ALL", "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") -> None:
monkeypatch.setenv("LC_ALL", "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"
def test_none(monkeypatch): def test_none(monkeypatch: "MonkeyPatch") -> None:
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_empty(monkeypatch): def test_empty(monkeypatch: "MonkeyPatch") -> None:
monkeypatch.setenv("LC_ALL", "") monkeypatch.setenv("LC_ALL", "")
localize.set_locale() localize.set_locale()
assert localize.number(9900) == "9,900" assert localize.number(9900) == "9,900"
+3 -3
View File
@@ -7,13 +7,13 @@ from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
class RecordsListHandler(logging.Handler): class RecordsListHandler(logging.Handler):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.records_list = [] self.records_list: list[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None: def emit(self, record: logging.LogRecord) -> None:
self.records_list.append(record) self.records_list.append(record)
def pop(self) -> None: def pop(self) -> str:
return self.records_list[-1].msg return str(self.records_list[-1].msg)
@pytest.mark.redact @pytest.mark.redact
+7 -1
View File
@@ -1,6 +1,7 @@
import pytest import pytest
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api.steam import find_user_id from dzgui.api.steam import find_user_id
from dzgui.const.constants import DEFAULT_STEAM_PATH from dzgui.const.constants import DEFAULT_STEAM_PATH
@@ -8,6 +9,9 @@ from tests.fixtures import fixture_path
pytestmark = pytest.mark.apitest pytestmark = pytest.mark.apitest
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fixture, expect", "fixture, expect",
@@ -19,12 +23,14 @@ pytestmark = pytest.mark.apitest
("api/loginusers_beta_client_multiple_2.vdf", 0), ("api/loginusers_beta_client_multiple_2.vdf", 0),
], ],
) )
def test_loginusers(monkeypatch, fixture: str, expect: int) -> None: def test_loginusers(monkeypatch: "MonkeyPatch", fixture: str, expect: int) -> None:
def mock_loginusers(path: Path) -> Path: def mock_loginusers(path: Path) -> Path:
return fixture_path(fixture) return fixture_path(fixture)
monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers) monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers)
uid = find_user_id(Path("")) uid = find_user_id(Path(""))
if uid is None:
raise TypeError
assert int(uid) == expect assert int(uid) == expect
+18 -8
View File
@@ -11,42 +11,52 @@ import dzgui.config.query
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.const.enums import Preferences from _pytest.monkeypatch import MonkeyPatch
from dzgui.const.enum import Preferences
pytestmark = pytest.mark.mods pytestmark = pytest.mark.mods
@pytest.fixture @pytest.fixture
def versions() -> str: def versions() -> "Path":
return fixture_path("dzg.versions") return fixture_path("dzg.versions")
@pytest.fixture @pytest.fixture
def tmp() -> str: def tmp() -> str:
with tempfile.NamedTemporaryFile(delete=False) as f: with tempfile.NamedTemporaryFile(delete=False) as f:
tmp = f.name tmp = f.name
return tmp return tmp
def mock_local_ids(path: Path) -> list[int]: def mock_local_ids(path: Path) -> list[int]:
return [3410710885, 3739934289] return [3410710885, 3739934289]
def mock_lookup(path: Path, prefs: "Preferences") -> str: def mock_lookup(path: Path, prefs: "Preferences") -> str:
return "" return ""
def test_signatures(monkeypatch, tmp: str, versions: str) -> None:
def test_signatures(monkeypatch: "MonkeyPatch", tmp: str, versions: str) -> None:
ids = [1559212036, 1654462998] ids = [1559212036, 1654462998]
shutil.copyfile(versions, tmp) shutil.copyfile(versions, tmp)
path = Path(tmp) path = Path(tmp)
monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup) monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup)
dzgui.api.mods.remove_stale_signatures(path, path, ids) dzgui.api.mods.remove_stale_signatures(path, path, ids)
with open(tmp, "r") as f: with open(tmp, "r") as f:
lines = f.readlines() lines = [line.rstrip() for line in f.readlines()]
assert ids not in lines concat = ",".join(map(str, ids))
assert concat not in lines
def test_signatures_with_no_ids(monkeypatch, tmp: str, versions: str) -> None:
def test_signatures_with_no_ids(
monkeypatch: "MonkeyPatch", tmp: str, versions: str
) -> None:
shutil.copyfile(versions, tmp) shutil.copyfile(versions, tmp)
path = Path(tmp) path = Path(tmp)
monkeypatch.setattr("dzgui.api.mods.get_local_mod_ids", mock_local_ids) monkeypatch.setattr("dzgui.api.mods.get_local_mod_ids", mock_local_ids)
monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup) monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup)
dzgui.api.mods.remove_stale_signatures(path, path) dzgui.api.mods.remove_stale_signatures(path, path)
with open(tmp, "r") as f: with open(tmp, "r") as f:
lines = f.readlines() lines = [line.rstrip() for line in f.readlines()]
assert lines == ["3410710885,1752864732\n", "3739934289,1780855136\n"] assert lines == ["3410710885,1752864732", "3739934289,1780855136"]
+3
View File
@@ -4,10 +4,13 @@ from pathlib import Path
from dzgui.api.mods import tokenize from dzgui.api.mods import tokenize
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
@pytest.mark.mods @pytest.mark.mods
@pytest.mark.parametrize("i", range(1, 7)) @pytest.mark.parametrize("i", range(1, 7))
def test_modmeta(i: int) -> None: def test_modmeta(i: int) -> None:
fixture = fixture_path(f"cpp/mod{i}") fixture = fixture_path(f"cpp/mod{i}")
path = Path(fixture) path = Path(fixture)
meta = tokenize(path) meta = tokenize(path)
if meta is None:
raise TypeError
assert meta["name"] == "ModName" assert meta["name"] == "ModName"
+6 -1
View File
@@ -1,8 +1,13 @@
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.config.xdg import get_xdg_paths from dzgui.config.xdg import get_xdg_paths
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
def test_path_fallback(monkeypatch) -> None:
def test_path_fallback(monkeypatch: "MonkeyPatch") -> None:
envs = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME"] envs = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME"]
for env in envs: for env in envs:
monkeypatch.setenv(env, "/etc/dzgui") monkeypatch.setenv(env, "/etc/dzgui")
+9 -3
View File
@@ -1,8 +1,12 @@
import pytest import pytest
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api.steam import is_dayz_downloading from dzgui.api.steam import is_dayz_downloading
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
@pytest.mark.parametrize( @pytest.mark.parametrize(
"file_list, expect", "file_list, expect",
@@ -16,12 +20,14 @@ from dzgui.api.steam import is_dayz_downloading
(["state_221100_1111"], False), (["state_221100_1111"], False),
], ],
) )
def test_dayz_patch_pending(monkeypatch, file_list: list[str], expect: bool) -> None: def test_dayz_patch_pending(
def mock_get_pending_downloads(steam_path: Path, appid: int) -> None: monkeypatch: "MonkeyPatch", file_list: list[str], expect: bool
) -> None:
def mock_get_pending_downloads(steam_path: Path, appid: int) -> list[Path]:
files = [Path(file) for file in file_list] files = [Path(file) for file in file_list]
return files return files
monkeypatch.setattr( monkeypatch.setattr(
"dzgui.api.steam.get_pending_downloads", mock_get_pending_downloads "dzgui.api.steam.get_pending_downloads", mock_get_pending_downloads
) )
assert is_dayz_downloading(Path("/tmp"), Path("/tmp")) == expect assert is_dayz_downloading(Path("/tmp"), 221100) == expect
+19 -11
View File
@@ -1,13 +1,14 @@
import pytest import pytest
from pathlib import Path
from pathlib import Path
from typing import TYPE_CHECKING
import dzgui.api.pefile as PeFile import dzgui.api.pefile as PeFile
from dzgui.api.steam import ( from dzgui.api.steam import (
VDFLoadError, VDFLoadError,
AppNotInstalledError, AppNotInstalledError,
AppMovedError, AppMovedError,
get_app_path get_app_path,
) )
from dzgui.config.query import get_config from dzgui.config.query import get_config
@@ -16,20 +17,22 @@ from dzgui.const.constants import APPID_DAYZ
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
pytestmark = pytest.mark.apitest pytestmark = pytest.mark.apitest
@pytest.fixture @pytest.fixture
def default_steam_path(): def default_steam_path() -> str:
paths = get_xdg_paths() paths = get_xdg_paths()
xdg = parse_filepaths(paths) xdg = parse_filepaths(paths)
conf = get_config(xdg.config) conf = get_config(xdg.config)
return conf["default_steam_path"] return str(conf["default_steam_path"])
# TODO: additional parsing tests
# TODO: split into separate test file def test_pefile_length(default_steam_path: str) -> None:
# FIXME: use fixtures
def test_pefile_length(default_steam_path):
try: try:
pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ) pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ)
vers = PeFile.get_dayz_version(pe_file_path) vers = PeFile.get_dayz_version(pe_file_path)
@@ -39,7 +42,7 @@ def test_pefile_length(default_steam_path):
assert len(st) == 3 assert len(st) == 3
def test_invalid_pefile_path(): def test_invalid_pefile_path() -> None:
with pytest.raises(VDFLoadError): with pytest.raises(VDFLoadError):
try: try:
PeFile.get_pefile_path(Path("/not/a/path"), APPID_DAYZ) PeFile.get_pefile_path(Path("/not/a/path"), APPID_DAYZ)
@@ -47,6 +50,7 @@ def test_invalid_pefile_path():
raise e raise e
# TODO: split into separate test file
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fixture, exception", "fixture, exception",
[ [
@@ -55,8 +59,11 @@ def test_invalid_pefile_path():
("malformed.vdf", VDFLoadError), ("malformed.vdf", VDFLoadError),
], ],
) )
def test_not_in_library(monkeypatch, fixture, exception): def test_not_in_library(
monkeypatch: "MonkeyPatch", fixture: str, exception: type[Exception]
) -> None:
import dzgui.api.steam as steam import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture) monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture)
folder_path = Path(fixture_path("api")) folder_path = Path(fixture_path("api"))
@@ -68,12 +75,13 @@ def test_not_in_library(monkeypatch, fixture, exception):
@pytest.fixture @pytest.fixture
def second_drive(): def second_drive() -> str:
return "in_library_on_second_drive.vdf" return "in_library_on_second_drive.vdf"
def test_on_second_drive(monkeypatch, second_drive): def test_on_second_drive(monkeypatch: "MonkeyPatch", second_drive: str) -> None:
import dzgui.api.steam as steam import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", second_drive) monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", second_drive)
folder_path = Path(fixture_path("api")) folder_path = Path(fixture_path("api"))
path = get_app_path(folder_path, APPID_DAYZ) path = get_app_path(folder_path, APPID_DAYZ)
+9 -7
View File
@@ -1,6 +1,8 @@
import pytest import pytest
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from typing import Any
from dzgui.const.constants import ( from dzgui.const.constants import (
FLATPAK_APPID, FLATPAK_APPID,
@@ -14,9 +16,9 @@ pytestmark = pytest.mark.flatpak
class TestSteamDetection(unittest.TestCase): class TestSteamDetection(unittest.TestCase):
def test_flatpak_detection_with_multiple_running_apps(self): def test_flatpak_detection_with_multiple_running_apps(self) -> None:
def flatpak_output(args, *, text): def flatpak_output(args: Any, *, text: str) -> str:
# Default `flatpak ps` includes instance, PID, app and runtime columns. # Default 'flatpak ps' includes instance, PID, app and runtime columns.
if args == ["flatpak", "ps", "--columns=application"]: if args == ["flatpak", "ps", "--columns=application"]:
return f"com.spotify.Client\n{FLATPAK_APPID}\n{FLATPAK_APPID}\n" return f"com.spotify.Client\n{FLATPAK_APPID}\n{FLATPAK_APPID}\n"
return f"3904921611\t1257689\t{FLATPAK_APPID}\torg.freedesktop.Platform\n" return f"3904921611\t1257689\t{FLATPAK_APPID}\torg.freedesktop.Platform\n"
@@ -29,7 +31,7 @@ class TestSteamDetection(unittest.TestCase):
with self.subTest(client=client): with self.subTest(client=client):
self.assertTrue(proc.is_steam_running(client)) self.assertTrue(proc.is_steam_running(client))
def test_flatpak_not_running(self): def test_flatpak_not_running(self) -> None:
with patch.object(proc, "has_cmd", return_value=True): with patch.object(proc, "has_cmd", return_value=True):
for output in ("", "com.spotify.Client\n", f"{FLATPAK_APPID}Helper\n"): for output in ("", "com.spotify.Client\n", f"{FLATPAK_APPID}Helper\n"):
with ( with (
@@ -38,7 +40,7 @@ class TestSteamDetection(unittest.TestCase):
): ):
self.assertFalse(proc.is_flatpak_steam_running()) self.assertFalse(proc.is_flatpak_steam_running())
def test_flatpak_not_installed(self): def test_flatpak_not_installed(self) -> None:
with ( with (
patch.object(proc, "has_cmd", return_value=False), patch.object(proc, "has_cmd", return_value=False),
patch.object(proc.subprocess, "check_output") as check_output, patch.object(proc.subprocess, "check_output") as check_output,
@@ -46,7 +48,7 @@ class TestSteamDetection(unittest.TestCase):
self.assertFalse(proc.is_flatpak_steam_running()) self.assertFalse(proc.is_flatpak_steam_running())
check_output.assert_not_called() check_output.assert_not_called()
def test_native_steam(self): def test_native_steam(self) -> None:
with patch.object(proc, "has_cmd", return_value=True): with patch.object(proc, "has_cmd", return_value=True):
for output, expected in ( for output, expected in (
( (
@@ -73,6 +75,6 @@ class TestSteamDetection(unittest.TestCase):
): ):
self.assertEqual(proc.is_native_steam_running(), expected) self.assertEqual(proc.is_native_steam_running(), expected)
def test_invalid_client(self): def test_invalid_client(self) -> None:
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
proc.is_steam_running("invalid-client") proc.is_steam_running("invalid-client")
+6 -4
View File
@@ -1,23 +1,25 @@
import pytest import pytest
from typing import Any
from dzgui.api import probe
from dzgui.config.query import get_config from dzgui.config.query import get_config
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.api import probe
pytestmark = pytest.mark.webtest pytestmark = pytest.mark.webtest
@pytest.fixture @pytest.fixture
def config(): def config() -> Any:
paths = get_xdg_paths() paths = get_xdg_paths()
xdg = parse_filepaths(paths) xdg = parse_filepaths(paths)
return get_config(xdg.config) return get_config(xdg.config)
def test_ipdb(): def test_ipdb() -> None:
assert probe.test_ipdb() assert probe.test_ipdb()
def test_steam(config): def test_steam(config: Any) -> None:
key = config["steam_api"] key = config["steam_api"]
assert probe.test_steam_api(key) assert probe.test_steam_api(key)
+27
View File
@@ -0,0 +1,27 @@
from dzgui.model.model_factory import ModelFactory
from dzgui.model.proxy_model import ServerRow
def test_row_conversion() -> None:
"""Converted FastInsertListStore is expected to have identical elements"""
data = [
"Test Server",
"chernarusplus",
"1PP",
"05:00",
10,
10,
0,
"127.0.0.0:27016",
27017,
99,
"Unofficial",
False,
]
rows = [ServerRow(*data)] # type: ignore
marshalled = ServerRow.marshal_rows(rows)
store = ModelFactory().make_server_store()
store.extend(marshalled)
cols = store.get_n_columns()
converted = [[row[i] for i in range(cols)] for row in store]
assert data == converted[0]
+10 -4
View File
@@ -3,9 +3,11 @@ import re
from dzgui.api.servers import Details, Record, get_details from dzgui.api.servers import Details, Record, get_details
pytestmark = pytest.mark.slow
@pytest.fixture @pytest.fixture
def details() -> Details: def details() -> Details | None:
""" """
Static official server, metadata is expected to be invariate Static official server, metadata is expected to be invariate
""" """
@@ -19,17 +21,21 @@ def test_invalid_server() -> None:
@pytest.mark.parametrize("key, expect", [("day_accel", 2.3), ("night_accel", 6.8)]) @pytest.mark.parametrize("key, expect", [("day_accel", 2.3), ("night_accel", 6.8)])
def test_time_accel(details, key, expect) -> None: def test_time_accel(details: Details | None, key: str, expect: float) -> None:
assert getattr(details, key) == expect assert getattr(details, key) == expect
def test_gametime(details) -> None: def test_gametime(details: Details | None) -> None:
time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)" time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)"
if details is None:
raise TypeError
match = re.match(time_reg, details.gametime) match = re.match(time_reg, details.gametime)
assert match is not None assert match is not None
def test_metadata(details) -> None: def test_metadata(details: Details | None) -> None:
if details is None:
raise TypeError
for row in details.data: for row in details.data:
label, value = row label, value = row
match label: match label:
+18 -17
View File
@@ -1,9 +1,9 @@
import pytest import pytest
from hypothesis import given, strategies as st
from datetime import datetime, timedelta from datetime import datetime, timedelta
from hypothesis import given, strategies as st
from dzgui.views.dialogs.calc import ( from dzgui.views.dialogs.calc import (
Emitter,
LocalClock, LocalClock,
ServerClock, ServerClock,
Time, Time,
@@ -13,31 +13,28 @@ from dzgui.views.dialogs.calc import (
) )
class DummyEmitter:
def __init__(self) -> None:
pass
def connect(self, _, __) -> None:
pass
@pytest.fixture(params=[("05:00", 10.0), ("15:30", 5.0)]) @pytest.fixture(params=[("05:00", 10.0), ("15:30", 5.0)])
def accel_server_clock(request) -> None: def accel_server_clock(request: pytest.FixtureRequest) -> tuple[str, ServerClock]:
clock_time, expect = request.param clock_time, expect = request.param
cur_time = Time(clock_time, 5.0, 2.0) cur_time = Time(clock_time, 5.0, 2.0)
emitter = DummyEmitter() emitter = Emitter()
local_clock = LocalClock(emitter) local_clock = LocalClock(emitter)
server_clock = ServerClock(cur_time, emitter, local_clock) server_clock = ServerClock(cur_time, emitter, local_clock)
return expect, server_clock return expect, server_clock
def test_accel_factor(accel_server_clock) -> None: def test_accel_factor(accel_server_clock: tuple[float, ServerClock]) -> None:
"""Static test checking times at known boundaries""" """Static test checking times at known boundaries"""
expect, clock = accel_server_clock expect, clock = accel_server_clock
assert clock.get_accel_factor(clock.cur_time) == expect assert clock.get_accel_factor(clock.cur_time) == expect
def reference_calc(start, end, day_factor, night_factor): def reference_calc(
start: datetime,
end: datetime,
day_factor: float,
night_factor: float,
) -> timedelta:
""" """
Reference implementation to test validity of UI-side calculator Reference implementation to test validity of UI-side calculator
Aggregates total seconds and applies accel factors after Aggregates total seconds and applies accel factors after
@@ -80,7 +77,7 @@ night_accel = st.floats(
@st.composite @st.composite
def time_ranges(draw): def time_ranges(draw: st.DrawFn) -> tuple[datetime, datetime]:
start_time = draw(st.times()) start_time = draw(st.times())
deltas = st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=1)) deltas = st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=1))
duration = draw(deltas) duration = draw(deltas)
@@ -91,12 +88,16 @@ def time_ranges(draw):
@given(span=time_ranges(), day_factor=day_accel, night_factor=night_accel) @given(span=time_ranges(), day_factor=day_accel, night_factor=night_accel)
def test_time_aggregation(span, day_factor, night_factor) -> None: def test_time_aggregation(
span: tuple[datetime, datetime],
day_factor: float,
night_factor: float,
) -> None:
start, end = span start, end = span
strtime = start.strftime("%H:%M") strtime = start.strftime("%H:%M")
t = Time(strtime, day_factor, night_factor) t = Time(strtime, day_factor, night_factor)
emitter = DummyEmitter() emitter = Emitter()
local_clock = LocalClock(emitter) local_clock = LocalClock(emitter)
server_clock = ServerClock(t, emitter, local_clock) server_clock = ServerClock(t, emitter, local_clock)
h = end.time().hour h = end.time().hour
+41 -37
View File
@@ -1,91 +1,95 @@
import pytest import pytest
import tempfile import tempfile
from pathlib import Path
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from collections.abc import Iterator
from pathlib import Path
from typing import Any, cast, TYPE_CHECKING
from dzgui.api.shortcuts import Shortcuts from dzgui.api.shortcuts import Shortcuts
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
pytestmark = pytest.mark.apitest pytestmark = pytest.mark.apitest
def mock_find_shortcuts(self, steam_path: Path) -> Path: def mock_find_shortcuts(self: Shortcuts, steam_path: Path) -> Path:
return Path(fixture_path("api/no_shortcuts.vdf")) return Path(fixture_path("api/no_shortcuts.vdf"))
@pytest.fixture(scope="module", autouse=True) @pytest.fixture(scope="module", autouse=True)
def patch_api() -> None: def patch_api() -> Iterator[None]:
mp = MonkeyPatch() mp = MonkeyPatch()
mp.setattr("dzgui.api.shortcuts.Shortcuts.find_shortcuts_path", mock_find_shortcuts) mp.setattr("dzgui.api.shortcuts.Shortcuts.find_shortcuts_path", mock_find_shortcuts)
yield yield
mp.undo() mp.undo()
def test_no_shortcuts(monkeypatch) -> None: def test_no_shortcuts(monkeypatch: "MonkeyPatch") -> None:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
assert len(s.shortcuts["shortcuts"]) == 0 assert len(s.shortcuts["shortcuts"]) == 0
@pytest.fixture @pytest.fixture
def dummy_app() -> None: def dummy_app() -> dict[str, Any]:
d = { d = {
"AppName": "TEST APP", "AppName": "TEST APP",
"StartDir": "TEST_DIR", "StartDir": Path("TEST_DIR"),
"Exe": "TEST_DIR/TEST_EXE.EXE", "Exe": Path("TEST_DIR/TEST_EXE.EXE"),
"icon": "IMAGES_DIR/TEST_IMAGE.PNG", "icon": Path("IMAGES_DIR/TEST_IMAGE.PNG"),
} }
return d return d
def test_wrap_exe(dummy_app: dict[str, str]) -> None: @pytest.fixture
def shortcuts(dummy_app: dict[str, Any]) -> Shortcuts:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values()) args = cast(tuple[str, Path, Path, Path], dummy_app.values())
assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"' s.add_shortcut(*args)
assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"' return s
def test_add_shortcut(dummy_app: dict[str, str]) -> None: def test_wrap_exe(shortcuts: Shortcuts) -> None:
s = Shortcuts(Path("")) assert shortcuts.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
s.add_shortcut(*dummy_app.values()) assert shortcuts.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
new = s.shortcuts["shortcuts"]
def test_add_shortcut(shortcuts: Shortcuts, dummy_app: dict[str, Any]) -> None:
new = shortcuts.shortcuts["shortcuts"]
ind = str(len(new) - 1) ind = str(len(new) - 1)
for k, v in dummy_app.items(): for k, v in dummy_app.items():
if k == "Exe": if k == "Exe":
v = f'"{v}"' v = f'"{v}"'
assert new[ind][k] == v assert new[ind][k] == str(v)
def test_save_shortcut(dummy_app: dict[str, str]) -> None: def test_save_shortcut(shortcuts: Shortcuts) -> None:
s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values())
with tempfile.NamedTemporaryFile() as f: with tempfile.NamedTemporaryFile() as f:
tmp = Path(f.name) tmp = Path(f.name)
s.shortcuts_path = tmp shortcuts.shortcuts_path = tmp
s.save_shortcuts() shortcuts.save_shortcuts()
s._load_shortcuts(tmp) shortcuts._load_shortcuts(tmp)
assert len(s.shortcuts["shortcuts"]) == 1 assert len(shortcuts.shortcuts["shortcuts"]) == 1
def test_shortcut_crc(dummy_app: dict[str, str]) -> None: def test_shortcut_crc(shortcuts: Shortcuts) -> None:
s = Shortcuts(Path("")) for key in shortcuts.shortcuts["shortcuts"].keys():
s.add_shortcut(*dummy_app.values()) entry = shortcuts.shortcuts["shortcuts"][key]
for key in s.shortcuts["shortcuts"].keys():
entry = s.shortcuts["shortcuts"][key]
name = entry["AppName"] name = entry["AppName"]
exe = entry["Exe"] exe = entry["Exe"]
uid = name + exe uid = name + exe
bpid = s.gen_bpid(uid) bpid = shortcuts.gen_bpid(uid)
assert entry["appid"] & 0xFFFFFFFF == bpid assert entry["appid"] & 0xFFFFFFFF == bpid
def test_reverse_crc(dummy_app: dict[str, str]) -> None: def test_reverse_crc(shortcuts: Shortcuts) -> None:
s = Shortcuts(Path("")) for key in shortcuts.shortcuts["shortcuts"].keys():
s.add_shortcut(*dummy_app.values()) entry = shortcuts.shortcuts["shortcuts"][key]
for key in s.shortcuts["shortcuts"].keys():
entry = s.shortcuts["shortcuts"][key]
name = entry["AppName"] name = entry["AppName"]
exe = entry["Exe"] exe = entry["Exe"]
uid = name + exe uid = name + exe
bpid = s.gen_bpid(uid) bpid = shortcuts.gen_bpid(uid)
assert s.find_appname_by_unsigned_id(bpid) == name assert shortcuts.find_appname_by_unsigned_id(bpid) == name
+2 -1
View File
@@ -2,7 +2,8 @@ import re
from dzgui.model.proxy_model import DAY_REG, NIGHT_REG from dzgui.model.proxy_model import DAY_REG, NIGHT_REG
def iterate(h: str, r: str) -> None:
def iterate(h: int, r: str) -> None:
for m in range(60): for m in range(60):
time = f"{h:02}:{m:02}" time = f"{h:02}:{m:02}"
assert re.match(r, time) is not None assert re.match(r, time) is not None