Compare commits

...
254 Commits
Author SHA1 Message Date
aclist cab10efc96 Merge pull request #367 from aclist/prerelease/7.0.0b12
Prerelease/7.0.0b12
2026-06-17 22:22:04 +09:00
aclist 0a4efba905 docs: add user to thanks 2026-06-17 22:16:00 +09:00
aclist 1e4e09e52c docs: update changelog 2026-06-17 22:14:56 +09:00
aclist 910c068109 chore: bump version 2026-06-17 22:14:30 +09:00
aclist 1dd773a9bf Merge pull request #366 from aclist/fix/validate-offline-mods
fix: validate custom folders before launch
2026-06-17 22:13:56 +09:00
aclist f4d065d766 fix: typo 2026-06-17 22:13:17 +09:00
aclist 1fe66076c6 fix: validate custom folders before launch 2026-06-17 22:12:12 +09:00
aclist 668ec1d7a0 Merge pull request #365 from aclist/fix/unsub
Fix/unsub
2026-06-17 21:43:11 +09:00
aclist 412f0d3193 chore: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-17 21:42:02 +09:00
aclist 894a9b106d chore: move HBox, VBox classes 2026-06-17 21:41:34 +09:00
aclist 1b0a9c9a69 chore: drop foreground logic 2026-06-17 21:33:49 +09:00
aclist c4818d2097 fix: name collision between function signature and variable 2026-06-17 21:33:28 +09:00
aclist 69135150c7 chore: add deprecaiton warning
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-17 03:16:44 +09:00
aclist 4d8949427b feat: acf parser 2026-06-17 03:15:57 +09:00
aclist 0139f6bfba fix: cast str to int 2026-06-17 02:57:50 +09:00
aclist 8d77d46eda feat: unsubscribe from mods gracefully 2026-06-17 02:56:26 +09:00
aclist 179f92234a chore: clear typehinting errors 2026-06-17 02:55:13 +09:00
aclist 650a5ecd7b chore: drop unused imports 2026-06-17 02:54:50 +09:00
aclist 8cebb362e9 chore: clear typehinting errors 2026-06-17 02:53:32 +09:00
aclist bfd1d3e0f9 change: move workshop button to mods page 2026-06-17 02:52:57 +09:00
aclist e8b18d83a5 Merge pull request #354 from aclist/feat/launch-offline
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: launch offline
2026-06-14 22:03:17 +09:00
aclist d305f47bb9 Merge branch 'dzgui7' into feat/launch-offline
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-14 22:03:02 +09:00
aclist 43f6c17c4b chore: port changes from dzgui7 branch 2026-06-14 22:02:21 +09:00
aclist 38bfef3836 chore: clear typehinting errors 2026-06-14 21:56:49 +09:00
aclist c818945a28 chore: add comment 2026-06-14 21:54:59 +09:00
aclist 5cb4f69500 fix: initialize string 2026-06-14 19:45:48 +09:00
aclist cee9052dea fix: append store to mod manager 2026-06-14 19:31:52 +09:00
aclist b3b98aa407 fix: parse parent dir 2026-06-14 19:11:35 +09:00
aclist b8230cbf43 feat: make symlink to mission path
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-14 01:54:08 +09:00
aclist 4b16edbbee fix: grab focus on unmap
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-13 04:14:28 +09:00
aclist 966d935032 fix: allow loading with only mission file 2026-06-13 03:52:20 +09:00
aclist 8b8c6268ab feat: expunge ephemeral symlinks 2026-06-13 03:36:59 +09:00
aclist 513c445b24 fix: restore statusbar when returning 2026-06-13 03:06:55 +09:00
aclist 55f82c2e91 chore: drop comment 2026-06-13 03:06:41 +09:00
aclist ad589ba850 fix: grab focus after popup 2026-06-13 03:06:19 +09:00
aclist 85f6fa053d fix: disable context menu on custom mod tree 2026-06-13 02:10:41 +09:00
aclist f7a42e2cdb fix: clear typehinting errors 2026-06-13 02:03:00 +09:00
aclist e1cb682e74 feat: custom symlink creation in thread 2026-06-13 01:50:34 +09:00
aclist a8b7ea5008 chore: clear type annotation errors
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-11 19:25:08 +09:00
aclist 6a6b4c8027 chore: drop extraneous setting 2026-06-11 19:22:03 +09:00
aclist c2ef7449e5 fix: override ctrl-click signal 2026-06-11 19:21:33 +09:00
aclist 96fb484b38 feat: add context aware multi select 2026-06-11 19:20:02 +09:00
aclist 1e6ff9c4f7 fix: set filepicker title 2026-06-11 19:19:36 +09:00
aclist 4313075b61 feat: left aligned icon 2026-06-11 18:02:58 +09:00
aclist 88270382f1 fix: signal arguments 2026-06-11 17:46:52 +09:00
aclist ad85bb7343 drop collision check for custom mods
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
Mod hashes are taken from the directory names, which cannot be the same
at the filesystem level. Custom mod hashes are also unique from local
mods because they are given a different namespace.
2026-06-11 16:17:02 +09:00
aclist 77df66979a chore: cleanup 2026-06-11 15:40:09 +09:00
aclist d4825390f7 chore: update strings 2026-06-11 15:39:52 +09:00
aclist d5a857fe3a fix: parse tree iters correctly 2026-06-11 15:31:49 +09:00
aclist 241df72bf3 fix: pass custom flag 2026-06-11 15:31:17 +09:00
aclist 4f51d962e7 fix: parse filepath correctly 2026-06-11 15:15:43 +09:00
aclist 98b550cec6 chore: dedicated mission frame 2026-06-11 15:13:09 +09:00
aclist 20c850a977 chore: drop print statement 2026-06-11 15:12:13 +09:00
aclist 5a8d85ac92 fix: less brittle meta parsing 2026-06-11 15:11:38 +09:00
aclist 450d54d7fd fix: swap logic 2026-06-11 12:37:27 +09:00
aclist 65fb07c8da chore: clear type annotation errors 2026-06-11 12:10:37 +09:00
aclist 8123893a90 chore: clean up offline mod manager 2026-06-11 12:03:44 +09:00
aclist 3c9b5c9ed3 chore: update strings 2026-06-11 12:03:22 +09:00
aclist f9be6dfae4 chore: drop unused method 2026-06-11 12:02:44 +09:00
aclist 586f7e6c66 feat: parse mission and mods 2026-06-11 12:02:28 +09:00
aclist 49eb3caf0a fix: handle empty store 2026-06-11 12:02:05 +09:00
aclist 56da5717fa feat: add emitter signal 2026-06-11 12:01:32 +09:00
aclist 9983e91526 feat: test mission integrity 2026-06-11 12:01:17 +09:00
aclist 820deb2955 fix: incorrect error state 2026-06-11 10:52:49 +09:00
aclist 0137bf4610 fix: process escape key 2026-06-11 10:47:32 +09:00
aclist 687b1a915e feat: capture escape signal in BootDialog 2026-06-11 10:43:52 +09:00
aclist 401c8625ad chore: add placeholder notes
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-11 08:07:58 +09:00
aclist 8503d7e698 feat: namespace mod hashes 2026-06-11 08:02:27 +09:00
aclist b35b6148bc change: tweak expansion properties 2026-06-11 07:51:16 +09:00
aclist 45fd180dcb fix: accept emitter as arg
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 19:23:14 +09:00
aclist 486da7749a fix: emitter arguments 2026-06-10 18:37:31 +09:00
aclist 606df3423c change: delegate early checks to BootWindow 2026-06-10 15:59:44 +09:00
aclist ab439144ea fix: use StoredFunc
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 15:21:04 +09:00
aclist db14c5252c change: clearer startup error 2026-06-10 15:18:42 +09:00
aclist 7d8b3b402f feat: use emitter 2026-06-10 14:30:50 +09:00
aclist 4506a0cfda chore: revert IconButton alignment changes 2026-06-10 08:27:24 +09:00
aclist 978edc5f5f chore: add format strings 2026-06-10 08:26:50 +09:00
aclist 2466887014 chore: drop print statement
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 06:45:51 +09:00
aclist 6050a370f8 feat: folder icons 2026-06-10 06:21:12 +09:00
aclist d4a99f8229 feat: prepare mods in manager (WIP) 2026-06-10 06:15:27 +09:00
aclist 46fba4eef1 chore: clear typehinting errors 2026-06-10 05:45:08 +09:00
aclist bc1118a29f feat: OfflineManager 2026-06-10 05:27:57 +09:00
aclist 775f582ea6 fix: drop extraneous arg 2026-06-10 05:27:39 +09:00
aclist 2dadd70be3 fix: pack tree and statusbar in vbox 2026-06-10 05:05:22 +09:00
aclist 3473f03350 fix: pad inner scrollbar 2026-06-10 04:51:05 +09:00
aclist e081d0ad16 feat: collect appid on toggle 2026-06-09 03:09:34 +09:00
aclist e6026e84f9 feat: launch offline (WIP) 2026-06-09 02:37:50 +09:00
aclist b17b8c24ff fix: fill model inside thread 2026-06-08 19:27:40 +09:00
aclist 20dab38fff Merge pull request #351 from aclist/fix/enqueue-mods
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: enqueue mods
2026-06-07 21:59:49 +09:00
aclist 1ddc4a5ff1 chore: bump version 2026-06-07 21:58:44 +09:00
aclist d605447fca chore: clear typehinting errors 2026-06-07 21:51:55 +09:00
aclist 94fe3f9dff fix: suppress search popup (#353) 2026-06-07 21:49:41 +09:00
aclist a966993999 fix: invoke steam client directly (#348) 2026-06-07 21:21:49 +09:00
aclist 8ee3b36816 fix: restore mod statusbar when returning from other pages 2026-06-07 21:20:56 +09:00
aclist 3af53775cd fix: grab focus on tree 2026-06-07 21:20:32 +09:00
aclist e2d6a462db fix: suppress headers if no model is loaded 2026-06-07 20:23:13 +09:00
aclist 4b9687a84d chore: clear typehinting errors 2026-06-07 20:22:32 +09:00
aclist 0c0f280d15 docs: update changelog 2026-06-07 20:18:27 +09:00
aclist 831bd9145d feat: launch offline (WIP)) 2026-06-07 20:17:58 +09:00
aclist 0070fe4061 fix: suppress headers if no model is loaded 2026-06-07 20:17:32 +09:00
aclist 9d1443cf65 feat: launch offline (WIP) 2026-06-07 20:15:30 +09:00
aclist c4cc9f04c4 Merge pull request #347 from aclist/chore/clean-statusbar
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: drop unused code from statusbar
2026-06-06 17:56:03 +09:00
aclist bcd59162b1 chore: drop unused code from statusbar 2026-06-06 17:55:05 +09:00
aclist 2f0bac75ac Merge pull request #346 from aclist/fix/permit-filters
fix: permit legacy filters
2026-06-06 17:53:19 +09:00
aclist 9191f43c8e chore: drop comment 2026-06-06 17:50:53 +09:00
aclist 2419fedd19 chore: format exceptions with type 2026-06-06 17:50:32 +09:00
aclist 83a7996189 The filter panel previously did not pack filter labels homogeneously, so
the width of the "Unofficial" label was being manually constrained by
abbreviating it. Labels auto ellipsize and adjust now, so the full name
is used. The filter file check is also allowed to fail if the user's
filter preferences file contains old key names.
2026-06-06 17:39:49 +09:00
aclist 9d3aa8d9f5 Merge pull request #344 from aclist/fix/stringify-exceptions
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: stringify Exception when passing to dialog
2026-06-06 02:31:09 +09:00
aclist 0de0dd4a03 fix: stringify Exception when passing to dialog 2026-06-06 02:29:09 +09:00
aclist aeefc830cd Merge pull request #343 from aclist/prelease/7.0.0b10
chore: bump version
2026-06-06 02:05:26 +09:00
aclist 647f75cde3 chore: bump version 2026-06-06 02:03:45 +09:00
aclist 7e8b8eb7f7 Merge pull request #341 from aclist/fix/7.0.0b10-hotfix
fix: extract tarfile correctly
2026-06-06 02:02:51 +09:00
aclist 79af78a7e0 fix: extract tarfile correctly 2026-06-06 02:00:15 +09:00
aclist a8e374cd74 Merge pull request #340 from aclist/prerelease/7.0.0b9
chore: bump version
2026-06-06 01:20:41 +09:00
aclist 6850f3d1fa chore: bump version 2026-06-06 01:19:48 +09:00
aclist 721f0d0960 Merge pull request #339 from aclist/chore/update-strings
chore: update strings
2026-06-06 01:18:17 +09:00
aclist af39b6c272 fix: signals not attached to mod panel buttons 2026-06-06 01:14:58 +09:00
aclist 1cdd37c2a6 chore: add dialog strings 2026-06-06 01:14:29 +09:00
aclist 7f19821433 docs: add DZGUI 6 changelog details 2026-06-06 01:11:52 +09:00
aclist 3b69d13817 Merge pull request #337 from aclist/fix/anchor-buttons
fix: wrap main content area in ScrolledWindow
2026-06-06 01:00:04 +09:00
aclist 44b14b28ae feat: add docking button 2026-06-06 00:58:55 +09:00
aclist 2efa216b00 fix: wrap main content area in ScrolledWindow 2026-06-06 00:27:01 +09:00
aclist adf498fcb5 Merge pull request #331 from aclist/fix/early-cancel
fix: check cancel event in loop prior to mod directory creation
2026-06-05 22:42:55 +09:00
aclist 9fa2840d1b chore: abstract cancel check function to controller
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-05 22:39:13 +09:00
aclist 549d441179 Merge pull request #336 from aclist/fix/release-dir
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: use default build dir for caching
2026-06-05 20:29:35 +09:00
aclist 52f3ec4570 fix: use default build dir for caching 2026-06-05 20:28:37 +09:00
aclist 14752440d1 Merge pull request #335 from aclist/fix/libgi-check
fix: make debian check non-mandatory
2026-06-05 19:36:06 +09:00
aclist b05c353914 fix: make debian check non-mandatory 2026-06-05 19:35:31 +09:00
aclist b21b7c5a5d Merge pull request #329 from aclist/chore/musl-build
chore: build musl release target
2026-06-05 19:32:02 +09:00
aclist 1b626130d5 Merge pull request #330 from aclist/feat/libgi-check
feat: check for missing local library
2026-06-05 19:21:17 +09:00
aclist c080847efc docs: mention atomic dpendency flags
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-05 18:56:19 +09:00
aclist 40e244e05a docs: update build instructions 2026-06-05 18:36:41 +09:00
aclist b615869278 fix: check cancel event in loop prior to mod directory creation 2026-06-04 19:45:13 +09:00
aclist a1ef96094f feat: check for missing local library 2026-06-04 19:41:43 +09:00
aclist 8edb38f175 fix: use correct platform compat tags
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-04 19:37:49 +09:00
aclist 485d0721da Merge branch 'dzgui7' into chore/musl-build 2026-06-04 19:32:58 +09:00
aclist 5b90068fe4 chore: wrap line 2026-06-04 19:26:53 +09:00
aclist 58a2bf0bfa Merge pull request #328 from aclist/docs/licenses
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
Docs/licenses
2026-06-04 19:24:25 +09:00
aclist 88ebbf4b49 chore: remove old shell script
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-04 19:23:19 +09:00
aclist 3f6c7008ae chore: drop unused import 2026-06-04 19:22:59 +09:00
aclist 63a853af7c chore: update build flags 2026-06-04 14:12:09 +09:00
aclist 1bbc99100a fix: use tarfile filter 2026-06-04 13:51:19 +09:00
aclist 41b0e86fda docs: update DEVELOPERS.md 2026-06-03 19:35:30 +09:00
aclist 460b07a4c5 chore: update license concat script 2026-06-03 19:26:31 +09:00
aclist abdfd2e995 feat: pack licenses into tarball (WIP) 2026-06-03 19:23:36 +09:00
aclist fa486c2b76 chore: build musl release target 2026-06-03 18:20:35 +09:00
aclist 5776f4c49f feat: parse toml entrypoint 2026-06-03 16:01:15 +09:00
aclist 48cb062d90 fix: typo 2026-06-03 15:49:19 +09:00
aclist 3b35d75b72 chore: restructure as class 2026-06-03 15:44:55 +09:00
aclist 8eb0a17b78 Merge pull request #327 from aclist/docs/dzgui7-onboarding
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
docs: add dzgui7 onboarding page
2026-06-03 15:17:51 +09:00
aclist ad1598ee0d chore: fix typo 2026-06-03 15:16:53 +09:00
aclist f4b653e9dd docs: concatenate licenses 2026-06-03 15:12:48 +09:00
aclist bddf792280 docs: drop suffix 2026-06-03 15:08:57 +09:00
aclist 3e3a86f985 docs: add vendored licenses 2026-06-03 15:06:48 +09:00
aclist 9e2d528436 docs: add dzgui7 onboarding page 2026-06-03 14:01:38 +09:00
aclist dac02d75f6 Merge pull request #319 from aclist/feat/test-writeable
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: disallow updates if binary path is not writeable
2026-05-29 01:09:25 +09:00
aclist 3a5c061dbf chore: update docstring 2026-05-29 01:03:29 +09:00
aclist 3851b76ca0 docs: update changelog 2026-05-29 00:44:35 +09:00
aclist 387930b147 chore: update strings file 2026-05-29 00:44:17 +09:00
aclist c0c3ee8c64 fix: catch error if no GTK theme is available (headless installs) 2026-05-29 00:44:00 +09:00
aclist 1212c09110 feat: add tooltip to load menu button 2026-05-29 00:43:34 +09:00
aclist 86e2249a23 change: enable empty/full filters on first run of LAN page 2026-05-29 00:43:01 +09:00
aclist 302a43e65e change: enable empty/full filters on first run of LAN page 2026-05-29 00:42:43 +09:00
aclist 3dc58c1a69 fix: abort if deps could not be installed in cpython distribution 2026-05-29 00:41:47 +09:00
aclist 9af2145ee8 fix: use dedicated LAN error message 2026-05-29 00:41:24 +09:00
aclist 7b4382b178 chore: bump version 2026-05-28 05:52:46 +09:00
aclist ea3631072b chore: add typehint ignore 2026-05-28 05:49:16 +09:00
aclist 67bae3c3e0 chore: uncomment connection logic 2026-05-28 05:48:25 +09:00
aclist 1ea30270ce fix: break after 10th record 2026-05-28 05:47:59 +09:00
aclist 2c9b36ba84 fix: append history queue (WIP) 2026-05-28 05:24:23 +09:00
aclist fd9eb33714 feat: load direct to menu 2026-05-28 03:34:11 +09:00
aclist 15d928026a feat: add file module 2026-05-28 02:41:41 +09:00
aclist 24d3bc8209 chore: drop comment 2026-05-28 02:41:26 +09:00
aclist 881847cee9 feat: disallow updates if binary path is not writeable 2026-05-28 02:40:13 +09:00
aclist d3a06cf970 Merge pull request #318 from aclist/docs/MTU
docs: update DZG-001 (#315)
2026-05-28 02:18:43 +09:00
aclist 741344e75e docs: update DZG-001 (#315) 2026-05-28 02:18:08 +09:00
aclist a8a7aba338 Merge pull request #317 from aclist/chore/update-actions
chore: update checkout action to v5
2026-05-28 02:11:48 +09:00
aclist fafd4c6000 chore: update checkout action to v5 2026-05-28 02:10:49 +09:00
aclist 7e5e326ebe Merge pull request #316 from aclist/chore/geofile-stripping
chore: add geofile test
2026-05-28 02:09:45 +09:00
aclist e7644b49f5 chore: add geofile test 2026-05-28 02:07:39 +09:00
aclist 88aa1c31d5 Merge pull request #314 from aclist/feat/preboot-dialog
feat: preboot dialog
2026-05-28 01:26:42 +09:00
aclist dbeaad73c6 trigger checks 2026-05-28 01:24:45 +09:00
aclist be6be2af7c fix: use absolute path 2026-05-28 00:34:19 +09:00
aclist 63889c2299 fix: UI frames dropping during IO events 2026-05-28 00:32:11 +09:00
aclist 78e17121b6 chore: add preboot strings file 2026-05-28 00:31:35 +09:00
aclist 69ab3723a1 feat: add get_enum() to ScrolledWizardPage 2026-05-27 21:16:28 +09:00
aclist c82d42da09 docs: update changelog 2026-05-27 21:16:08 +09:00
aclist 9266473853 chore: optimize ipv6 strip logic 2026-05-27 15:49:18 +09:00
aclist b5d9c030dd fix: use dedicated copy button 2026-05-27 15:49:02 +09:00
aclist 4852e4a31f fix: abort early on failure 2026-05-27 05:13:31 +09:00
aclist d56cfdd0b2 docs: update special thanks 2026-05-27 04:55:44 +09:00
aclist 760208917c chore: bump version 2026-05-27 04:41:11 +09:00
aclist 26006daa74 docs: update changelog 2026-05-27 04:41:00 +09:00
aclist 97244e783c fix: make BMValidationPage optional (#311) 2026-05-27 04:38:39 +09:00
aclist 162839672b fix: make parents if missing (#312) 2026-05-27 04:38:00 +09:00
aclist ac9f582def chore: clear typehinting errors 2026-05-27 04:21:55 +09:00
aclist fe40346a01 chore: update user on tarfile 2026-05-27 04:00:49 +09:00
aclist d1cf2a9850 chore: clear typehinting errors 2026-05-27 04:00:15 +09:00
aclist 4face6f115 feat: preboot dialog (WIP) 2026-05-27 03:59:57 +09:00
aclist e3027fa830 Merge pull request #310 from aclist/fix/7.0.0b6-hotfixes
fix: 7.0.0b6 hotfixes
2026-05-26 04:54:48 +09:00
aclist 92699ebaa9 chore: revert delay to 3s 2026-05-26 04:42:50 +09:00
aclist ef2709561d fix: resolve #306 2026-05-26 04:40:14 +09:00
aclist 89a6996695 Merge pull request #304 from aclist/chore/prebuild-script
chore: update build tools
2026-05-26 04:01:24 +09:00
aclist 41b3ad8eca chore: clear typehinting errors 2026-05-26 03:59:11 +09:00
aclist d57277f527 fix: drop unused import 2026-05-26 03:57:13 +09:00
aclist c45678ca88 chore: add custom ScrolledWindow class 2026-05-26 03:56:35 +09:00
aclist c95ad53f6f chore: bump version 2026-05-26 03:55:51 +09:00
aclist 3e144dc7d2 fix: resolve #305 2026-05-26 03:54:30 +09:00
aclist afdfa76fdf fix: resolve #309, #307 2026-05-26 03:53:01 +09:00
aclist 00c27ad020 fix: resolve #308 2026-05-26 03:52:11 +09:00
aclist d60204b676 chore: reorder operations in entrypoint function 2026-05-26 03:51:44 +09:00
aclist 3d2e27bfe1 chore: update release script 2026-05-26 00:56:02 +09:00
aclist 8a66bafe4c chore: update build tools 2026-05-26 00:55:11 +09:00
aclist 2963c4e01c Merge pull request #299 from aclist/chore/move-updates
chore: move updates
2026-05-25 16:18:07 +09:00
aclist f849cbc183 chore: update tests 2026-05-25 16:07:44 +09:00
aclist 77505db621 fix: collision with native method name 2026-05-25 15:13:08 +09:00
aclist ba7a2b7ab4 fix: fallback for missing entries in columns file 2026-05-25 15:11:21 +09:00
aclist 6d7ed80996 Merge branch 'chore/move-updates' of github.com:aclist/dztui into chore/move-updates 2026-05-25 00:04:14 +09:00
aclist 72024c5d43 fix: refresh increment overwriting when changing tabs 2026-05-25 00:04:06 +09:00
aclist 0426d50d3e fix: refresh increment ov refresh decrement resetting when changing tabs 2026-05-25 00:03:21 +09:00
aclist 26ee560cb4 chore: test build version after export 2026-05-24 18:56:41 +09:00
aclist 6ec77f6542 fix: pass update args to manager 2026-05-24 01:53:16 +09:00
aclist 59959ae3a1 chore: clear typehinting errors 2026-05-24 01:49:08 +09:00
aclist 05fd95cd8e chore: bump version 2026-05-24 01:34:22 +09:00
aclist 86ba8d9187 chore: move update logic to manager 2026-05-24 01:34:16 +09:00
aclist e715e6da1c fix: use LC_ALL var in test 2026-05-23 22:23:44 +09:00
aclist 18ba851b7e chore: drop unused code 2026-05-23 22:23:29 +09:00
aclist d2643d08e9 Merge pull request #296 from aclist/fix/empty-statusbar
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: statusbar being wiped when calcdist fails on empty row
2026-05-21 21:07:23 +09:00
aclist 6274f62c93 chore: remove print statement
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-05-21 21:06:40 +09:00
aclist 24609a1c71 docs: update changelog 2026-05-21 20:58:32 +09:00
aclist 1b344c1260 chore: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-21 20:55:01 +09:00
aclist e72e09eec1 feat: save filters on exit 2026-05-21 20:42:24 +09:00
aclist 00e78b1228 fix: set filter panel to insensitive on lan load 2026-05-21 20:42:00 +09:00
aclist efddd61ea1 chore: bump version 2026-05-21 01:49:55 +09:00
aclist d006908bbc fix: statusbar being wiped when calcdist fails on empty row 2026-05-21 01:47:05 +09:00
aclist 5ba6b6c040 Merge pull request #295 from aclist/fix/lan-server
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: break out of loop when sek out of loop on first server
2026-05-20 00:36:58 +09:00
aclist d3b9ba031d Merge branch 'fix/lan-server' of github.com:aclist/dztui into fix/lan-server 2026-05-20 00:35:49 +09:00
aclist 8bfcb6c8fe fix: break out of loop when scanning LAN 2026-05-20 00:35:35 +09:00
aclist 353b10ebfc fix: break out of loop when sek out of loop on first server 2026-05-20 00:25:10 +09:00
aclist 698150645d Merge pull request #290 from aclist/feat/airgapped-installer
feat: airgapped installer
2026-05-20 00:09:31 +09:00
aclist 87f347870f chore: clean up try/except block
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-05-19 23:41:51 +09:00
aclist 74bf4b5cfc se str, not Path
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-19 18:35:47 +09:00
aclist 896799f904 chore: update string 2026-05-19 18:34:35 +09:00
aclist 0f591e658c fix: call proper workshop method 2026-05-19 18:31:59 +09:00
aclist f9083b33ef fix: change lan panel tate on map/unmap 2026-05-19 18:28:45 +09:00
aclist 89fcfcb5d6 fix: use hostname.local 2026-05-19 18:26:09 +09:00
aclist fc7f1d7815 chore: update build script paths 2026-05-19 18:25:29 +09:00
aclist fb36425083 chore: bump version 2026-05-19 06:23:53 +09:00
aclist c5d8afbc4d chore: build dependencies into distribution tarball 2026-05-19 06:08:55 +09:00
aclist 17f732fafe feat: continue to main app after config wizard closes 2026-05-19 06:05:22 +09:00
aclist 676a7ea9eb fix: notes file may not be present 2026-05-19 06:04:45 +09:00
aclist d3f64c1ba5 chore: trigger backup workflow
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-05-18 22:05:46 +09:00
aclist 5da3397a29 Merge branch 'dzgui7' of github.com:aclist/dztui into dzgui7 2026-05-18 22:03:02 +09:00
aclist eca6df90d4 chore: add .github directory 2026-05-18 22:02:16 +09:00
109 changed files with 5617 additions and 692 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: aclist
+57
View File
@@ -0,0 +1,57 @@
name: Bug report
description: Submit bugs that affect functionality here
title: "[BUG] Your title here"
labels: ["bug"]
assignees:
- aclist
body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: checkboxes
id: kb
attributes:
label: Did you check if this issue already exists?
options:
- label: I have checked the [Knowledge Base](https://aclist.github.io/dzgui/kb.html) and searched through [past issues](https://github.com/aclist/dztui/issues?q=is%3Aissue+is%3Aclosed) and did not find my issue there.
required: true
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the bug
description: What did you expect to happen?
placeholder: "E.g., When doing X, Y occurred. Include as much detail as possible."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Additional log output
description: Attach the file `$HOME/.local/state/dzgui/DZGUI_DEBUG.log` by clicking this area to highlight it and then dragging the file in.
validations:
required: false
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+47
View File
@@ -0,0 +1,47 @@
name: Feature suggestion
description: Submit requests for new features here
title: "[FEAT] Your title here"
labels: ["enhancement"]
assignees:
- aclist
body:
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the feature
description: A clear and detailed description
placeholder: "E.g., Add a dialog to the Options menu that automatically lets the user do Y"
validations:
required: true
- type: textarea
id: why
attributes:
label: Describe the benefit
description: How will this feature benefit large numbers of users?
placeholder: "E.g., Currently, users have to manually do X to achieve Y. Implementing this feature would achieve A, B, and C."
validations:
required: true
+50
View File
@@ -0,0 +1,50 @@
name: Troubleshooting
description: Submit user error/troubleshooting questions here
title: "[HELP] Your title here"
labels: ["troubleshooting"]
assignees:
- aclist
body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: dropdown
id: device
attributes:
label: Device type
options:
- Desktop PC
- Steam Deck
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: DZGUI version
validations:
required: true
placeholder: "5.6.0"
- type: input
id: distro
attributes:
label: Linux distribution
validations:
required: true
placeholder: "Arch Linux"
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: A detailed description. What did you expect to happen?
placeholder: "Include as much detail as possible and state the steps leading up to the problem. E.g., First I do this, then I do this, then this happens."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Additional log output
description: Attach the file `$HOME/.local/state/dzgui/DZGUI_DEBUG.log` by clicking this area to highlight it and then dragging the file in.
validations:
required: false
+18
View File
@@ -0,0 +1,18 @@
name: Mirror to Codeberg
on:
push:
workflow_dispatch:
jobs:
mirror-to-codeberg:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
if: ${{ vars.GIT_REMOTE != '' }}
with:
target_repo_url: ${{ vars.GIT_REMOTE }}
ssh_private_key: ${{ secrets.GIT_SSH_PRIVATE_KEY }}
+790 -2
View File
@@ -1,3 +1,5 @@
# Changelog
## Added
- Setup wizard
- Changelog text wrapping and formatting
@@ -34,7 +36,11 @@
- Early load alerts button
- Preconnect dialog
- Preconnect warnings/failsafes like filesize
- Save filters per server context between sessions
- Preboot progress dialog
- Choose to jump into splash screen instead of server
- Collapsible connection panel
- Play offline (load mods directly)
## Changed
- Conform to PEP 440 versioning for beta versions
@@ -49,6 +55,9 @@
- Suppress log messages from imported modules
- Embed Workshop link in Options menu
- Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB)
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped
- Debug mode
@@ -64,7 +73,6 @@
## Unreleased
- Load offline mods
- Choose to jump into splash screen instead of server
- Local documentation
- Raw debug command in context menu
@@ -72,3 +80,783 @@
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
## [6.0.5] 2026-05-17
## Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.4] 2026-04-13
## Fixed
- Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12
## Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs
## Changed
- Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first
- Rename some internal functions
- Wrap entire startup process in dialog
## [6.0.1] 2026-01-16
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14
## Added
- Support DayZ Experimental
- Ping display on server tables
- Save descriptive text notes on a per-server basis
- Speed up load time and navigation of server tables
- More robust threading and cache system when filtering servers
- Show additional DayZ client information in Options menu
- Warn user of DayZ client version mismatches
- Support clickable hyperlinks in dialogs
- Rich server metadata details dialog (right-click on server table)
- Filter by modded servers
- Breadcrumbs showing current menu context
- Dedicated changelog page
- Dedicated keybindings page
- Dedicated settings page ("Options")
- Support sandboxed version of Flatpak
- Vim-style navigation keybindings
- Added "Return to main menu" button to dialog windows when failing to load server table
- Dynamic context menus for modded servers
- Additional keybindings for new filter toggles
- If a server is locked, prompt user when connecting
- Internal flag to allow distro-packaged releases to disable in-app updates
- Pre-boot checks to test whether script was invoked directly
- Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing
## Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading
- Normalized buttons in dialogs and restored proper padding
- GTK errors being emitted to stdout when inserting debug table
- Entry dialog sensitivity when validating API keys
- Additional validation on entry dialogs to prevent submitting empty text
- Suppress errors during pre-boot checks when mods are not installed
- Centered filter checkboxes within panel
- Do not pop unhighlight/select stale buttons if no stale mods exist
- Improved keybinding interaction with side panels
- Suppress typeahead search in mod dialogs
- Do not trigger global API cooldown if no LAN/favorite servers are found
- Fix table column expansion in server mod dialogs
- Missing parameters when closing application via window decorations
- Statusbar contents not updating on certain pages
- Prevent debug button from activating when in certain input fields
- Window size not updating correctly when unmaximizing window after changing "fullscreen at boot" setting
- Branch toggle signal being emitted when entering Options menu from other contexts
- Mods rarely not appearing in local mod list if download completed too quickly
- Statusbar not updating when clicking a row after spamming keyboard input
- Extraneous logs being generated when subscribing to mods
- Narrow width of columns in modlist dialogs occluding text
- Newline terminators in history file
- Floating point number calculation
- Window resizing too small if no prior resolution was set
- ESC key destroying wait dialogs while a thread is pending
- Tooltip signals being erroneously emitted on main menu
- UI not being constructed correctly if CHANGELOG.md was missing
- Config file erroneously getting updated when populating settings menu
- Leaky variable name in dialog titles
- Prevent extraneous signals from propagating when column width is adjusted
- Server filter panel not being hidden when entering other page contexts
- Path to remote changelog being constructed incorrectly
- Dialogs with newlines breaking output in log table
- Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## Changed
- Require Python 3.13
- Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found
- Auto-focus first item when opening context menus
- Opacity setting on side buttons when in a different context
- Refactored BM API key validation to account for new key format
- State file serialization methods
- Optimize time complexity of pre-boot checks (GaryBlackbourne)
- Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox
## Dropped
- Ping readout in statusbar
- Extraneous information from right statusbar
## [5.8.3] 2026-01-04
## Fixed
- Normalize checksum numbers and dates
## [5.8.2] 2026-01-04
## Fixed
- Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27
## Fixed
- Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## [5.8.0] 2025-07-06
## Added
- Filter servers by official/unofficial status
- Automatically fetch geolocation records
## Changed
- Updated internal versioning of helper files
## Fixed
- Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17
## Added
- Save application dimensions when quitting and restore on subsequent boot
## Fixed
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
## Changed
- More direct server ping query method that should return marginally more accurate times
## [5.6.7] 2025-04-04
## Dropped
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
## [5.6.6] 2025-03-16
## Changed
- Update IP database records for 2025-03
## [5.6.5] 2025-03-04
## Fixed
- Livonia server results being dropped from batch queries
## [5.6.4] 2025-02-10
### Changed
- Drop launch flag and check for invocation through Steam automatically
## [5.6.3] 2025-02-09
### Changed
- Update IP database records for 2025-02
## [5.6.2] 2025-01-22
### Fixed
- Resolve regression introduced with IP resolution feature in 5.6.0 (restores functionality of right-click action: Add to My Servers)
## [5.6.1] 2025-01-10
### Fixed
- Add fallback support for jq 1.6
## [5.6.0] 2025-01-06
### Added
- Application header bar and controls
- Menu context subtitle in header bar
- Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode
- Highlight stale mods in mods list
- Added --steam launch parameter
- Open Steam workshop subscriptions dialog when switching from Manual to Auto mode
- Hover tooltips to most buttons
- Add in-app documentation link to Codeberg mirror
- Additional logging
### Fixed
- Avoid sudo escalation if system map count is sufficient (jiriks74)
- Duplicate dialog title on Steam Deck
- Resolve remote IP when saving records for game servers with multiple hosts
- More performant symlink traversal when checking for legacy links
- Only iterate on missing symlinks at boot (400% performance uplift)
- Fix for server list truncation causing some servers to not appear in results
- Empty dialog popups if user manually deletes local mods while application is running
- Update statusbar when removing servers from list/repopulating
- Abort DayZ path discovery if Steam files are not synched
- Race condition when checking for installed mods
- Favorite server message not updating correctly
- Improved handling for cases where there are no locally installed mods
- Set up mod symlinks at boot, rather than only on server connect
- Prevent context menus from opening when table is empty
- When reloading table in-place, prevent duplicate panel elements from being added if already present
- Clean up local mod signatures from versions file when deleting mods
- Focus first row when opening mods list
### Changed
- Redact usernames in log files
- Prevent ArrowUp/ArrowDown input when inside keyword field
- Admonish user to restart Steam in error dialog if DayZ path could not be found
- Refactor control flow for more robust contextual parsing
- Stop sending modal dialog hints to outer window
- Clean up signal emission
- Clarify some error messages and normalize text formatting
- Update forum URL
- Update README.md
- Update IP database to 2025-01
- Reduce IP database size by 50%
- Update documentation to 5.6.x standard
### Dropped
- Hall of Fame section from button links, to be moved inside documentation
- Unused imports
## [5.5.3] 2024-12-13
### Fixed
- Add remote resource health checks before downloading updates
### Added
- Add fallback repository
## [5.5.1] 2024-12-03
### Fixed
- Support localized decimal separators when parsing installed mod sizes
## [5.5.0] 2024-11-10
### Added
- Support servers running DLC content (fixes Frostline servers)
- Expose a toggle setting for whether to launch the application in fullscreen
- Text autocompletion in maps search field (partial search)
- Add disk space warning to popup dialog when downloading mods
### Fixed
- Servers in saved servers list would populate context menu with same option when right-clicking in server browser
- Enable adding/removing servers to/from My Saved Servers when in Recent Servers context
- Prevent maps combobox from duplicating contents
- Restore keyboard input to keyword entry field
### Changed
- Abort fallback query method if DLC is required
## [5.4.2] 2024-10-05
### Fixed
- Sanitize third-party API IDs to remove UGC collisions
## [5.4.1] 2024-09-25
### Added
- Pre-boot validation check for users with self-compiled version of jq
### Fixed
- Use fallback logic for modlist queries when user traverses networks
- Fix signal handling control flow for checkbox toggles
- When reloading the server browser, the map combobox selection would revert to the last selected map instead of All Maps
- Server filter toggle signals were accessible from the main menu when switching between menu contexts
- Global cooldown dialog could sometimes block filter toggles after cooldown reset
- Normalized minor version number due to a previous clerical error
## [5.4.0] 2024-08-27
### Added
- Scan local area network for DayZ servers
- Freedesktop application icons for system taskbar, tray, and other dialogs
- Emit CPU model name when exporting system debug log
### Fixed
- Errors being printed to the console when Exit button was explicitly clicked
- Detect Steam Deck OLED APU variant during initial setup
- Encapsulate player names correctly so that names with whitespace in them are supported
- Test if DayZ directory is empty at startup, implying that the game was moved to a new library collection
- Report WM_CLASS name to the window manager
## [5.3.2] 2024-07-02
### Fixed
- Server list would sometimes be missing some expected servers due to truncation being caused when a server erroneously set an incomplete gametype
## [5.3.1] 2024-06-15
### Added
- Uninstall routine: invoke via dzgui.sh -u or dzgui.sh --uninstall and choose from full or partial uninstall
## [5.3.0] 2024-06-15
### Added
- Server browser tables now display the number of players in queue (for full servers)
- Warning dialog if the user's API key returned no servers, and a global API cooldown dialog (60 seconds)
### Fixed
- Removing servers from My Servers via the right-click context menu was not actually removing the server and drawing an empty dialog
- Resolved an issue where, when Steam was not installed in a standard location and the user was prompted to let DZGUI auto-discover the correct path, this information was not retained correctly
## [5.2.4] 2024-05-29
### Fixed
- Resolved an issue that could cause an old path to a prior DayZ installation to be reinserted in the config file if the Steam client did not synch the new path internally. DZGUI would try to ask the user to re-run first-time setup and update the path, but the old path would still be used.
## [5.2.3] 2024-04-19
### Added
- Added Debug Mode button to main menu
### Changed
- Reworded debug mode notification dialog
- Updated geolocation records
### Dropped
- Dropped seen_news key from configs
## [5.2.2] 2024-04-18
### Added
- Cover artwork/icons for Steam "Recent Games" and tree view
### Changed
- Updated documentation to the v5.0.0 spec
## [5.2.1] 2024-04-01
### Fixed
- Fixed a regression where where the first-time setup dialog would not trigger auto-path discovery
### Changed
- Reworded some menus and dialogs for clarity
## [5.2.0] 2024-03-21
### Added
- Refresh player count for active row: invoke via right-click context meu or directly with the Ctrl-r hotkey. This feature has a 30 second global cooldown to prevent throttling.
### Fixed
- Improve case-insensitive keyword search to be portable across awk versions (previous version required gawk)
- Fixed a dialog string from being shown twice when adding a server to favorites via context menus
### Changed
- Use a more robust method for downloading mods when auto mod install is enabled
## [5.1.1] 2024-03-18
### Fixed
- Hotfix for remote helper files not being fetched correctly
## [5.1.0] 2024-03-18
### Added
- Make columns in the server browser user-resizable (affects Server Browser, My Servers, and Recent Servers)
- Save dragged position of user-resized columns
- Display ping to server in statusbar: by popular request, added the ability to visualize both distance to server and round-trip latency (ping), at the cost of a small calculation delay. Please leave feedback regarding whether this feature feels fast/responsive enough.
### Fixed
- Fixed a rare scenario in Auto Mod Install Mode where defunct mods (mods no longer available on Steam) would try to be downloaded if the user had previously downloaded the mod
## [5.0.0] 2024-01-31
### Added
- Context switching: navigate to different pages using side buttons
- Dynamic statusbar: updates metadata and server distance when selecting rows
- Show server-side modlist and allow jumping to Steam Workshop pages to browse, and list whether mod is currently installed
- Print debug logs in-app (Help > Show debug log)
- Functionality to change API keys in-app (used when revoking old API keys)
- Print atomic mod sizes when listing installed mods
- Dialogs show direct links to API key management URLs when changing API keys
- Toggle dry-run mode directly from server browser
- Extensive keybindings for fully controlling the application without the mouse
- Extensive pre-boot sanity checks
- Keybinding help dialog in main menu
- Right-click context menus in server browsers/mod list: add/remove from favorites, show server-side mods, delete mod, copy server IP to clipboard
### Changed
- Utilize GTK bindings and MVC paradigm for UI creation and data flow
- Filter servers dynamically from within server browser
- Performance and security improvements to DZGUI helper files
### Fixed
- First-time setup dialogs respawning repeatedly in certain scenarios
- Issues with Steam client switching to the wrong page when using auto-mod install
- Set text input module correctly when launching on Steam Deck
- Separate current/total player count and use proper integer sort method in table
## [4.1.1] 2023-12-18
### Fixed
- News marquee not showing
## [4.1.0] 2023-12-17
This update adds support for DayZ servers running on a local area network. To connect or add to your server list, supply the server IP and query port in the format IP:PORT.
Support for Steam Deck Game Mode has also been restored, with a new dialog format that allows for virtual keyboard input. This should allow you to enter text in form fields by activating the Steam button + X. In addition, you can unlock the mouse and keyboard input when launching DZGUI through Steam by using an internal binding provided by Steam Deck: long-press the three dots button on the right of the device for three seconds to toggle the input state. This allows you to use mouse and keyboard-style bindings on Game Mode and vice versa.
### Added
- Virtual keyboard support for text entry fields on Steam Deck Game Mode
- Validate and connect to LAN server IPs
## [4.0.4] 2023-12-23
### Fixed
- Enforce version check for Python versions before 3.10
## [4.0.3] 2023-11-22
### Fixed
- Query helper: backwards compatibility for pre-2021 versions of Python 3
## [4.0.2] 2023-11-22
### Fixed
- Query helper not loading: fixed a remote link pointing to the wrong destination and added a checksum verification to ensure file is present
## [4.0.1] 2023-11-22
### Fixed
- Emergency hotfix to remove build artifacts leaking into main script: if you updated DZGUI from 3.3.18 to 4.0.0 between 2023-11-22 15:00:02 and 2023-11-22 15:03:37 GMT, there is a small chance it will be unable to launch correctly. If so, please follow the instructions on the manual to reinstall.
## [4.0.0] 2023-11-22
Hello players, this is a major version update which overhauls many of DZGUI's underlying systems to improve responsiveness of the application and make menus more intuitive to interact with. It should be considerably more difficult, if not impossible, to inadvertently crash a dialog, and nested dialogs should behave in a more expected fashion, such as when going back and forth between menus or changing options dynamically within a given menu.
In addition, with this update we are querying servers directly. The net benefit of this is that results will returned faster, bootup is faster, and it also paves the way for future features like connecting to servers on a LAN. The Battlemetrics API is now entirely optional and can be skipped during the setup process. However, if you have already set up a BM API key, you can continue using it if you want to query or add servers by ID instead of IP.
As part of this change, version 4.0.0 introduces the ability to connect to or add servers to your list based on either ID or IP. Previously, you could only connect by IP and add by IP, but now you can connect by IP/ID or add by IP/ID. If you choose the ID method, this will be translated into an IP seamlessly in the background. Similarly, favorite servers are now stored using the full IP rather than the ID. Due to the variety of systems and methods for connecting to servers, the application was carrying around and converting server IPs, IDs, and other formats back and forth, creating unnecessary complexity. By normalizing everything to an IP basis, maintainability should be more consistent. When upgrading to this version, your old favorites lists will be updated automatically to the new IP method. Note that as a result of this change, we must purge old history lists ("Recent Servers"), but everything else should carry over as before.
If you encounter any problems with this new release or with the migration of configs, please do not hesitate to submit a bug report.
Attention Fedora 38 users: problems with upstream GNOME packages causing crashes have been reported to GNOME development and a fix has been issued. You have the choice of compiling the zenity package
from source or waiting until the latest version is merged into Fedora's package manager.
### Added
- Change in game name: dynamically change your profile name via the Advanced Options menu
- Connect by ID: supply a Battlemetrics ID to connect to a server; this can be used in lieu of the IP
- Add by IP: supply a standard IP to add a server to your list; this can be used as a more direct way of saving servers
- Filter by 1PP and 3PP in the server browser
- Save connected server to favorites: prior to connecting, asks the user if they want to save this server for future use
- Generate additional output when generating system logs
### Fixed
- Rare cases where the keyword filter would not filter server results correctly
- Handling of dialog exit signals: made it much more difficult to crash the application in rare cases when spamming input or returning from menus
- Update menus in place: when toggling options in the Advanced Options menu, displays the current state/mode of the option for better readability into what option is currently enabled
- More intuitive menu navigation: dialog setup and teardown is more responsive and follows expected flows when inside of nested menus
- Message formatting: fold long messages inside of popup dialogs for proper word wrapping regardless of screen type
- Fixed a rare case where dialogs would spawn twice during first-time setup
- Properly remain inside of menus when looping where it would make sense to do so : e.g., Delete Servers list, Advanced Options
### Changed
- Query servers directly to reduce API hops: initial bootup and subsequent server queries should be considerably faster
- Store complete IP:Port instead of server IDs
- Make Battlemetrics API key optional: this is only used for the 'Connect by ID' and 'Add server by ID' methods and is not required. If you prefer, you can simply connect/add by IP.
- Prevent the application from launching in Game Mode on Steam Deck: Steam Deck's kiosk mode has problems sending keyboard input to third party applications. To prevent unintended usage, DZGUI now warns the user to launch the app in Desktop Mode if they attempt to use it from Game Mode. Adding DZGUI as a Non-Steam Game does work on desktop PCs, but is not recommended due to the way Steam handles subshells. For best results, launch DZGUI directly via the script/applications menu (PC) or via the desktop icon (Steam Deck).
- Omit null servers from list: servers that time out or send an empty response are now omitted entirely from the My Servers list, as they will not return meaningful metadata unless they are online.
The My Servers list thus shows online and accessible servers
## [3.3.0] 2023-05-16
### Added
- Fetch more inclusive global "players in-game" count
- List mod directory on installed mods list
- Detect default Flatpak Steam path
- Dark mode/light mode theme to help file
- Alpha-sort My Servers list
- Add description of how to enable hidden folders on GTK2/3
- Initial logging framework
### Changed
- Test for wmctrl when enabling full auto mod installation
- Steam Deck: block toggling full auto mod installation due to extra dependencies needed
- First-time setup: sudo escalation when checking system map count for the first time
### Fixed
- Steam Deck: non-ASCII delimiter causing setup menu to despawn on some devices
- Don't add items in My Servers multiple times to array when the list of favorites is paginated
- Trigger progress dialogs sooner and in sequence to reduce appearance of visual lag
- First-time setup: break out of dialogs correctly when user backs out
- First-time setup: break out of automatic path discovery when user specifies a path manually
- More portable interpreter invocation
- Properly size down window resolution when returning from server browser
## [3.2.10] 2023-05-11
### Fixed
- Return from lockfile function if first-time setup has not been run
- Sanitize inputs when using file picker
- Require both wmctrl and xdotool
## [3.2.9] 2023-05-10
### Changed
- Reword button to "Choose path manually" instead of "Retry"
## [3.2.7] 2023-05-10
### Changed
- Better sudo escalation within zenity dialogs if vm map count is too small
## [3.2.6] 2023-05-10
### Fixed
- Don't parse Flatpak symlinks when setting up default Steam path
## [3.2.5] 2023-05-07
### Fixed
- Require sudo when checking vm map count
## [3.2.4] 2023-03-01
### Fixed
- BM API returning stale query port and preventing fetching modlist
## [3.2.3] 2023-02-17
### Fixed
- sysctl map count value not being loaded immediately after setting
- Application terminating when user declines to update map count value
- Erroneous stderror output when flatpak is not installed
## [3.2.0] 2023-01-19
### Added
- Support Flatpak version of Steam
## [3.1.8] 2023-01-18
### Fixed
- Progress window blocking rest of window stack
- Bug when updating old mods if automod set to ON
## [3.1.7] 2023-01-06
### Fixed
- Hotfix for xdotool repeating input
## [3.1.6] 2023-01-01
### Changed
- Tick low pop servers by default
## [3.1.5] 2023-01-01
### Fixed
- Validate BM key on initial setup
- Fix history menu not parsing query ports correctly
### Changed
- More permissive Steam client discovery for tiling WMs
## [3.1.4] 2022-12-10
### Fixed
- Issue #43: Hotfix for workspace-driven WMs
## [3.1.3] 2022-12-06
### Fixed
- Explicitly require Python 3
## [3.1.1-2] 2022-12-03
### Fixed
- Fix lockfile path
## [3.1.0] 2022-12-03
### Added
- Recent connect history
- Simple, OS-agnostic automod installation
- Track local mod versions
- Force update local mods option
- Added python to dependencies
- File-picker driven path discovery on initial setup
### Dropped
- Headless mod installation
- Drop server ID field requirement on initial setup
### Changed
- Clean up main menu options
- Enforce Steam API key on initial setup
- More accurate path discovery on initial setup
- Add thousands separator to player counts in server browser
### Fixed
- Initial setup dialog causing early crash
- Improved error handling on initial setup to avoid malformed config files
- Delete server menu not clearing when returning to main menu
- Handle whitelist deletion when only one entry present
- Include path to drives under /run in path discovery
- Use Steam-safe local zenity version
## [3.0.7] 2022-11-25
### Fixed
- Hotfix for server reporting multiple versions of same mod
## [3.0.6] 2022-11-09
### Changed
- More verbose logs
## [3.0.5] 2022-10-27
### Fixed
- Properly create .desktop file on desktop PCs
## [3.0.3-4] 2022-10-16
### Fixed
- Steam Deck path discovery on first-time setup
## [3.0.2] 2022-10-12
### Fixed
- Size of certain popups on Steam Deck
## [3.0.1] 2022-10-12
### Fixed
- Initial popup size on Steam Deck
## [3.0.0] 2022-10-12
### Added
- Foreground progress of manual mod subscriptions
- Automatic mod helper through steamcmd
- Forum link
- Enforce Steam API
### Dropped
- Stop retrieving extra metadata from BM
### Changed
- Reorder main menu
- More verbose error messages
- Better abstraction of URLs
### Fixed
- Width and text of some popups on Steam Deck
## [2.7.2] 2022-10-07
### Fixed
- Fix internal URL
## [2.7.1] 2022-10-05
### Fixed
- Game launch not kicking off after symlink creation
## [2.7.0] 2022-10-04
### Added
- Server browser and geolocation algorithm
- More verbose error codes
- Additional progress bar setup and destruction throughout the application
- Additional API response validation
### Fixed
- Encapsulate title strings to prevent leaky arguments in title bars
- Remove erroneous slow boot process if first-time setup was already complete
- Delete server list not emptying when returning to main menu
### Changed
- Group main menu entries in advance of future functionality
- Better abstraction of paths
- Clarification of certain options and errors
## [2.6.3] 2022-10-02
### Fixed
- Hotfix for connect-to-fav not getting modlist
## [2.6.2] 2022-10-02
### Fixed
- Hotfix for BM API returning malformed publishedfileids
## [2.6.1] 2022-09-25
### Fixed
- Freedesktop shortcut errors
## [2.6.0] 2022-09-05
### Added
- Connect by IP method
## [2.5.1] 2022-08-17
### Fixed
- Hotfix for malformed paths during first-time setup
## [2.5.0] 2022-08-16
### Added
- Lockfile: prevent concurrent instances of DZGUI from being opened
### Changed
- Faster path discovery on first-time setup
### Fixed
- Symlink collision on servers with many mods
- API response pagination for large server lists
## [2.4.1] 2022-08-09
### Fixed
- Hotfix for progress bar breaking table when >9 servers in list
## [2.4.0] 2022-08-07
### Added
- Delete server command added to main menu
- Write log to file for bug reports
- Prompt to permanently increase map count size
- Dependency check for Steam
- Cover artwork
- News backend for OTA updates
- Clean up stale symlinks when checking mods
### Fixed
- Minify long mod launch params for servers with launch params breaking the upper limit
- Port DZTUI method of handling legacy symlinks
- Prevent user from entering invalid data on first-time setup
- Prompt to re-run first-time setup if config is malformed
- Better handling of field output from table
- Better Steam Deck detection and handling
- Prevent garbage in error messages
- Miscellaneous backend improvements
### Changed
- Enforce download when switching branches
## [2.3.2] 2022-08-04
### Fixed
Set branch flag to 'stable' if no config file present
## [2.3.1] 2022-08-04
### Fixed
- Improved error handling of first-time setup fields
- Interpolate config file values for debug, branch when writing file
### Changed
- Prompt user to restart first-time setup if broken config is found
## [2.3.0] 2022-07-18
### Added
- Numbered mod links in browser
- Admonition to upgrade versions for bug fixes
- Toggle branch between stable/testing
### Fixed
- Hotfix for fav server select on main menu
## [2.2.1] 2022-07-17
### Fixed
- Hotfix for upstream API returning malformed modlists
- Handle servers with no mods
- Fix dialog window depending on browser exit
- Remove stray newlines in config file
## [2.2.0] 2022-06-22
### Added
- Toggle debug mode in-app
## [2.1.0] 2022-06-19
### Changed
- Updated link to new documentation
## [2.0.3] 2022-06-17
### Fixed
- Safer expansion of originating script path
## [2.0.2] 2022-06-16
### Fixed
- Remove enforced runtime check of workshop path
## [2.0.2] 2022-06-16
### Fixed
- Expansion of Steam path prefix when default path was found
## [2.0.1] 2022-06-16
### Fixed
- Regenerate mod links file in browser when clicking dialog
- Reset whitelist when canceling one-shot (fav) mode
## [2.0.0] 2022-06-15
### Added
- Attempt to find DayZ path and write to config on first launch
- Merge existing config values into new config format when upgrading version
- Dynamically detect Steam Deck and set launch parameters
- Add "gametime" column to server list
- One-shot mode to open mod links in browser if using desktop
- Add favorite server from main menu
- Update favorite server changes in real time
- Add link to help pages from main menu
- Add link to changelog from main menu
### Changed
- Don't require duplication of fav server in whitelist
- More permissive truncation of long server names (50 char limit)
- Render mod list as a scrollable menu
- Reword errors for greater verbosity
- Initial support of granular error handling for API response codes
- Move extended path variables out of user config
### Fixed
- Check if mod dir is sane before listing mods
- Prevent favorite server launch if none set
- Reset server list to entire whitelist if canceling out of fav connect
- Do not load table on empty API response and warn user
- Suppress stderr cruft in logs and use logger instead
## [1.2.1] 2022-06-12
### Fixed
- Print the entire changelog
- Add confirmation dialog before run
## [1.2.0] 2022-06-12
### Changed
- Mod validation process now uses faster, single-pass API query
- Improved logger output when setting fav server
### Added
- In-app changelog
## [1.1.2] 2022-06-08
### Fixed
- Fix array used for mod concatenation; fetch post-sanitized list of mods
## [1.1.1] 2022-06-05
### Fixed
- Stricter regex to parse upstream version number
## [1.1.0] 2022-06-05
### Added
- Main menu: fav server label on header
- Main menu: add servers by ID directly into config file
- Main menu: link to report a bug
- Main menu: quick connect to fav server
- Connect: mod compatibility check
- Connect: mod download prompt
- Connect: mod auto symlinks
- New version download prompt
- Additional visualization of progress/menus
### Fixed
- Menu recursion when navigating backwards
### Changed
- Reduced ping timeout interval
+21 -9
View File
@@ -1,18 +1,30 @@
# TODO: link to here from sphinx docs
# Setting up precommit hooks
pre-commit install
## Installing from source
# Fetching submodules
### Fetch submodules
git submodule update --recursive --init
# Installing dev dependencies
uv pip install -e .[dev]
### Install with dev dependencies
uv pip install -e '.[dev]'
# Building documentation
This includes all dependencies for building, linting, testing, and checking in code.
If you optionally want to install only a subset of dev tools, replace 'dev' with the following flag:
build: package release deps
docs: documentation tools
lint: code linting, fixing, and type checking
test: unit and integration tests
## Miscellaneous
### Setting up precommit hooks
pre-commit install
### Building documentation
sphinx-build -M html source build -a
# Building standalone DZGUI release binaries
1. Run `./scripts/dl_pyapp.sh`
### Building standalone release binaries
1. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
2. Run `python3 scripts/release.py`
3. Generate a release and tag against the bundled `dzgui` executable
3. Generate a release and tag against the resulting tarfile generated in `dist`
-5
View File
@@ -1,8 +1,3 @@
Additional licenses to git submodules not explicitly shipped with the source code:
python-a2s: https://raw.githubusercontent.com/Yepoleb/python-a2s/refs/heads/master/LICENSE
dayzquery: https://raw.githubusercontent.com/Yepoleb/dayzquery/refs/heads/master/LICENSE
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
+502
View File
@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+48
View File
@@ -0,0 +1,48 @@
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python
alone or in any derivative version, provided, however, that PSF's
License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved"
are retained in Python alone or in any derivative version prepared
by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Gabriel Huber
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the psutil authors nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Gabriel Huber
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+175
View File
@@ -0,0 +1,175 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
+2 -2
View File
@@ -36,5 +36,5 @@ html_show_copyright = False
html_show_sourcelink = False
today_fmt = "%Y-%m-%d"
release = "7.x.x"
version = "7.x.x"
release = "6.x.x - 7.x.x"
version = "6.x.x - 7.x.x"
+110
View File
@@ -0,0 +1,110 @@
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
@@ -0,0 +1,33 @@
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>`_.
- 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
Next steps
---------------
Proceed to the :doc:`setup` section for setup instructions.
+16 -98
View File
@@ -1,110 +1,28 @@
Installation
==================
Basic runtime dependencies
---------------------------------
As of this writing, you have a choice of two DZGUI versions:
If not already installed, the below can be found in your system’s package manager.
- DZGUI 6: Legacy stable version, more complicated to install
- DZGUI 7: Unstable beta version, much easier to install
If any dependencies are missing when the application starts, it will warn you, so you need not take any preemptive measures here.
DZGUI 6
--------------------
This version is no longer receiving feature updates, but will receive security updates until it is replaced by DZGUI 7.
- curl
- jq
- steam
- wmctrl or xdotool
- zenity
DZGUI 6 must be installed from the source code and has many more package dependencies and prerequisites for it to function correctly.
If you are looking for a one-click, self-contained installer, use :ref:`DZGUI 7` instead.
.. note::
All dependencies are installed out of the box on Steam Deck.
To install **DZGUI 6**, visit :doc:`dzgui6`.
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.
DZGUI 7
--------------------
This version is currently in beta testing and has all of the features found in DZGUI 6, but may be unstable.
Install the following build dependencies:
DZGUI 7 has an all-in-one installer and setup wizard, and many new features not found in DZGUI 6.
However, this codebase is newer and is provided as a pre-release beta candidate.
.. code:: console
If you want the simplest installer and don't mind experiencing the occasional bug, use this version.
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 next section for setup instructions.
To install **DZGUI 7**, visit :doc:`dzgui7`.
+12 -7
View File
@@ -3,16 +3,21 @@ Knowledge Base
.. _DZG-001:
DZG-001: Periodically getting dropped from servers, or servers appear unreachable
DZG-001: Periodically getting dropped from servers, or servers time out in DZGUI
------------------------------------------------------------------------------------
It is a longstanding issue in DayZ that the game opens a large number of connections when querying servers and/or while connected to servers.
This can result in excess traffic on the user's PC which, depending on how much headroom your network has, can lead to getting dropped, unresponsiveness, or a timeout.
DayZ opens a large number of connections while querying/connected to servers.
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. If it does, your wireless router settings do not have enough headroom for max simultaneous connections. Use a wired connection, or update your network settings to a more permissive setup.
In addition, packets sent from server responses are expected to be a standard size (see warning below).
Bohemia has acknowledged this issue going back 10+ years and stated that DayZ has a heavy impact on the network, but there is as yet no proposed solution on the DayZ side.
If your network does not have enough headroom or has settings departing from defaults, this may lead to getting dropped from servers,
unresponsiveness, or a timeout.
This issue is frequently seen on Steam Deck, due to the tendency for users to use it in an untethered Wi-Fi setup.
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. Consumer Wi-Fi routers
tend to have less headroom than their wired counterparts.
.. important::
Ensure that MTU (maximum tranmission unit) on your network is set to the standard size of 1,500 bytes.
If your network preferences or router have "jumbo" frames enabled, packets may be dropped, causing server queries to time out.
.. _DZG-002:
@@ -34,7 +39,7 @@ DZG-004: On Steam Deck, some mods in the Workshop show a black screen when DZGUI
---------------------------------------------------------------------------------------------------------
This is a bug in the Steam client that is being tracked at Valve's Steam for Linux issue tracker here: https://github.com/ValveSoftware/steam-for-linux/issues/9598.
To resolve this issue, manually intervene in the Steam client by selecting a different context (e.g., Store, Library), waiting for it to load, then navigating back to the Workshop context. This should
To resolve this issue, manually intervene in the Steam client by selecting a different context (e.g., Store, Library), waiting for it to load, then navigating back to the Workshop context. This should
clear the blockage and allow the contents to render.
.. _DZG-005:
+96
View File
@@ -0,0 +1,96 @@
import re
from collections.abc import Iterator
from typing import Any
from warnings import deprecated
@deprecated("Use dzgui.api.steam.unsubscribe()")
class WorkshopACF:
def __init__(self, file: str) -> None:
super().__init__()
self.dict: dict[str, Any]
self.load(file)
def as_dict(self) -> dict[str, Any]:
return self.dict
def load(self, file: str) -> None:
delimiter = r"\t\t"
lines = []
with open(file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
els = re.split(delimiter, line, maxsplit=1)
lines.append(els)
self.dict = self.parse(iter(lines))
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
acf: dict[str, Any] = {}
try:
while True:
line = next(lines)
if len(line) == 1:
key = line[0]
if key == "{":
continue
return acf
elif key == "}":
return acf
else:
key = self.dequote(key)
acf[key] = self.parse(lines)
elif len(line) == 2:
k, v = line
k = self.dequote(k)
v = self.dequote(v)
try:
n = list(acf.keys())[-1]
acf[n][k] = v
except Exception:
acf[k] = v
except StopIteration:
return acf
def unpack(self, d: dict, lines: list[Any] = []) -> str:
t1 = "AppWorkshop"
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
for k, v in d.items():
if type(v) is dict:
if k in t1:
self.indent = 0
elif k in t2:
self.indent = 1
else:
self.indent = 2
pref = "\t" * self.indent
lines.append(pref + self.enquote(k))
lines.append(pref + "{")
self.unpack(v, lines)
lines.append(pref + "}")
else:
pref = "\t" * (self.indent + 1)
lines.append(pref + self.enquote(k) + "\t\t" + self.enquote(v))
s = ""
for line in lines:
s += line + "\n"
return s
def to_file(self, file: str) -> None:
s = self.unpack(self.dict)
with open(file, "w") as f:
f.write(s)
@classmethod
def enquote(cls, s: str) -> str:
return f'"{s}"'
@classmethod
def dequote(cls, s: str) -> str:
return s.rstrip('"').lstrip('"')
def delete(self, modid: int) -> None:
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
del self.dict["AppWorkshop"]["WorkshopItemDetails"][modid]
+46 -39
View File
@@ -1,6 +1,6 @@
import hashlib
import logging
import shlex
import re
from concurrent.futures import wait
from concurrent.futures import ThreadPoolExecutor
@@ -12,6 +12,7 @@ from dzgui.api.servers import get_rules, fqip_to_record
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
DAYZ_COMMUNITY_ROOT,
LIBRARYFOLDERS_PATH,
WORKSHOP_PATH,
)
@@ -48,36 +49,32 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
mods = [file for file in workshop_path.iterdir() if file.is_dir()]
return mods
def is_mission(path: Path) -> bool:
# TODO: parse integrity of other files
parent = path.parent.name
if parent != DAYZ_COMMUNITY_ROOT:
return False
file = path / "init.c"
return file.exists()
def parse_meta(file: Path) -> ModMeta | None:
mod = file / "meta.cpp"
if mod.exists() is False:
def tokenize(mod: Path) -> dict[str, Any] | None:
file = mod.joinpath("meta.cpp")
delimiter=r"\s*=\s*"
modmeta = {}
try:
with open(file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip().rstrip(";")
if not line:
continue
els = re.split(delimiter, line, maxsplit=1)
if len(els) == 2:
key, value = els
modmeta[key.strip()] = value.strip('"')
return modmeta
except Exception as e:
logger.critical(e)
return None
with open(file / "meta.cpp", "r") as f:
st = f.read()
lex = shlex.shlex(st)
lex.whitespace += "=;"
v = []
while True:
tok = lex.get_token()
if not tok:
break
if tok == "protocol" or tok == "publishedid":
ntok = lex.get_token()
elif tok == "timestamp":
# NOTE: some malformed .NET tick conversions result in numbers < 0
ntok = lex.get_token()
if ntok == "-":
ntok += str(lex.get_token())
elif tok == "name":
ntok = lex.get_token()
if ntok is not None:
ntok = ntok.split('"')[1]
if ntok is not None:
v.append(ntok)
meta = ModMeta(*v)
return meta
def get_mod_size(path: Path) -> float:
s = 0
@@ -87,33 +84,43 @@ def get_mod_size(path: Path) -> float:
return size
def get_delimited_mods(steam_path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
def get_custom_mods(path: Path) -> list[Any]:
# TODO: rename this function for clarity ("get local vs. get custom")
mods = get_local_mods(path)
# TODO: error handling
return parse_mods(mods, use_custom=True)
def parse_mods(mods: list[Path], use_custom: bool = False) -> list[Any]:
clean = []
for mod in mods:
mod_dir = mod.name
symlink = _hash(mod_dir)
# FIXME: malformed .cpp files could break this
# mention that mods may be downloading
meta = parse_meta(mod)
symlink = _hash(mod_dir, use_custom)
meta = tokenize(mod)
if meta is None:
continue
size = get_mod_size(mod)
# NOTE: final col is cell renderer highlight toggle
clean.append([meta.name, symlink, mod_dir, size, False])
clean.append([meta["name"], symlink, mod_dir, size, False])
clean.sort(key=lambda row: str(row[0]).casefold())
return clean
def get_delimited_mods(steam_path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
return parse_mods(mods)
def get_missing_mods(local: list, remote: list) -> list:
return [mod for mod in remote if mod not in local]
def _hash(uid: str) -> str:
def _hash(uid: str, use_custom: bool = False) -> str:
md5 = hashlib.md5()
md5.update(uid.encode("ascii"))
return "@" + md5.hexdigest()[:8]
prefix = "@C" if use_custom else "@"
return prefix + md5.hexdigest()[:8]
def remove_stale_signatures(config: Path, versions: Path) -> None:
+1 -2
View File
@@ -456,6 +456,5 @@ def get_pretty_version(steam_path: Path, appid: int) -> str | None:
vers = get_dayz_version(pe_file_path)
dayz_version = dayz_version_to_str(vers)
return dayz_version
except Exception as e:
print(e)
except Exception:
return None
+12 -3
View File
@@ -14,6 +14,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union
from dzgui.api.bm import map_id_to_record
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import STEAM_SERVERS
from dzgui.util.format import format_exception
from dzgui.util import strings
import a2s
@@ -61,6 +62,8 @@ class Details:
@dataclass(slots=True)
class Record:
# TODO: investigate whether this enum is still being used;
# can this be frozen?
"""
The gameport field is manipulated by the RowType.CONN_BY_IP method
"""
@@ -106,7 +109,7 @@ class A2SInfo:
def get_netmask() -> str:
hostname = os.uname()[1]
i = socket.gethostbyname(hostname)
i = socket.gethostbyname(f"{hostname}.local")
netmask = i.rsplit(".", 1)[0]
return netmask
@@ -270,8 +273,9 @@ def source_info_to_dict(ip: str, qport: int, info: "SourceInfo") -> dict[str, An
res["ping"] = ping
return res
except Exception as e:
# TODO: generalized function
logger.critical(f"{type(e).__name__}: {e} ({ip}:{qport})")
msg = format_exception(e)
msg += f" ({ip}:{qport})"
logger.critical(msg)
raise e
@@ -472,6 +476,7 @@ def query_by_ip(addr: str) -> A2SInfo:
return A2SInfo(Record("0", 0, 0), None)
return query_by_record(record, update_gameport=True)
def query_by_id(server_id: int, key: str) -> A2SInfo:
"""
Used with numeric Battlemetrics IDs
@@ -508,6 +513,10 @@ def fqip_to_record(addr: str) -> Optional[Record]:
return Record(r[0], int(r[1]), int(r[2]))
def record_to_fqip(record: Record) -> str:
return f"{record.ip}:{record.gameport}:{record.qport}"
def response_to_fqip(res: dict) -> str:
ip = res["addr"].split(":")[0]
gameport = res["gameport"]
+80 -21
View File
@@ -3,20 +3,25 @@ import logging
import os
import requests
import subprocess
from typing import Union
from warnings import deprecated
from shlex import shlex
from pathlib import Path
from dzgui.init.prereqs import has_steam_client
from dzgui.api.mods import _hash
from dzgui.const.constants import (
APPID_DAYZ,
APP_NAME,
DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH,
UBUNTU_STEAM_PATH,
REQUEST_TIMEOUT,
VDF_PATH,
)
from dzgui.const.endpoints import STEAM_PUBLISHED_FILES
from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_ENDPOINT
from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args
@@ -53,8 +58,6 @@ def get_steam_paths() -> list[tuple[Path, str]]:
def concat_mods(mods: list[str]) -> str:
from dzgui.util.symlink import _hash
hashes = []
for mod in mods:
md5sum = _hash(mod)
@@ -68,31 +71,21 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
for line in lines:
data = line.split(",")
_id = data[0]
_hash = int(data[1])
hashes[_id] = _hash
mod_hash = int(data[1])
hashes[_id] = mod_hash
return hashes
def enqueue_mod(mod: str, appid: int) -> None:
args = [
"steam",
f"steam://url/CommunityFilePage/{mod}+workshop_download_item",
str(appid),
mod,
]
subprocess.Popen(["/usr/bin/env", "bash", *args])
def get_needs_update(
version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
) -> list[tuple[str, str, int, int]]:
local_hashes = get_local_signatures(version_file)
local_stamps = get_local_signatures(version_file)
needs_update: list[tuple[str, str, int, int]] = []
for title, _id, _hash, size in remote_hashes:
if _id not in local_hashes:
needs_update.append((title, _id, _hash, size))
elif _hash != local_hashes[_id]:
needs_update.append((title, _id, _hash, size))
for title, _id, stamp, size in remote_hashes:
if _id not in local_stamps:
needs_update.append((title, _id, stamp, size))
elif stamp != local_stamps[_id]:
needs_update.append((title, _id, stamp, size))
else:
continue
return needs_update
@@ -146,6 +139,43 @@ def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> i
return proc.returncode
def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
"""Loads to the menu screen with the selected mods; used for AFK/pre-joining"""
concat = concat_mods(mods)
client_args = concat_bash_args(client)
params = [
"-applaunch",
str(appid),
"-nolauncher",
"-nosplash",
"-skipintro",
f"-name={name}",
f"-mod={concat}",
]
proc = subprocess.run([*client_args, *params])
return proc.returncode
def launch_offline(
client: str, appid: int, name: str, mods: list[str], mission: str
) -> int:
"""Launch offline with specific mods/missions"""
symlinks = ";".join(mods)
client_args = concat_bash_args(client)
params = [
"-applaunch",
str(appid),
"-nolauncher",
"-nosplash",
"-skipintro",
f"-name={name}",
f"-mod={symlinks}",
f"-mission={mission}",
]
proc = subprocess.run([*client_args, *params])
return proc.returncode
def find_user_id(path: Path) -> str | None:
resolved_path = path / "config" / "loginusers.vdf"
try:
@@ -200,6 +230,29 @@ def vdf2json(path: Path) -> str:
jbuf += "\n"
def update_workshop(key: str, mod: int, endpoint: str) -> None:
payload: dict[str, Union[int, str]] = {
"publishedfileid": mod,
"appid": APPID_DAYZ,
"key": key,
"list_type": 1,
"notify_client": 1,
}
try:
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
res.raise_for_status()
except Exception as e:
logger.critical(e)
def subscribe(key: str, mod: int) -> None:
update_workshop(key, mod, SUB_ENDPOINT)
def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT)
def gen_shortcut() -> None:
# TODO:
"""
@@ -212,3 +265,9 @@ def gen_shortcut() -> None:
# or get right-most 32 bits
# STEAMID_64 & 0xFFFFFFFF
pass
@deprecated("Use subscribe()")
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client)
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
+50 -39
View File
@@ -23,40 +23,36 @@ def find_download_url(url: str) -> str | None:
def get_ipdb(ips_path: Path) -> None:
url = find_download_url(DB_IP)
if url is None:
return
date = find_date(url)
month_file = ips_path.parent / ".month"
if ips_path.exists() and month_file.exists():
old_date = month_file.read_text().rstrip("\n")
if old_date == date:
logger.info(f"IP DB date matches: {date}")
try:
url = find_download_url(DB_IP)
if url is None:
return
# TODO: log additional output
logger.info(f"Fetching IPDB for {date} from {url}")
try:
date = find_date(url)
month_file = ips_path.parent / ".month"
if ips_path.exists() and month_file.exists():
old_date = month_file.read_text().rstrip("\n")
if old_date == date:
logger.info(f"IP DB date matches: {date}")
return
# TODO: log additional output
logger.info(f"Fetching IPDB for {date} from {url}")
tmp = serialize(url)
except Exception as e:
logger.critical(e)
return
logger.info(f"Extracting {tmp}")
unzip(tmp, ips_path)
logger.info(f"Extracting {tmp}")
unzip(tmp, ips_path)
logger.info("Stripping IPv6 records")
try:
logger.info("Stripping IPv6 records")
strip_ipv6(ips_path)
with open(month_file, "w") as f:
f.write(date)
logger.info(f"Wrote {date} to {month_file}")
except Exception as e:
# NOTE: in the event of failure, geolocation calc is simply not performed
logger.critical(e)
return
with open(month_file, "w") as f:
f.write(date)
logger.info(f"Wrote {date} to {month_file}")
def find_date(url: str) -> str:
date = re.sub(r"(dbip-city-lite-)(.*)(.csv.gz)", r"\2", url)
@@ -81,19 +77,34 @@ def serialize(url: str) -> Path:
return tmp
# TODO: optimize this function
# consider using grep
# cf. grep -vE "^[a-z0-9]{4}:" | grep -v "::" > "$ip_file"
def strip_ipv6(path: Path) -> None:
ips = []
with open(path, "r") as f:
s = f.read()
"""Can be IO intensive and cause visual lag on UI frames
running in the main thread even when run in its own thread;
lines are batched into memory-manageable chunks to reduce
disk writes. Raw file can be 8M+ records long, so it is
not read into memory at once.
reg = r"^\d{1,3}\..*"
ips = re.findall(reg, s, re.MULTILINE)
assert ips[0].split(",")[0] == "0.0.0.0"
assert ips[-1].split(",")[0] == "224.0.0.0"
with open(path, "w") as f:
for ip in ips:
f.write(ip + "\n")
Relative size is reduced by ~100MB by pruning unwanted columns
"""
# NOTE: "^::," is the boundary line between IPv4 and IPv6
# NOTE: deprecated regex matching (slower by 5s)
# reg = r"^\d{1,3}\..*"
alt_path = path.parent.joinpath("ips_stripped.csv")
merged = ""
its = 0
with open(path, "r") as f, open(alt_path, "w") as out:
for line in f:
els = line.split(",")
if "." not in els[0]:
break
final = ",".join([els[0], els[1], els[-2], els[-1]])
merged += final
its += 1
if its == 500:
out.write(merged)
its = 0
merged = ""
if its > 0:
out.write(merged)
path.unlink()
alt_path.rename(path)
+3 -3
View File
@@ -17,6 +17,7 @@ class Xdg:
system: Path
debug: Path
ips: Path
filters: Path
def is_writeable(path_str: str) -> bool:
@@ -63,9 +64,6 @@ def get_xdg_paths() -> dict:
def parse_filepaths(xdg: dict) -> Xdg:
# cache = xdg["XDG_CACHE_HOME"]
# data = xdg["XDG_DATA_HOME"]
config = xdg["XDG_CONFIG_HOME"]
state = xdg["XDG_STATE_HOME"]
@@ -80,6 +78,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
history = state / "dzg.history"
versions = state / "dzg.versions"
ips = state / "ips.csv"
filters = state / "dzg.filters.json"
return Xdg(
config,
@@ -91,4 +90,5 @@ def parse_filepaths(xdg: dict) -> Xdg:
system,
debug,
ips,
filters
)
+11 -2
View File
@@ -4,6 +4,7 @@ UDP_PORT = 27016
VM_FILE = "/proc/sys/vm/max_map_count"
MIN_COUNT = 1048576
RATE_LIMIT_THRESHOLD = 3
REQUEST_TIMEOUT = 10
APPNAME_DAYZ = "DayZ"
@@ -28,10 +29,17 @@ APP_NAME = "DZGUI"
APP_NAME_LOWER = "dzgui"
APP_NAME_ABBR = "dzg"
HEX_GREEN = "#32CD32"
HEX_RED = "#FF0000"
HEX_ORANGE = "#FFAC1C"
# FIXME: normalize icon names
CARET_DOWN = "go-down-symbolic"
CARET_UP = "go-up-symbolic"
CLIPBOARD = "edit-copy-symbolic"
FOLDER = "folder-symbolic"
EDIT_DELETE = "edit-delete-symbolic"
ERROR = "dialog-error-symbolic"
HELP_BUBBLE = "help-about-symbolic"
INPUT_KEYBOARD = "input-keyboard-symbolic"
LIST_ADD = "list-add-symbolic"
@@ -40,7 +48,6 @@ SEARCH_ICON = "system-search-symbolic"
STEAM_ICON = "steam_tray_mono"
VIEW_CONCEAL = "view-conceal-symbolic"
VIEW_REVEAL = "view-reveal-symbolic"
ERROR = "dialog-error-symbolic"
WARNING = "dialog-warning-symbolic"
WEB_BROWSER = "web-browser-symbolic"
@@ -76,4 +83,6 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
TMP_PATH = "/tmp"
TMP_TARBALL = "/tmp/dzgui.tar.gz"
TMP_EXE = "/tmp/dzgui"
TMP_EXE = "/tmp/dzgui/dzgui"
DAYZ_COMMUNITY_ROOT = "Missions"
+7 -2
View File
@@ -1,6 +1,11 @@
# internal
STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json"
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
STEAM_PUBLISHED_FILES = (
"https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1"
)
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
+7 -5
View File
@@ -103,6 +103,7 @@ class Preferences(EnumWithAttrs):
class NotebookPage(EnumWithAttrs):
OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True}
CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True}
DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False}
HELP = {"crumbs": strings.crumbs._help, "statusbar": True}
@@ -167,7 +168,7 @@ class ContextMenu(EnumWithAttrs):
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
COPY_SERVER_IP = {"label": strings.copy_ip}
COPY_SERVER_NAME = {"label": strings.copy_name}
DELETE_MOD = {"label": strings.delete_mod}
UNSUB_MOD = {"label": strings.unsub_mod}
OPEN_WORKSHOP = {"label": strings.open_workshop}
REFRESH_PLAYERS = {"label": strings.refresh_players}
REMOVE_HISTORY = {"label": strings.remove_history}
@@ -183,7 +184,8 @@ class ContextMenuGroup(Enum):
"""
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = (
ContextMenu.CONNECT,
@@ -248,9 +250,9 @@ class ModButton(EnumWithAttrs):
"label": strings.mod_panel.unhighlight_stale,
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
}
DELETE_SELECTED = {
"label": strings.mod_panel.delete_selected,
"tooltip": strings.mod_panel.delete_selected_tooltip,
UNSUB_SELECTED = {
"label": strings.mod_panel.unsub_selected,
"tooltip": strings.mod_panel.unsub_selected_tooltip,
}
SELECT_STALE = {
"label": strings.mod_panel.select_stale,
+22
View File
@@ -8,10 +8,13 @@ from gi.repository import Gdk, GObject # noqa E402
if TYPE_CHECKING:
from dzgui.const.enum import NotebookPage, ServerTab
from dzgui.model.model_factory import FastInsertListStore
from dzgui.views.pages.offline import FolderHBox
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal
class Emitter(GObject.GObject):
def __init__(self) -> None:
super().__init__()
@@ -150,3 +153,22 @@ class Emitter(GObject.GObject):
def keyword_set(self, keyword: str) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def lan_page_initialized(self) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, str))
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
def custom_mods_unloaded(self, widget: "FolderHBox") -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def invalid_custom_mods(self) -> None:
pass
+75 -24
View File
@@ -20,7 +20,6 @@ from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.mods import ModManager
from dzgui.managers.notes import NoteManager
from dzgui.model.servers import ServerModelManager
from dzgui.util.diag import write_diagnostic
@@ -47,12 +46,13 @@ if TYPE_CHECKING:
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.right_panel import RightPanel
from dzgui.views.components.statusbar import Statusbar
from dzgui.views.pages.offline import OfflineLoader
from dzgui.views.pages.options import Options
from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook
from dzgui.views.trees.tree_log import LogTreeView
from dzgui.views.trees.tree_menu import MenuTreeView
from dzgui.views.trees.tree_mods import ModTreeView
from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView
from dzgui.views.trees.tree_servers import ServerTreeView
@@ -72,6 +72,7 @@ class AppNavigation:
filters: "FilterPanel"
preconnect: "PreConnectionAssistant"
options: "Options"
offline_loader: "OfflineLoader"
class Controller(GObject.GObject):
@@ -84,7 +85,6 @@ class Controller(GObject.GObject):
self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled)
self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init)
# NOTE: suppress requests until entire UI is loaded
self.loaded = False
@@ -219,19 +219,29 @@ class Controller(GObject.GObject):
open_user_workshop(uid, client)
def load_mods(self) -> None:
self.mod_man = ModManager(self)
self.mediator.modtreeview.load_mods()
def uncolorize_mods(self) -> None:
self.mod_man.uncolorize_mods()
mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.uncolorize_mods()
def highlight_stale(self) -> None:
self.mod_man.highlight_stale()
mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.highlight_stale()
def toggle_mod_selection(self, state: bool) -> None:
self.mod_man.toggle_mod_selection(state)
mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.toggle_mod_selection(state)
def delete_mods(self) -> None:
self.mod_man.delete_mods()
def unsub_mods(
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None:
if treeview is None:
view = self.mediator.modtreeview
else:
view = treeview
mod_man = view.get_mod_man()
mod_man.unsub_mods()
def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model()
@@ -258,7 +268,23 @@ class Controller(GObject.GObject):
dialog.run()
def select_colorized(self) -> None:
self.mod_man.select_colorized()
mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.select_colorized()
def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
servers = self.get_servers()
all_filters: dict[str, dict[str, bool]] = {}
trees = (
servers.get_browser(),
servers.get_saved(),
servers.get_recent(),
servers.get_lan(),
)
for tree in trees:
enum = str(tree.get_enum())
filters = tree.get_filter_man().get_filters()
all_filters[enum] = filters
return all_filters
def get_filter_man(self) -> "FilterManager":
"""Each ServerTreeView has an atomic FilterManager"""
@@ -303,8 +329,22 @@ class Controller(GObject.GObject):
self.emitter.emit("servers_loaded", tv.get_enum())
return
self.mediator.statusbar.set_by_context(tv.get_enum(), "")
self.mediator.filters.button_grid.block_toggles(True)
self.populate_filter_prefs()
self.mediator.filters.button_grid.block_toggles(False)
self.emitter.emit("lan_page_initialized")
ServerModelManager(self, tv).load()
def populate_filter_prefs(self) -> None:
tv = self.get_active_treeview()
filters = self.config_man.get_filters(tv)
if filters is None:
return
filter_man = self.get_filter_man()
for filt, state in filters.items():
filter_man.set_filter(filt, state)
self.mediator.filters.set_filters(filters)
def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache
@@ -319,16 +359,7 @@ class Controller(GObject.GObject):
filter_man = self.get_filter_man()
return filter_man.get_filters()
# TODO: rename
def _on_servers_loaded_init(self, emitter: "Emitter") -> None:
"""Triggered after servers load but prior to maps loading"""
# FIXME: wipe maps store when changing tabs if model is none
# e.g. select recent, toggle map, then select lan -> not wiped
tv = self.get_active_treeview()
if tv.loaded is False:
return
store = self.get_map_store()
self.emitter.emit("load_maps", store)
# FIXME: wipe maps store when changing tabs if model is none
def has_server_model(self) -> bool:
treeview = self.get_active_treeview()
@@ -390,9 +421,12 @@ class Controller(GObject.GObject):
saved_tree = self.get_servers().get_saved()
ServerModelManager(self, saved_tree).remove_by_record(record)
def add_to_history(self, record: dict[str, Any]) -> None:
def add_to_history(self, row: dict[str, Any], record: "Record") -> None:
tv = self.get_servers().get_recent()
ServerModelManager(self, tv).add_to_history(record)
ServerModelManager(self, tv).add_to_history((row, record))
# def append_to_history(self, record: "Record") -> None:
# self.config_man.append_to_history_file(record)
def remove_from_history(self, record: "Record") -> None:
# NOTE: remove action is only possible from history tree context menu,
@@ -489,8 +523,11 @@ class Controller(GObject.GObject):
ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind)
def update_and_connect(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(raise_window)
def update_and_load_to_menu(self) -> None:
self.connection_man.update_and_connect(menu_only=True)
def update_and_connect(self) -> None:
self.connection_man.update_and_connect()
def update_status(self) -> None:
self.mediator.preconnect.mark_finished()
@@ -500,3 +537,17 @@ class Controller(GObject.GObject):
def present_window(self) -> None:
self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
def is_cancel_pending(self) -> bool:
if self.get_exit_event().is_set():
return True
if self.get_cancel_event().is_set():
self.clear_cancel_event()
return True
return False
def open_offline(self) -> None:
self.open_page(NotebookPage.OFFLINE)
mod_man = self.mediator.modtreeview.get_mod_man()
store = mod_man.store
self.mediator.offline_loader.populate(store)
+790 -2
View File
@@ -1,3 +1,5 @@
# Changelog
## Added
- Setup wizard
- Changelog text wrapping and formatting
@@ -34,7 +36,11 @@
- Early load alerts button
- Preconnect dialog
- Preconnect warnings/failsafes like filesize
- Save filters per server context between sessions
- Preboot progress dialog
- Choose to jump into splash screen instead of server
- Collapsible connection panel
- Play offline (load mods directly)
## Changed
- Conform to PEP 440 versioning for beta versions
@@ -49,6 +55,9 @@
- Suppress log messages from imported modules
- Embed Workshop link in Options menu
- Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB)
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
## Dropped
- Debug mode
@@ -64,7 +73,6 @@
## Unreleased
- Load offline mods
- Choose to jump into splash screen instead of server
- Local documentation
- Raw debug command in context menu
@@ -72,3 +80,783 @@
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
## [6.0.5] 2026-05-17
## Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.4] 2026-04-13
## Fixed
- Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12
## Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs
## Changed
- Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first
- Rename some internal functions
- Wrap entire startup process in dialog
## [6.0.1] 2026-01-16
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
## [6.0.0] 2026-01-14
## Added
- Support DayZ Experimental
- Ping display on server tables
- Save descriptive text notes on a per-server basis
- Speed up load time and navigation of server tables
- More robust threading and cache system when filtering servers
- Show additional DayZ client information in Options menu
- Warn user of DayZ client version mismatches
- Support clickable hyperlinks in dialogs
- Rich server metadata details dialog (right-click on server table)
- Filter by modded servers
- Breadcrumbs showing current menu context
- Dedicated changelog page
- Dedicated keybindings page
- Dedicated settings page ("Options")
- Support sandboxed version of Flatpak
- Vim-style navigation keybindings
- Added "Return to main menu" button to dialog windows when failing to load server table
- Dynamic context menus for modded servers
- Additional keybindings for new filter toggles
- If a server is locked, prompt user when connecting
- Internal flag to allow distro-packaged releases to disable in-app updates
- Pre-boot checks to test whether script was invoked directly
- Commandline usage help text (GaryBlackbourne)
- More descriptive help dialogs when initial dependencies are missing
## Fixed
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
- Key stickiness when quickly navigating through entries in tables
- Servers returning malformed A2S_INFO blocking server browser from loading
- Normalized buttons in dialogs and restored proper padding
- GTK errors being emitted to stdout when inserting debug table
- Entry dialog sensitivity when validating API keys
- Additional validation on entry dialogs to prevent submitting empty text
- Suppress errors during pre-boot checks when mods are not installed
- Centered filter checkboxes within panel
- Do not pop unhighlight/select stale buttons if no stale mods exist
- Improved keybinding interaction with side panels
- Suppress typeahead search in mod dialogs
- Do not trigger global API cooldown if no LAN/favorite servers are found
- Fix table column expansion in server mod dialogs
- Missing parameters when closing application via window decorations
- Statusbar contents not updating on certain pages
- Prevent debug button from activating when in certain input fields
- Window size not updating correctly when unmaximizing window after changing "fullscreen at boot" setting
- Branch toggle signal being emitted when entering Options menu from other contexts
- Mods rarely not appearing in local mod list if download completed too quickly
- Statusbar not updating when clicking a row after spamming keyboard input
- Extraneous logs being generated when subscribing to mods
- Narrow width of columns in modlist dialogs occluding text
- Newline terminators in history file
- Floating point number calculation
- Window resizing too small if no prior resolution was set
- ESC key destroying wait dialogs while a thread is pending
- Tooltip signals being erroneously emitted on main menu
- UI not being constructed correctly if CHANGELOG.md was missing
- Config file erroneously getting updated when populating settings menu
- Leaky variable name in dialog titles
- Prevent extraneous signals from propagating when column width is adjusted
- Server filter panel not being hidden when entering other page contexts
- Path to remote changelog being constructed incorrectly
- Dialogs with newlines breaking output in log table
- Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## Changed
- Require Python 3.13
- Reduced global API cooldown from 60s to 30s
- Clarify dialog messages when DayZ path could not be found
- Auto-focus first item when opening context menus
- Opacity setting on side buttons when in a different context
- Refactored BM API key validation to account for new key format
- State file serialization methods
- Optimize time complexity of pre-boot checks (GaryBlackbourne)
- Rewrote distance calculation module (GaryBlackbourne)
- Changed preferred client setting from radio toggle to combobox
## Dropped
- Ping readout in statusbar
- Extraneous information from right statusbar
## [5.8.3] 2026-01-04
## Fixed
- Normalize checksum numbers and dates
## [5.8.2] 2026-01-04
## Fixed
- Fix regression in symlink routine introduced by previous hotfix
## [5.8.1] 2025-12-27
## Fixed
- Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## [5.8.0] 2025-07-06
## Added
- Filter servers by official/unofficial status
- Automatically fetch geolocation records
## Changed
- Updated internal versioning of helper files
## Fixed
- Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17
## Added
- Save application dimensions when quitting and restore on subsequent boot
## Fixed
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
## Changed
- More direct server ping query method that should return marginally more accurate times
## [5.6.7] 2025-04-04
## Dropped
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
## [5.6.6] 2025-03-16
## Changed
- Update IP database records for 2025-03
## [5.6.5] 2025-03-04
## Fixed
- Livonia server results being dropped from batch queries
## [5.6.4] 2025-02-10
### Changed
- Drop launch flag and check for invocation through Steam automatically
## [5.6.3] 2025-02-09
### Changed
- Update IP database records for 2025-02
## [5.6.2] 2025-01-22
### Fixed
- Resolve regression introduced with IP resolution feature in 5.6.0 (restores functionality of right-click action: Add to My Servers)
## [5.6.1] 2025-01-10
### Fixed
- Add fallback support for jq 1.6
## [5.6.0] 2025-01-06
### Added
- Application header bar and controls
- Menu context subtitle in header bar
- Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode
- Highlight stale mods in mods list
- Added --steam launch parameter
- Open Steam workshop subscriptions dialog when switching from Manual to Auto mode
- Hover tooltips to most buttons
- Add in-app documentation link to Codeberg mirror
- Additional logging
### Fixed
- Avoid sudo escalation if system map count is sufficient (jiriks74)
- Duplicate dialog title on Steam Deck
- Resolve remote IP when saving records for game servers with multiple hosts
- More performant symlink traversal when checking for legacy links
- Only iterate on missing symlinks at boot (400% performance uplift)
- Fix for server list truncation causing some servers to not appear in results
- Empty dialog popups if user manually deletes local mods while application is running
- Update statusbar when removing servers from list/repopulating
- Abort DayZ path discovery if Steam files are not synched
- Race condition when checking for installed mods
- Favorite server message not updating correctly
- Improved handling for cases where there are no locally installed mods
- Set up mod symlinks at boot, rather than only on server connect
- Prevent context menus from opening when table is empty
- When reloading table in-place, prevent duplicate panel elements from being added if already present
- Clean up local mod signatures from versions file when deleting mods
- Focus first row when opening mods list
### Changed
- Redact usernames in log files
- Prevent ArrowUp/ArrowDown input when inside keyword field
- Admonish user to restart Steam in error dialog if DayZ path could not be found
- Refactor control flow for more robust contextual parsing
- Stop sending modal dialog hints to outer window
- Clean up signal emission
- Clarify some error messages and normalize text formatting
- Update forum URL
- Update README.md
- Update IP database to 2025-01
- Reduce IP database size by 50%
- Update documentation to 5.6.x standard
### Dropped
- Hall of Fame section from button links, to be moved inside documentation
- Unused imports
## [5.5.3] 2024-12-13
### Fixed
- Add remote resource health checks before downloading updates
### Added
- Add fallback repository
## [5.5.1] 2024-12-03
### Fixed
- Support localized decimal separators when parsing installed mod sizes
## [5.5.0] 2024-11-10
### Added
- Support servers running DLC content (fixes Frostline servers)
- Expose a toggle setting for whether to launch the application in fullscreen
- Text autocompletion in maps search field (partial search)
- Add disk space warning to popup dialog when downloading mods
### Fixed
- Servers in saved servers list would populate context menu with same option when right-clicking in server browser
- Enable adding/removing servers to/from My Saved Servers when in Recent Servers context
- Prevent maps combobox from duplicating contents
- Restore keyboard input to keyword entry field
### Changed
- Abort fallback query method if DLC is required
## [5.4.2] 2024-10-05
### Fixed
- Sanitize third-party API IDs to remove UGC collisions
## [5.4.1] 2024-09-25
### Added
- Pre-boot validation check for users with self-compiled version of jq
### Fixed
- Use fallback logic for modlist queries when user traverses networks
- Fix signal handling control flow for checkbox toggles
- When reloading the server browser, the map combobox selection would revert to the last selected map instead of All Maps
- Server filter toggle signals were accessible from the main menu when switching between menu contexts
- Global cooldown dialog could sometimes block filter toggles after cooldown reset
- Normalized minor version number due to a previous clerical error
## [5.4.0] 2024-08-27
### Added
- Scan local area network for DayZ servers
- Freedesktop application icons for system taskbar, tray, and other dialogs
- Emit CPU model name when exporting system debug log
### Fixed
- Errors being printed to the console when Exit button was explicitly clicked
- Detect Steam Deck OLED APU variant during initial setup
- Encapsulate player names correctly so that names with whitespace in them are supported
- Test if DayZ directory is empty at startup, implying that the game was moved to a new library collection
- Report WM_CLASS name to the window manager
## [5.3.2] 2024-07-02
### Fixed
- Server list would sometimes be missing some expected servers due to truncation being caused when a server erroneously set an incomplete gametype
## [5.3.1] 2024-06-15
### Added
- Uninstall routine: invoke via dzgui.sh -u or dzgui.sh --uninstall and choose from full or partial uninstall
## [5.3.0] 2024-06-15
### Added
- Server browser tables now display the number of players in queue (for full servers)
- Warning dialog if the user's API key returned no servers, and a global API cooldown dialog (60 seconds)
### Fixed
- Removing servers from My Servers via the right-click context menu was not actually removing the server and drawing an empty dialog
- Resolved an issue where, when Steam was not installed in a standard location and the user was prompted to let DZGUI auto-discover the correct path, this information was not retained correctly
## [5.2.4] 2024-05-29
### Fixed
- Resolved an issue that could cause an old path to a prior DayZ installation to be reinserted in the config file if the Steam client did not synch the new path internally. DZGUI would try to ask the user to re-run first-time setup and update the path, but the old path would still be used.
## [5.2.3] 2024-04-19
### Added
- Added Debug Mode button to main menu
### Changed
- Reworded debug mode notification dialog
- Updated geolocation records
### Dropped
- Dropped seen_news key from configs
## [5.2.2] 2024-04-18
### Added
- Cover artwork/icons for Steam "Recent Games" and tree view
### Changed
- Updated documentation to the v5.0.0 spec
## [5.2.1] 2024-04-01
### Fixed
- Fixed a regression where where the first-time setup dialog would not trigger auto-path discovery
### Changed
- Reworded some menus and dialogs for clarity
## [5.2.0] 2024-03-21
### Added
- Refresh player count for active row: invoke via right-click context meu or directly with the Ctrl-r hotkey. This feature has a 30 second global cooldown to prevent throttling.
### Fixed
- Improve case-insensitive keyword search to be portable across awk versions (previous version required gawk)
- Fixed a dialog string from being shown twice when adding a server to favorites via context menus
### Changed
- Use a more robust method for downloading mods when auto mod install is enabled
## [5.1.1] 2024-03-18
### Fixed
- Hotfix for remote helper files not being fetched correctly
## [5.1.0] 2024-03-18
### Added
- Make columns in the server browser user-resizable (affects Server Browser, My Servers, and Recent Servers)
- Save dragged position of user-resized columns
- Display ping to server in statusbar: by popular request, added the ability to visualize both distance to server and round-trip latency (ping), at the cost of a small calculation delay. Please leave feedback regarding whether this feature feels fast/responsive enough.
### Fixed
- Fixed a rare scenario in Auto Mod Install Mode where defunct mods (mods no longer available on Steam) would try to be downloaded if the user had previously downloaded the mod
## [5.0.0] 2024-01-31
### Added
- Context switching: navigate to different pages using side buttons
- Dynamic statusbar: updates metadata and server distance when selecting rows
- Show server-side modlist and allow jumping to Steam Workshop pages to browse, and list whether mod is currently installed
- Print debug logs in-app (Help > Show debug log)
- Functionality to change API keys in-app (used when revoking old API keys)
- Print atomic mod sizes when listing installed mods
- Dialogs show direct links to API key management URLs when changing API keys
- Toggle dry-run mode directly from server browser
- Extensive keybindings for fully controlling the application without the mouse
- Extensive pre-boot sanity checks
- Keybinding help dialog in main menu
- Right-click context menus in server browsers/mod list: add/remove from favorites, show server-side mods, delete mod, copy server IP to clipboard
### Changed
- Utilize GTK bindings and MVC paradigm for UI creation and data flow
- Filter servers dynamically from within server browser
- Performance and security improvements to DZGUI helper files
### Fixed
- First-time setup dialogs respawning repeatedly in certain scenarios
- Issues with Steam client switching to the wrong page when using auto-mod install
- Set text input module correctly when launching on Steam Deck
- Separate current/total player count and use proper integer sort method in table
## [4.1.1] 2023-12-18
### Fixed
- News marquee not showing
## [4.1.0] 2023-12-17
This update adds support for DayZ servers running on a local area network. To connect or add to your server list, supply the server IP and query port in the format IP:PORT.
Support for Steam Deck Game Mode has also been restored, with a new dialog format that allows for virtual keyboard input. This should allow you to enter text in form fields by activating the Steam button + X. In addition, you can unlock the mouse and keyboard input when launching DZGUI through Steam by using an internal binding provided by Steam Deck: long-press the three dots button on the right of the device for three seconds to toggle the input state. This allows you to use mouse and keyboard-style bindings on Game Mode and vice versa.
### Added
- Virtual keyboard support for text entry fields on Steam Deck Game Mode
- Validate and connect to LAN server IPs
## [4.0.4] 2023-12-23
### Fixed
- Enforce version check for Python versions before 3.10
## [4.0.3] 2023-11-22
### Fixed
- Query helper: backwards compatibility for pre-2021 versions of Python 3
## [4.0.2] 2023-11-22
### Fixed
- Query helper not loading: fixed a remote link pointing to the wrong destination and added a checksum verification to ensure file is present
## [4.0.1] 2023-11-22
### Fixed
- Emergency hotfix to remove build artifacts leaking into main script: if you updated DZGUI from 3.3.18 to 4.0.0 between 2023-11-22 15:00:02 and 2023-11-22 15:03:37 GMT, there is a small chance it will be unable to launch correctly. If so, please follow the instructions on the manual to reinstall.
## [4.0.0] 2023-11-22
Hello players, this is a major version update which overhauls many of DZGUI's underlying systems to improve responsiveness of the application and make menus more intuitive to interact with. It should be considerably more difficult, if not impossible, to inadvertently crash a dialog, and nested dialogs should behave in a more expected fashion, such as when going back and forth between menus or changing options dynamically within a given menu.
In addition, with this update we are querying servers directly. The net benefit of this is that results will returned faster, bootup is faster, and it also paves the way for future features like connecting to servers on a LAN. The Battlemetrics API is now entirely optional and can be skipped during the setup process. However, if you have already set up a BM API key, you can continue using it if you want to query or add servers by ID instead of IP.
As part of this change, version 4.0.0 introduces the ability to connect to or add servers to your list based on either ID or IP. Previously, you could only connect by IP and add by IP, but now you can connect by IP/ID or add by IP/ID. If you choose the ID method, this will be translated into an IP seamlessly in the background. Similarly, favorite servers are now stored using the full IP rather than the ID. Due to the variety of systems and methods for connecting to servers, the application was carrying around and converting server IPs, IDs, and other formats back and forth, creating unnecessary complexity. By normalizing everything to an IP basis, maintainability should be more consistent. When upgrading to this version, your old favorites lists will be updated automatically to the new IP method. Note that as a result of this change, we must purge old history lists ("Recent Servers"), but everything else should carry over as before.
If you encounter any problems with this new release or with the migration of configs, please do not hesitate to submit a bug report.
Attention Fedora 38 users: problems with upstream GNOME packages causing crashes have been reported to GNOME development and a fix has been issued. You have the choice of compiling the zenity package
from source or waiting until the latest version is merged into Fedora's package manager.
### Added
- Change in game name: dynamically change your profile name via the Advanced Options menu
- Connect by ID: supply a Battlemetrics ID to connect to a server; this can be used in lieu of the IP
- Add by IP: supply a standard IP to add a server to your list; this can be used as a more direct way of saving servers
- Filter by 1PP and 3PP in the server browser
- Save connected server to favorites: prior to connecting, asks the user if they want to save this server for future use
- Generate additional output when generating system logs
### Fixed
- Rare cases where the keyword filter would not filter server results correctly
- Handling of dialog exit signals: made it much more difficult to crash the application in rare cases when spamming input or returning from menus
- Update menus in place: when toggling options in the Advanced Options menu, displays the current state/mode of the option for better readability into what option is currently enabled
- More intuitive menu navigation: dialog setup and teardown is more responsive and follows expected flows when inside of nested menus
- Message formatting: fold long messages inside of popup dialogs for proper word wrapping regardless of screen type
- Fixed a rare case where dialogs would spawn twice during first-time setup
- Properly remain inside of menus when looping where it would make sense to do so : e.g., Delete Servers list, Advanced Options
### Changed
- Query servers directly to reduce API hops: initial bootup and subsequent server queries should be considerably faster
- Store complete IP:Port instead of server IDs
- Make Battlemetrics API key optional: this is only used for the 'Connect by ID' and 'Add server by ID' methods and is not required. If you prefer, you can simply connect/add by IP.
- Prevent the application from launching in Game Mode on Steam Deck: Steam Deck's kiosk mode has problems sending keyboard input to third party applications. To prevent unintended usage, DZGUI now warns the user to launch the app in Desktop Mode if they attempt to use it from Game Mode. Adding DZGUI as a Non-Steam Game does work on desktop PCs, but is not recommended due to the way Steam handles subshells. For best results, launch DZGUI directly via the script/applications menu (PC) or via the desktop icon (Steam Deck).
- Omit null servers from list: servers that time out or send an empty response are now omitted entirely from the My Servers list, as they will not return meaningful metadata unless they are online.
The My Servers list thus shows online and accessible servers
## [3.3.0] 2023-05-16
### Added
- Fetch more inclusive global "players in-game" count
- List mod directory on installed mods list
- Detect default Flatpak Steam path
- Dark mode/light mode theme to help file
- Alpha-sort My Servers list
- Add description of how to enable hidden folders on GTK2/3
- Initial logging framework
### Changed
- Test for wmctrl when enabling full auto mod installation
- Steam Deck: block toggling full auto mod installation due to extra dependencies needed
- First-time setup: sudo escalation when checking system map count for the first time
### Fixed
- Steam Deck: non-ASCII delimiter causing setup menu to despawn on some devices
- Don't add items in My Servers multiple times to array when the list of favorites is paginated
- Trigger progress dialogs sooner and in sequence to reduce appearance of visual lag
- First-time setup: break out of dialogs correctly when user backs out
- First-time setup: break out of automatic path discovery when user specifies a path manually
- More portable interpreter invocation
- Properly size down window resolution when returning from server browser
## [3.2.10] 2023-05-11
### Fixed
- Return from lockfile function if first-time setup has not been run
- Sanitize inputs when using file picker
- Require both wmctrl and xdotool
## [3.2.9] 2023-05-10
### Changed
- Reword button to "Choose path manually" instead of "Retry"
## [3.2.7] 2023-05-10
### Changed
- Better sudo escalation within zenity dialogs if vm map count is too small
## [3.2.6] 2023-05-10
### Fixed
- Don't parse Flatpak symlinks when setting up default Steam path
## [3.2.5] 2023-05-07
### Fixed
- Require sudo when checking vm map count
## [3.2.4] 2023-03-01
### Fixed
- BM API returning stale query port and preventing fetching modlist
## [3.2.3] 2023-02-17
### Fixed
- sysctl map count value not being loaded immediately after setting
- Application terminating when user declines to update map count value
- Erroneous stderror output when flatpak is not installed
## [3.2.0] 2023-01-19
### Added
- Support Flatpak version of Steam
## [3.1.8] 2023-01-18
### Fixed
- Progress window blocking rest of window stack
- Bug when updating old mods if automod set to ON
## [3.1.7] 2023-01-06
### Fixed
- Hotfix for xdotool repeating input
## [3.1.6] 2023-01-01
### Changed
- Tick low pop servers by default
## [3.1.5] 2023-01-01
### Fixed
- Validate BM key on initial setup
- Fix history menu not parsing query ports correctly
### Changed
- More permissive Steam client discovery for tiling WMs
## [3.1.4] 2022-12-10
### Fixed
- Issue #43: Hotfix for workspace-driven WMs
## [3.1.3] 2022-12-06
### Fixed
- Explicitly require Python 3
## [3.1.1-2] 2022-12-03
### Fixed
- Fix lockfile path
## [3.1.0] 2022-12-03
### Added
- Recent connect history
- Simple, OS-agnostic automod installation
- Track local mod versions
- Force update local mods option
- Added python to dependencies
- File-picker driven path discovery on initial setup
### Dropped
- Headless mod installation
- Drop server ID field requirement on initial setup
### Changed
- Clean up main menu options
- Enforce Steam API key on initial setup
- More accurate path discovery on initial setup
- Add thousands separator to player counts in server browser
### Fixed
- Initial setup dialog causing early crash
- Improved error handling on initial setup to avoid malformed config files
- Delete server menu not clearing when returning to main menu
- Handle whitelist deletion when only one entry present
- Include path to drives under /run in path discovery
- Use Steam-safe local zenity version
## [3.0.7] 2022-11-25
### Fixed
- Hotfix for server reporting multiple versions of same mod
## [3.0.6] 2022-11-09
### Changed
- More verbose logs
## [3.0.5] 2022-10-27
### Fixed
- Properly create .desktop file on desktop PCs
## [3.0.3-4] 2022-10-16
### Fixed
- Steam Deck path discovery on first-time setup
## [3.0.2] 2022-10-12
### Fixed
- Size of certain popups on Steam Deck
## [3.0.1] 2022-10-12
### Fixed
- Initial popup size on Steam Deck
## [3.0.0] 2022-10-12
### Added
- Foreground progress of manual mod subscriptions
- Automatic mod helper through steamcmd
- Forum link
- Enforce Steam API
### Dropped
- Stop retrieving extra metadata from BM
### Changed
- Reorder main menu
- More verbose error messages
- Better abstraction of URLs
### Fixed
- Width and text of some popups on Steam Deck
## [2.7.2] 2022-10-07
### Fixed
- Fix internal URL
## [2.7.1] 2022-10-05
### Fixed
- Game launch not kicking off after symlink creation
## [2.7.0] 2022-10-04
### Added
- Server browser and geolocation algorithm
- More verbose error codes
- Additional progress bar setup and destruction throughout the application
- Additional API response validation
### Fixed
- Encapsulate title strings to prevent leaky arguments in title bars
- Remove erroneous slow boot process if first-time setup was already complete
- Delete server list not emptying when returning to main menu
### Changed
- Group main menu entries in advance of future functionality
- Better abstraction of paths
- Clarification of certain options and errors
## [2.6.3] 2022-10-02
### Fixed
- Hotfix for connect-to-fav not getting modlist
## [2.6.2] 2022-10-02
### Fixed
- Hotfix for BM API returning malformed publishedfileids
## [2.6.1] 2022-09-25
### Fixed
- Freedesktop shortcut errors
## [2.6.0] 2022-09-05
### Added
- Connect by IP method
## [2.5.1] 2022-08-17
### Fixed
- Hotfix for malformed paths during first-time setup
## [2.5.0] 2022-08-16
### Added
- Lockfile: prevent concurrent instances of DZGUI from being opened
### Changed
- Faster path discovery on first-time setup
### Fixed
- Symlink collision on servers with many mods
- API response pagination for large server lists
## [2.4.1] 2022-08-09
### Fixed
- Hotfix for progress bar breaking table when >9 servers in list
## [2.4.0] 2022-08-07
### Added
- Delete server command added to main menu
- Write log to file for bug reports
- Prompt to permanently increase map count size
- Dependency check for Steam
- Cover artwork
- News backend for OTA updates
- Clean up stale symlinks when checking mods
### Fixed
- Minify long mod launch params for servers with launch params breaking the upper limit
- Port DZTUI method of handling legacy symlinks
- Prevent user from entering invalid data on first-time setup
- Prompt to re-run first-time setup if config is malformed
- Better handling of field output from table
- Better Steam Deck detection and handling
- Prevent garbage in error messages
- Miscellaneous backend improvements
### Changed
- Enforce download when switching branches
## [2.3.2] 2022-08-04
### Fixed
Set branch flag to 'stable' if no config file present
## [2.3.1] 2022-08-04
### Fixed
- Improved error handling of first-time setup fields
- Interpolate config file values for debug, branch when writing file
### Changed
- Prompt user to restart first-time setup if broken config is found
## [2.3.0] 2022-07-18
### Added
- Numbered mod links in browser
- Admonition to upgrade versions for bug fixes
- Toggle branch between stable/testing
### Fixed
- Hotfix for fav server select on main menu
## [2.2.1] 2022-07-17
### Fixed
- Hotfix for upstream API returning malformed modlists
- Handle servers with no mods
- Fix dialog window depending on browser exit
- Remove stray newlines in config file
## [2.2.0] 2022-06-22
### Added
- Toggle debug mode in-app
## [2.1.0] 2022-06-19
### Changed
- Updated link to new documentation
## [2.0.3] 2022-06-17
### Fixed
- Safer expansion of originating script path
## [2.0.2] 2022-06-16
### Fixed
- Remove enforced runtime check of workshop path
## [2.0.2] 2022-06-16
### Fixed
- Expansion of Steam path prefix when default path was found
## [2.0.1] 2022-06-16
### Fixed
- Regenerate mod links file in browser when clicking dialog
- Reset whitelist when canceling one-shot (fav) mode
## [2.0.0] 2022-06-15
### Added
- Attempt to find DayZ path and write to config on first launch
- Merge existing config values into new config format when upgrading version
- Dynamically detect Steam Deck and set launch parameters
- Add "gametime" column to server list
- One-shot mode to open mod links in browser if using desktop
- Add favorite server from main menu
- Update favorite server changes in real time
- Add link to help pages from main menu
- Add link to changelog from main menu
### Changed
- Don't require duplication of fav server in whitelist
- More permissive truncation of long server names (50 char limit)
- Render mod list as a scrollable menu
- Reword errors for greater verbosity
- Initial support of granular error handling for API response codes
- Move extended path variables out of user config
### Fixed
- Check if mod dir is sane before listing mods
- Prevent favorite server launch if none set
- Reset server list to entire whitelist if canceling out of fav connect
- Do not load table on empty API response and warn user
- Suppress stderr cruft in logs and use logger instead
## [1.2.1] 2022-06-12
### Fixed
- Print the entire changelog
- Add confirmation dialog before run
## [1.2.0] 2022-06-12
### Changed
- Mod validation process now uses faster, single-pass API query
- Improved logger output when setting fav server
### Added
- In-app changelog
## [1.1.2] 2022-06-08
### Fixed
- Fix array used for mod concatenation; fetch post-sanitized list of mods
## [1.1.1] 2022-06-05
### Fixed
- Stricter regex to parse upstream version number
## [1.1.0] 2022-06-05
### Added
- Main menu: fav server label on header
- Main menu: add servers by ID directly into config file
- Main menu: link to report a bug
- Main menu: quick connect to fav server
- Connect: mod compatibility check
- Connect: mod download prompt
- Connect: mod auto symlinks
- New version download prompt
- Additional visualization of progress/menus
### Fixed
- Menu recursion when navigating backwards
### Changed
- Reduced ping timeout interval
-5
View File
@@ -13,11 +13,6 @@ logger = logging.getLogger(APP_NAME)
def get_local_coords(path: "Path") -> Coords | None:
try:
my_ip = get_local_ip()
except Exception as e:
logger.warn(e)
return None
try:
return get_coords(path, my_ip)
except Exception as e:
logger.warn(e)
+1 -4
View File
@@ -1,12 +1,10 @@
import logging
import sys
from pathlib import Path
from dzgui.config.query import lookup
from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH
from dzgui.const.enum import Preferences
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
import dzgui.api.pefile as PeFile
@@ -19,5 +17,4 @@ def is_dayz_installed(config: Path) -> None:
PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ)
except Exception as e:
logger.critical(e)
EarlyAlertDialog(str(e))
sys.exit(1)
raise e
+51
View File
@@ -0,0 +1,51 @@
import platform
import sys
from pathlib import Path
from warnings import deprecated
LIB = "libgirepository-2.0"
@deprecated("currently unused")
def is_debian() -> bool:
"""
cf.
NAME="Debian GNU/Linux"
ID=debian
NAME="Ubuntu"
ID=ubuntu
ID_LIKE=debian
NAME="Linux Mint"
ID=linuxmint
ID_LIKE="ubuntu debian"
NAME="Pop!_OS"
ID=pop
ID_LIKE="ubuntu debian"
"""
release = platform.freedesktop_os_release()
strings = ["ubuntu debian", "debian"]
try:
self_id = release["ID"]
if self_id == "debian":
return True
id_like = release["ID_LIKE"]
return id_like in strings
except Exception:
return False
def has_libgi() -> bool:
parent_dir = Path("/usr/lib64")
exists = any(LIB in subdir.name for subdir in parent_dir.iterdir())
return exists
def test_missing_lib() -> bool:
msg = f"System is missing the required library '{LIB}'. Install it via your system package manager."
if has_libgi() is False:
print(msg)
sys.exit(1)
+21 -9
View File
@@ -1,6 +1,7 @@
import shutil
from pathlib import Path
from typing import Any
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH
from dzgui.config.convert import rc2json
from dzgui.util._json import read_json, write_json
@@ -17,17 +18,28 @@ def has_new_config(config: Path) -> bool:
return config.exists()
def migrate_cols_file(res: Path) -> None:
# NOTE: dzg.columns.json is API 7 spec
old_res = Path.home() / LEGACY_COLS_PATH
if old_res.is_file():
j = read_json(old_res)
cols = j["cols"]
if "View" in cols:
return
def convert_cols_file(res: Path) -> Any | None:
j = read_json(res)
cols = j["cols"]
# NOTE: implies prior conversion
if "View" in cols:
return None
# NOTE: user may not have changed these widths in API 6
try:
cols["View"] = cols.pop("Perspective")
cols["Max"] = cols.pop("Maximum")
write_json(j, res)
except Exception:
pass
return j
def migrate_cols_file(res: Path) -> None:
# NOTE: filename "dzg.columns.json" is API 7 spec
old_res = Path.home() / LEGACY_COLS_PATH
if old_res.is_file():
j = convert_cols_file(old_res)
if j is not None:
write_json(j, res)
def copy_state_files(state_path: Path) -> None:
+7 -17
View File
@@ -6,15 +6,11 @@ import warnings
from typing import TYPE_CHECKING
from dzgui.api.mods import remove_stale_signatures
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences
from dzgui.config.ipdb import get_ipdb
from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.coords import get_local_coords
from dzgui.init.dayz import is_dayz_installed
from dzgui.init.flock import lock_acquire
from dzgui.init.migrate import (
has_new_config,
@@ -24,17 +20,16 @@ from dzgui.init.migrate import (
from dzgui.init.prefix import get_version
from dzgui.init.prereqs import has_steam_client
from dzgui.strings import boot
from dzgui.init.update import check_updates
# from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale
from dzgui.util.symlink import rebuild_symlinks
from dzgui.util.strings import init, flags
from dzgui.views.base import App
from dzgui.views.dialogs.wizard import SetupWizard
from dzgui.views.dialogs.boot import BootWindow
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.views.dialogs.wizard import SetupWizard
if TYPE_CHECKING:
from pathlib import Path
@@ -112,14 +107,15 @@ def main() -> None:
copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard
SetupWizard(_is_steam_deck, XDG.config)
# NOTE: implies that setup wizard failed or was closed
if has_new_config(XDG.config) is False:
return
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
f.truncate(0)
latest_release = check_updates(version)
if _is_steam_deck is False:
# TODO: sudo escalation dialog
# count = get_map_count()
@@ -127,16 +123,10 @@ def main() -> None:
if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam)
# NOTE: clear versions file of unlinked mods
rebuild_symlinks(XDG.config)
remove_stale_signatures(XDG.config, XDG.version)
bootwin = BootWindow(XDG, version)
local_coords, latest_release = bootwin.get_results()
is_dayz_installed(XDG.config)
# TODO: handle IP DB failure and use coords fallback
get_ipdb(XDG.ips)
local_coords = get_local_coords(XDG.ips)
use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs(
is_steam_deck=_is_steam_deck,
is_game_mode=_is_game_mode,
+34 -7
View File
@@ -1,6 +1,7 @@
import logging
import traceback
from collections import deque
from typing import Any, TYPE_CHECKING
from dzgui.const.constants import (
@@ -15,6 +16,7 @@ from dzgui.api.probe import test_steam_api, test_bm_api
from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.strings import dialogs
from dzgui.util._json import read_json, write_json
import gi
@@ -22,7 +24,6 @@ import gi
gi.require_version("Gtk", "3.0")
from gi.repository.Gtk import main_quit # noqa E402
# import dzgui.api.servers as Servers
if TYPE_CHECKING:
from dzgui.config.userprefs import UserPrefs
from dzgui.controllers.mc import Controller
@@ -53,11 +54,25 @@ class ConfigManager:
ips.append(record)
self.update_config(Preferences.IP_LIST, ips)
def update_history_file(self, records: list[Any]) -> None:
def update_history_file(self, fqip: str) -> None:
with open(self.prefs.paths.history, "r") as f:
ips = [line.rstrip() for line in f.readlines()]
seen = set()
ips.append(fqip)
# NOTE: Preserve linear order of records while deduplicating
unique = deque(maxlen=10) # type: ignore
for i in range(len(ips) - 1, -1, -1):
if ips[i] not in seen:
seen.add(ips[i])
unique.appendleft(ips[i])
# NOTE: appendleft will push 11th item off right edge
if len(unique) == 10:
break
with open(self.prefs.paths.history, "w") as f:
for record in records:
addr = f"{record[7]}:{record[8]}"
f.write(f"{addr}\n")
for record in unique:
f.write(f"{record}\n")
def remove_saved_server(self, record: str) -> None:
ips = self.lookup(Preferences.IP_LIST)
@@ -80,8 +95,7 @@ class ConfigManager:
logger.critical(e)
raise e
# TODO: strings
@call_on_thread("Checking api")
@call_on_thread(dialogs.checking_api)
def update_api_key(self, key: Preferences, text: str) -> None:
if key is Preferences.STEAM:
res = test_steam_api(text)
@@ -160,6 +174,9 @@ class ConfigManager:
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
columns = tv.get_columns()
filters = self.controller.get_all_tree_filters()
filters_file = self.prefs.paths.filters
columns_file = self.prefs.paths.columns
try:
data = read_json(columns_file)
@@ -174,6 +191,7 @@ class ConfigManager:
try:
write_json(data, columns_file)
write_json(filters, filters_file)
except Exception as e:
logger.critical(e)
@@ -217,3 +235,12 @@ class ConfigManager:
h = WINDOW_DEFAULT_Y
logger.info(f"Using default window size {w},{h}")
window.set_default_size(w, h)
def get_filters(self, tv: "ServerTreeView") -> Any | None:
filters = self.prefs.paths.filters
try:
enum = tv.get_enum()
filters = read_json(filters)
return filters[str(enum)]
except Exception:
return None
+43 -25
View File
@@ -12,9 +12,10 @@ import dzgui.api.servers as Servers
from dzgui.api.steam import (
connect,
enqueue_mod,
get_remote_signatures,
get_needs_update,
load_to_menu,
subscribe,
)
from dzgui.api.mods import (
@@ -29,6 +30,7 @@ from dzgui.const.constants import (
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
RATE_LIMIT_THRESHOLD,
)
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.init.proc import is_dayz_running, is_steam_running
@@ -74,6 +76,7 @@ class Prerequisites:
steam_proc: SteamProcess
mods: list[list[str]]
game_mode: bool
is_last_server: bool
class ConnectionManager:
@@ -86,6 +89,7 @@ class ConnectionManager:
self.record: Record
self.workshop: Path
self.client: str
self.remote_mod_ids: list[str] = []
self.missing_mods: list[tuple[str, str, int, int]] = []
@@ -161,7 +165,7 @@ class ConnectionManager:
if len(self.missing_mods) > 0:
required_size = sum(row[2] for row in self.missing_mods)
required_mib = format_mib(required_size)
free_mib = format_mib(required_size)
free_mib = format_mib(free)
dayz_running = is_dayz_running()
@@ -169,9 +173,12 @@ class ConnectionManager:
client = self.controller.query_config(Preferences.CLIENT)
running = is_steam_running(client)
steam_proc = SteamProcess(client_name, running)
self.client = client
game_mode = prefs.is_game_mode
is_last = self.is_last_server()
prereqs = Prerequisites(
name=info.server_name,
appid=info.game_id,
@@ -186,11 +193,23 @@ class ConnectionManager:
steam_proc=steam_proc,
mods=remote_mods,
game_mode=game_mode,
is_last_server=is_last,
)
func = StoredFunc(self.controller.open_connection_assistant, prereqs)
self.thread_man.set_cleanup_func(func, destroy_first=True)
def is_last_server(self) -> bool:
prefs = self.controller.get_prefs()
history = prefs.paths.history
try:
lines = history.read_text().splitlines()
last = lines[-1]
current = Servers.record_to_fqip(self.record)
return last == current
except Exception:
return False
@call_on_thread(dialog.querying)
def query_details(self, record: Servers.Record) -> None:
details = Servers.get_details(record)
@@ -244,11 +263,13 @@ class ConnectionManager:
dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run()
def _connect_steam(self) -> None:
def _connect_steam(self, menu_only: bool) -> None:
addr = f"{self.record.ip}:{self.record.gameport}"
playername = self.controller.query_config(Preferences.NAME)
client = self.controller.query_config(Preferences.CLIENT)
rc = connect(client, addr, self.appid, playername, self.remote_mod_ids)
if menu_only:
rc = load_to_menu(self.client, self.appid, playername, self.remote_mod_ids)
else:
rc = connect(self.client, addr, self.appid, playername, self.remote_mod_ids)
if rc != 0:
# TODO: log/pop the error
func = StoredFunc(self.controller.update_status)
@@ -258,8 +279,8 @@ class ConnectionManager:
self.thread_man.show_cancel(False)
self.thread_man.update_dialog(waiting_for_launch)
while True:
# TODO: check cancel event
if self.controller.get_exit_event().is_set():
# FIXME: cancel should not be visible here per setting above
if self.controller.is_cancel_pending():
# TODO: some facility to also close spawned steam process
return
if is_dayz_running():
@@ -270,33 +291,30 @@ class ConnectionManager:
self.thread_man.set_cleanup_func(func)
def _add_to_history_and_return(self) -> None:
self.controller.add_to_history(self.history)
self.controller.add_to_history(self.history, self.record)
self.controller.open_page(NotebookPage.SERVERS)
def _update_mods(self, raise_window: bool) -> None:
# NOTE: fast enqueue all mods in auto mode
def _update_mods(self, menu_only: bool = False) -> None:
prefs = self.controller.get_prefs()
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
for title, mod, stamp, size in self.missing_mods:
# TODO: check cancel and exit events
enqueue_mod(mod, self.appid)
time.sleep(2)
if raise_window is True:
logger.info("Bringing window to foreground")
GLib.idle_add(self.controller.present_window)
if self.controller.is_cancel_pending():
return
subscribe(key, int(mod))
time.sleep(RATE_LIMIT_THRESHOLD)
for title, mod, stamp, size in self.missing_mods:
mod_path = self.workshop / mod
# NOTE: Steam updates mod chunks in parallel, will finish at the same time
while mod_path.is_dir() is False:
if self.controller.is_cancel_pending():
return
time.sleep(1)
while True:
if self.controller.get_exit_event().is_set():
return
if self.controller.get_cancel_event().is_set():
self.controller.clear_cancel_event()
if self.controller.is_cancel_pending():
return
cur_size = get_mod_dir_size(mod_path)
if cur_size == size:
@@ -306,11 +324,11 @@ class ConnectionManager:
update_signatures(self.missing_mods, prefs.paths.version)
# TODO: just push steam path directly
rebuild_symlinks(prefs.paths.config)
self._connect_steam()
self._connect_steam(menu_only)
@call_on_thread(waiting_for_mods, show_cancel=True)
def update_and_connect(self, raise_window: bool) -> None:
def update_and_connect(self, menu_only: bool = False) -> None:
if len(self.missing_mods) > 0:
self._update_mods(raise_window)
self._update_mods(menu_only)
else:
self._connect_steam()
self._connect_steam(menu_only)
+11 -5
View File
@@ -8,7 +8,7 @@ from dzgui.util.clip import copy_clipboard
from dzgui.util.open_links import open_workshop_page
from dzgui.views.dialogs.note import NoteDialog
from dzgui.views.trees.tree_mods import ModTreeView
from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView
from dzgui.views.trees.tree_log import LogTreeView
from dzgui.views.trees.tree_servers import ServerTreeView
from dzgui.views.trees.tree_server_mods import ServerModTreeView
@@ -27,7 +27,13 @@ if TYPE_CHECKING:
class ContextMenuManager:
def __init__(
self,
treeview: LogTreeView | ModTreeView | ServerTreeView | ServerModTreeView,
treeview: (
LogTreeView
| ModTreeView
| ServerTreeView
| ServerModTreeView
| OfflineModTreeView
),
controller: "Controller",
) -> None:
self.controller = controller
@@ -72,10 +78,10 @@ class ContextMenuManager:
case ContextMenu.SHOW_MODS:
self.controller.get_modlist(record)
if isinstance(self.treeview, ModTreeView):
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action:
case ContextMenu.DELETE_MOD:
self.controller.delete_mods()
case ContextMenu.UNSUB_MOD:
self.controller.unsub_mods(self.treeview)
case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page()
+5 -4
View File
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
class FilterManager:
def __init__(self) -> None:
def __init__(self, show_empty: bool = False, show_full: bool = False) -> None:
self.map_store = ModelFactory().make_map_store()
@@ -17,10 +17,10 @@ class FilterManager:
self.default_filters = {
strings.filter_1pp: True,
strings.filter_day: True,
strings.filter_empty: False,
strings.filter_empty: show_empty,
strings.filter_3pp: True,
strings.filter_night: True,
strings.filter_full: False,
strings.filter_full: show_full,
strings.filter_lowpop: True,
strings.filter_nonascii: False,
strings.filter_duplicate: False,
@@ -89,8 +89,9 @@ class FilterManager:
continue
self.append_map([m])
# TODO: currently unused
def get_unique_maps(self) -> list[str]:
return [row[0] for row in self.map_store if row != "All maps"]
return [row[0] for row in self.map_store if row[0] != "All maps"]
def get_all_filters(self) -> tuple:
map_name = self.get_active_map_name()
+47 -32
View File
@@ -1,20 +1,26 @@
import logging
import shutil
import time
from pathlib import Path
from typing import Any, TYPE_CHECKING
from typing import TYPE_CHECKING
from dzgui.api.steam import unsubscribe
from dzgui.api.mods import (
get_delimited_mods,
get_local_mod_path,
find_stale_mods,
_hash,
remove_stale_signatures,
)
from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
APPID_DAYZ_EXP,
RATE_LIMIT_THRESHOLD,
)
from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
from dzgui.strings import dialogs
from dzgui.util.format import format_mods
from dzgui.util.strings import server_timeout
from dzgui.views.dialogs.generic import ExceptionDialog
@@ -39,21 +45,20 @@ class ModManager:
this manager is instantiated each time the Mods page is opened
"""
def __init__(self, controller: "Controller") -> None:
def __init__(self, tree: Gtk.TreeView, controller: "Controller") -> None:
self.treeview = tree
self.controller = controller
self.emitter = controller.get_emitter()
self.prefs = controller.get_prefs()
self.path = controller.query_config(Preferences.DEFAULT)
self.treeview = self.controller.get_modtreeview()
self.store: FastInsertListStore
self.store: FastInsertListStore | None = None
self.thread_man = ThreadingManager(controller)
self._get_mods()
# TODO: dialog strings
@call_on_thread("getting mods")
def _get_mods(self) -> None:
@call_on_thread(dialogs.fetching_mods)
def load_mods(self) -> None:
mods = get_delimited_mods(self.path)
if len(mods) < 1:
msg = self.format_mod_statusbar()
@@ -61,18 +66,23 @@ class ModManager:
self.thread_man.set_cleanup_func(func)
return
func = StoredFunc(self._on_mods_loaded, mods)
self.thread_man.set_cleanup_func(func)
def _on_mods_loaded(self, mods: list[list[Any]]) -> None:
self.store = ModelFactory().make_mod_store()
self.store.extend(mods)
func = StoredFunc(self._on_mods_loaded)
self.thread_man.set_cleanup_func(func)
def set_store(self, store: "FastInsertListStore") -> None:
self.store = store
def _on_mods_loaded(self) -> None:
self.treeview.set_model(self.store)
if self.store is None:
return
msg = self.format_mod_statusbar()
total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods)
def delete_mods(self) -> None:
def unsub_mods(self) -> None:
sel = self.treeview.get_selection()
model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection
@@ -83,7 +93,7 @@ class ModManager:
continue
mod, _iter = res
mods.append((mod, _iter))
self.delete_mods_on_system(mods)
self.unsub_all_mods(mods)
def get_mod_from_tree_path(
self, tree_path: Gtk.TreePath
@@ -95,26 +105,29 @@ class ModManager:
mod = model.get_value(tree_iter, 2)
return mod, tree_iter
# TODO: strings
@call_on_thread("deleting mods")
def delete_mods_on_system(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
@call_on_thread(dialogs.deleting_mods)
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
for mod, _iter in mods:
self.delete_single_mod(mod)
self.unsub_atomic_mod(mod)
iters = [_iter for mod, _iter in mods]
func = StoredFunc(self._on_mods_deleted, iters)
func = StoredFunc(self._on_mods_unsubbed, iters)
self.thread_man.set_cleanup_func(func)
def delete_single_mod(self, mod: str) -> None:
def unsub_atomic_mod(self, mod: str) -> None:
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
unsubscribe(key, int(mod))
steam_path = Path(self.path)
mods_path = get_local_mod_path(steam_path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
md5 = _hash(mod)
symlink = app_path / md5
symlink.unlink()
shutil.rmtree(mods_path / mod)
try:
md5 = _hash(mod)
symlink = app_path / md5
symlink.unlink()
except Exception as e:
logger.warning(e)
# NOTE: second pass to unlink DAYZ_EXP mods
# TODO: test this with working APPID_DAYZ_EXP installation
try:
@@ -123,8 +136,11 @@ class ModManager:
symlink.unlink()
except PeFile.AppNotInstalledError:
pass
time.sleep(RATE_LIMIT_THRESHOLD)
def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None:
def _on_mods_unsubbed(self, iters: list[Gtk.TreeIter]) -> None:
if self.store is None:
return
for _iter in iters:
self.store.remove(_iter)
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
@@ -181,8 +197,7 @@ class ModManager:
model[path][4] = True
self.emitter.emit("mods_highlighted")
# TODO: dialog strings
@call_on_thread("working")
@call_on_thread(dialogs.scanning_mods)
def highlight_stale(self) -> None:
stale = find_stale_mods(self.prefs.paths.config)
func = StoredFunc(self._on_stale_mods_found, stale)
+4 -1
View File
@@ -20,7 +20,10 @@ logger = logging.getLogger(APP_NAME)
class NoteManager:
def __init__(self, controller: "Controller", path: "Path") -> None:
self.notes_path = path
self.cache: dict[str, str] = read_json(self.notes_path)
try:
self.cache: dict[str, str] = read_json(self.notes_path)
except Exception:
self.cache = {}
def add_note(self, server: str, note: str) -> None:
self.cache[server] = note
+132
View File
@@ -0,0 +1,132 @@
import logging
import time
from pathlib import Path
from typing import Callable, TYPE_CHECKING, Union
import dzgui.api.pefile as PeFile
from dzgui.api.mods import is_mission, get_custom_mods
from dzgui.api.steam import launch_offline
from dzgui.const.constants import APP_NAME, APPID_DAYZ_EXP
from dzgui.const.enum import Preferences
from dzgui.init.proc import is_dayz_running
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import ModelFactory
from dzgui.strings import dialogs
from dzgui.util.symlink import create_custom_symlinks, symlink_mission
from dzgui.views.dialogs.filepicker import FolderPicker
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.model.model_factory import FastInsertListStore
logger = logging.getLogger(APP_NAME)
class OfflineManager:
def __init__(
self,
controller: "Controller",
) -> None:
super().__init__()
self.controller = controller
self.emitter = controller.get_emitter()
self.thread_man = ThreadingManager(controller)
self.appid: int
self.local_mods: list[str] | None
self.custom_mods: list[str] | None
# TODO: more robust test
def is_custom_folder_valid(self, folder: str) -> bool:
return Path(folder).exists()
# TODO: set properties as members of offline manager on demand and check them here
# rather than passing strings again
def is_mission_valid(self, folder: str) -> bool:
return is_mission(Path(folder))
def get_mission(self) -> None:
folder = self.open_folderpicker(dialogs.mission_dialog)
if folder is None:
return
is_valid = is_mission(folder)
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
def find_custom_mods(self, callback: Callable) -> None:
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
window = self.controller.get_window()
window.set_sensitive(False)
if folder is None:
window.set_sensitive(True)
return
# NOTE: starts spinner in calling UI
callback()
self.parse_custom_mods(folder)
@call_on_thread(dialogs.parsing_mods, show_dialog=False)
def parse_custom_mods(self, folder: str) -> None:
mods = get_custom_mods(Path(folder))
store = ModelFactory().make_mod_store()
store.extend(mods)
func = StoredFunc(self.post_mod_loading, store, folder)
self.thread_man.set_cleanup_func(func)
def post_mod_loading(self, store: "FastInsertListStore", folder: str) -> None:
window = self.controller.get_window()
window.set_sensitive(True)
self.emitter.emit("custom_mods_loaded", store, folder)
@call_on_thread(dialogs.waiting_for_launch)
def launch(
self,
appid: int,
mission: str,
local_mods: list[str],
custom_folder: str,
custom_mods: list[str],
) -> None:
# NOTE: local_mods and custom_mods are lists of symlinks
client = self.controller.query_config(Preferences.CLIENT)
name = self.controller.query_config(Preferences.NAME)
steam_path = self.controller.query_config(Preferences.DEFAULT)
combined_mods: list[str] = []
if len(local_mods) > 0:
combined_mods.extend(local_mods)
if len(custom_mods) > 0:
new_symlinks = create_custom_symlinks(
Path(steam_path), Path(custom_folder), custom_mods
)
combined_mods.extend(new_symlinks)
relative_link = ""
if len(mission) > 0:
relative_link = symlink_mission(Path(steam_path), mission)
launch_offline(client, appid, name, combined_mods, relative_link)
while True:
if is_dayz_running():
break
time.sleep(1)
def open_folderpicker(self, title: str) -> Union["Path", None]:
picker = FolderPicker(self.controller.get_window(), title)
folder = picker.pick_folder()
picker.destroy()
return folder
def has_dayz_exp(self) -> bool:
try:
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam_path = Path(default_steam_path)
dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
if dayz_exp is None:
return False
return True
except Exception as e:
logger.warning(e)
return False
+3 -3
View File
@@ -47,8 +47,8 @@ class StoredFunc:
self.func = func
self.bindings = sig.bind(*args, **kwargs)
def call(self) -> None:
self.func(*self.bindings.args, *self.bindings.kwargs)
def call(self) -> Any:
return self.func(*self.bindings.args, *self.bindings.kwargs)
class ThreadingManager:
@@ -113,8 +113,8 @@ class ThreadingManager:
func = self.get_cleanup_func()
if func is not None:
func.call()
self.set_cleanup_func(None)
func.call()
if not self.destroy_first:
self.destroy_dialog()
+67
View File
@@ -0,0 +1,67 @@
import logging
import requests
import shutil
import subprocess
import tarfile
from typing import TYPE_CHECKING
from dzgui.const.constants import (
APP_NAME,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
)
from dzgui.strings import dialogs
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
logger = logging.getLogger(APP_NAME)
class UpdateManager:
def __init__(self, controller: "Controller") -> None:
self.thread_man = ThreadingManager(controller)
self.controller = controller
@call_on_thread(dialogs.fetching_update)
def update_version(self, exe_path: str, url: str) -> None:
try:
res = requests.get(url)
if res.status_code == 200:
with open(TMP_TARBALL, "wb") as file:
file.write(res.content)
with tarfile.open(TMP_TARBALL) as tar:
tar.extractall(TMP_PATH)
shutil.copy(TMP_EXE, exe_path)
proc = subprocess.run([exe_path, "self", "restore"])
if proc.returncode == 0:
func = StoredFunc(self._on_update_success)
self.thread_man.set_cleanup_func(func, destroy_first=True)
else:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
except Exception as e:
func = StoredFunc(self._on_update_failure, str(e))
self.thread_man.set_cleanup_func(func, destroy_first=True)
logger.warning(e)
def _on_update_success(self) -> None:
msg = dialogs.update_success
dialog = QuitDialog(self.controller, msg)
dialog.run()
def _on_update_failure(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg)
dialog.run()
+3
View File
@@ -165,3 +165,6 @@ class ModelFactory:
def make_server_store(self) -> FastInsertListStore:
return self.new_model_from_class(ServerCols)
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
# return [[el for el in row] for row in model]
+3 -2
View File
@@ -85,8 +85,9 @@ class ProxyModelManager:
if found is False:
self.control_model.append(history)
if len(self.control_model) == 11:
del self.control_model[0]
# if len(self.control_model) == 11:
# del self.control_model[0]
self.filter(FilterMode.INITIAL, skip_cache=True)
+40 -23
View File
@@ -14,6 +14,7 @@ from dzgui.const.constants import (
)
from dzgui.const.enum import FilterMode, Preferences, ServerTab
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import dialogs
from dzgui.util.strings import api_warn_msg, dialog
from dzgui.views.dialogs.generic import ExceptionDialog
@@ -23,7 +24,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
if TYPE_CHECKING:
from dzgui.api.servers import A2SInfo
from dzgui.api.servers import A2SInfo, Record
from dzgui.controllers.mc import Controller
from dzgui.model.proxy_model import ProxyModelManager
from dzgui.views.trees.tree_servers import ServerTreeView
@@ -73,6 +74,7 @@ class ServerModelManager:
self._dump_history()
case ServerTab.LAN:
# NOTE: LAN tab is only loaded on demand
self.emitter.emit("lan_page_initialized")
pass
case _:
pass
@@ -123,31 +125,30 @@ class ServerModelManager:
parsed = Servers.parse_json(servers)
self._push_data(parsed)
@call_on_thread(dialog.scanning)
@call_on_thread(dialog.scanning, show_cancel=True)
def dump_lan(self, port: int, early_abort: bool) -> None:
self.first_iteration = True
servers = []
ports = range(1, 256)
failure_func = StoredFunc(self._cleanup_on_failure)
failure_func = StoredFunc(self._cleanup_on_lan_failure)
event = threading.Event()
with ThreadPoolExecutor() as executor:
futures = [executor.submit(Servers.test_ip, i, port, event) for i in ports]
for future in as_completed(futures):
try:
if self.controller.get_exit_event().is_set():
if self.controller.is_cancel_pending():
event.set()
return
res = future.result(timeout=LAN_TIMEOUT)
if res is None:
continue
servers.append(res)
if early_abort is True:
# NOTE: on first non-empty hit, flag pending threads to close
event.set()
servers.append(res)
self.thread_man.set_cleanup_func(
StoredFunc(self._cleanup_on_success)
)
return
servers.append(res)
break
except Exception as e:
logger.critical(e)
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
@@ -221,15 +222,12 @@ class ServerModelManager:
) = None,
) -> None:
proxy_man = self._get_proxy_man()
config_man = self.controller.get_config_man()
if rows is not None:
records = rows
else:
control_model = proxy_man.get_control()
records = control_model
config_man.update_history_file(records)
config_man.update_history_file(records)
self._sort_unique_maps(records)
proxy = proxy_man.get_proxy_model()
self.tv.set_model(proxy)
@@ -239,19 +237,26 @@ class ServerModelManager:
filter_man = self.tv.get_filter_man()
maps = self._get_new_maps()
filter_man.set_unique_maps(maps)
store = filter_man.get_map_store()
self.first_iteration = False
self.emitter.emit("servers_loaded_init")
self.emitter.emit("load_maps", store)
# self.emitter.emit("servers_loaded_init")
def add_to_history(self, record: dict[str, Any]) -> None:
# TODO: dataclass for record rows; check for other dict annotations
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
row, record = data
proxy_man = self._get_proxy_man()
rows = Servers.parse_json([record])
rows = Servers.parse_json([row])
try:
proxy_man.append_row_to_history(rows[0])
self.update_history()
except Exception:
self.update_history(rows)
return
self.update_history()
fqip = Servers.record_to_fqip(record)
config_man = self.controller.get_config_man()
config_man.update_history_file(fqip)
def remove_from_history(self, record: Servers.Record) -> None:
proxy_man = self._get_proxy_man()
@@ -318,6 +323,7 @@ class ServerModelManager:
self.thread_man.set_cleanup_func(func)
return
config_man.add_saved_server(fqip)
# FIXME: this is valid if saved servers tab is already open,
# but not if app was just booted; causes single row to appear prematurely
if proxy_man.has_control_model() is False:
@@ -368,33 +374,44 @@ class ServerModelManager:
proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy)
self.emitter.emit("servers_loaded", self.enum)
if self.controller.get_active_treeview().get_enum() == ServerTab.SAVED:
self.emitter.emit("servers_loaded", self.enum)
filter_man = self.tv.get_filter_man()
# NOTE: maps are set outside of thread because it triggers map changed signals
maps = self._get_new_maps()
filter_man.set_unique_maps(maps)
store = filter_man.get_map_store()
self.emitter.emit("load_maps", store)
self.first_iteration = False
self.emitter.emit("servers_loaded_init")
self.emitter.emit("saved_servers_changed")
def _update_maps(self) -> None:
filter_man = self.tv.get_filter_man()
filter_man.set_unique_maps(self._get_new_maps())
self.emitter.emit("servers_loaded_init")
store = filter_man.get_map_store()
self.emitter.emit("load_maps", store)
self.first_iteration = False
def _cleanup_on_success(self) -> None:
proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy)
# TODO: servers_loaded vs servers_reloaded
self.emitter.emit("servers_loaded", self.enum)
if self.first_iteration:
self._update_maps()
def _cleanup_on_lan_failure(self, show_dialog: bool = True) -> None:
if self.preserve_on_fail is False:
self.tv.set_model(None)
filter_man = self.tv.get_filter_man()
filter_man.set_unique_maps([])
if show_dialog:
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
dialog.run()
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
# TODO: disable map, keyword, and filter widgets if model is None
# signal driven (servers_empty, servers_failed_to_load)
+15
View File
@@ -4,3 +4,18 @@ waiting_for_mods = "Waiting for Steam to update mods"
fetching_update = "Fetching update"
failed_to_update = "Failed to update DZGUI executable"
update_success = "Updated successfully. Please exit and relaunch."
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
fetching_mods = "Fetching mod metadata"
deleting_mods = "Unsubscribing mods"
scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods"
checking_api = "Checking API key"
running = "Running"
failed = "FAILED"
ok = "OK"
mission_dialog = "Set a mission folder location"
custom_mod_dialog = "Set location to a custom mods folder"
+1
View File
@@ -3,3 +3,4 @@ api_validation_error = (
)
api_popover = "API key validation failed."
dayz_not_found = "Could not find a valid DayZ installation."
+1
View File
@@ -0,0 +1 @@
selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range."
+22
View File
@@ -0,0 +1,22 @@
heading = "Play offline"
mission_button = "Select mission folder"
mission_frame = "Mission"
version = "DayZ Version"
custom_frame = "Custom mods"
custom_button = "Set custom mod folder"
local_frame = "Installed mods"
no_local_mods = "No local mods found"
no_mods = "No valid mods found"
no_mission = "Not a valid mission"
folder_changed = "Folder missing or changed on disk"
custom_eventbox = "Set the root folder. Mods must be in atomic folders and have a meta.cpp file at a minimum."
mission_eventbox = "Select a single mission folder containing an init.c file."
folder_prefix = "<b>Current folder: </b>"
unset_button = "Unset folder"
+6
View File
@@ -0,0 +1,6 @@
symlinks = "Rebuilding symlinks"
signatures = "Updating mod signatures"
geo = "Checking geolocation records"
coords = "Checking local coordinates"
updates = "Checking for updates"
dayz = "Looking for DayZ"
+4
View File
@@ -1,5 +1,9 @@
update_mods = "Update mods and connect"
connect = "Connect"
connect_last = "Load to menu screen"
connect_last_tooltip = (
"Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
)
back = "Back"
cancel = "Cancel"
warnings = "Warnings"
+1 -1
View File
@@ -67,4 +67,4 @@ label_client = "Steam client"
### Completion page
heading_completion = "Setup complete"
blurb_completion = "Configuration completed successfully. Please exit and restart DZGUI to apply changes."
blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI."
+6 -7
View File
@@ -2,20 +2,19 @@ import json
from pathlib import Path
from typing import Any
def read_json(path: Path) -> Any:
try:
with open(path, "r") as infile:
try:
data = json.load(infile)
return data
except json.decoder.JSONDecodeError as e:
raise e
except OSError as e:
data = json.load(infile)
return data
except Exception as e:
raise e
def write_json(data: dict, path: Path) -> None:
def write_json(data: dict[str, Any], path: Path) -> None:
try:
path.parent.mkdir(parents=True, exist_ok=True)
j = json.dumps(data, indent=2)
path.write_text(j)
except Exception as e:
+15
View File
@@ -0,0 +1,15 @@
from datetime import datetime
from pathlib import Path
def is_writeable(path: "Path") -> bool:
now = int(datetime.now().timestamp())
suffix = ".dzgtmp"
file = str(now) + suffix
filepath = Path(str(path) + "_" + file)
try:
filepath.touch()
filepath.unlink()
return True
except OSError:
return False
+6 -2
View File
@@ -1,6 +1,7 @@
import re
from dzgui.util.localize import number
from dzgui.strings.generic import selectable_tree
from dzgui.util.strings import no_mods, no_servers, workshop
import gi
@@ -46,8 +47,7 @@ def format_mods(size: int, mods: int) -> str:
return no_mods
l_size = number(size)
plural = pluralize("mods", mods)
# TODO: strings
suffix = "Ctrl-click to select multiple; Shift-click to select a range."
suffix = selectable_tree
return f"Found {mods:n} {plural} taking up {l_size} MiB. {suffix}"
@@ -83,3 +83,7 @@ def format_player_count(model: Gtk.TreeModel | None, control: list) -> str:
def embolden(text: str) -> str:
return f"<b>{text}</b>"
def format_exception(e: Exception) -> str:
return f"{type(e).__name__}: {e}"
+14 -8
View File
@@ -52,7 +52,7 @@ show_mods = "Show server-side mods"
show_details = "Show server details"
refresh_players = "Refresh player count"
open_workshop = "Open in Steam Workshop"
delete_mod = "Delete mod"
unsub_mod = "Unsubscribe mod"
copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard"
@@ -64,7 +64,7 @@ connect = "Connect"
# Columns
server_mod_cols = ["Mod", "ID", "Installed"]
mod_cols = ["Mod", "Symlink", "Dir", "Size (MiB)", "Color"]
mod_cols = ["Mod", "Symlink", "ID", "Size (MiB)", "Color"]
log_cols = ["Timestamp", "Flag", "Traceback", "Message"]
browser_cols = [
"Name",
@@ -101,7 +101,7 @@ build_corrupted = (
"Steam settings or DayZ installation may be corrupted. Try restarting Steam."
)
api_warn_msg = """No servers returned. Please wait and try again.
If this issue persists, your API key may be defunct or your network is blocking requests.
This usually indicates a transitory network timeout. If this issue persists, your API key or network may be malfunctioning.
"""
server_timeout = "Timed out when querying server, check IP or try again later."
server_error = (
@@ -181,7 +181,7 @@ filter_lowpop = "Low pop"
filter_nonascii = "Non-ASCII"
filter_duplicate = "Duplicate"
filter_official = "Official"
filter_unofficial = "Unoffic."
filter_unofficial = "Unofficial"
filter_modded = "Modded"
# maps
@@ -215,8 +215,8 @@ class ModPanelStrings:
unhighlight_stale_tooltip: str
highlight_stale: str
highlight_stale_tooltip: str
delete_selected: str
delete_selected_tooltip: str
unsub_selected: str
unsub_selected_tooltip: str
unselect_all: str
unselect_all_tooltip: str
select_all: str
@@ -273,6 +273,7 @@ class Crumbs:
thanks: str
developers: str
default: str
offline: str
@dataclass(slots=True, frozen=True)
@@ -336,8 +337,8 @@ mod_panel = ModPanelStrings(
"Shows locally-installed mods which are not used by any server "
"in your Saved Servers"
),
delete_selected="Delete selected",
delete_selected_tooltip="Deletes selected mods from the system",
unsub_selected="Unsubscribe selected",
unsub_selected_tooltip="Unsubscribes from selected mods",
unselect_all="Unselect all",
unselect_all_tooltip="Bulk unselects all mods",
select_all="Select all",
@@ -360,12 +361,15 @@ thanks = Thanks(
"bongjutsu",
"Deku",
"dj3hac",
"finntux",
"GaryBlackbourne",
"jiriks74",
"Johnofwrong",
"MatheusLasserr",
"nolan-perez",
"OnniSaarni",
"scandalouss",
"SnackSBR",
"StevelDusa",
"Thoughtduck216",
],
@@ -460,6 +464,7 @@ crumbs = Crumbs(
thanks="Help > Special thanks",
developers="Options > Developers",
default="Servers > ",
offline="Mods > Play offline",
)
no_mods = "No local mods found."
@@ -609,6 +614,7 @@ atomic_buttons = AtomicButton(
keys_tooltip="Toggles the keybindings dialog",
)
gtk_theme_missing = "No GTK theme provided"
steam_icon_missing = "Steam icon not found in IconTheme"
missing_changelog = "Error: failed to read changelog"
+54 -11
View File
@@ -2,7 +2,7 @@ import logging
from pathlib import Path
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME, DAYZ_COMMUNITY_ROOT
from dzgui.const.enum import Preferences
from dzgui.config.query import lookup
@@ -11,14 +11,23 @@ import dzgui.api.pefile as PeFile
logger = logging.getLogger(APP_NAME)
def expunge_link(file: Path) -> None:
if not file.is_symlink():
return
# NOTE: unlink stale symlinks
if file.exists() is False:
file.unlink()
if str(file.stem)[:2] == "@C":
file.unlink()
def rebuild_symlinks(config: Path) -> None:
# TODO: pass direct path as argument
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
# NOTE: unlink stale symlinks
for file in dayz_path.iterdir():
if file.is_symlink() and file.exists() is False:
file.unlink()
expunge_link(file)
workshop = get_local_mod_path(steam_path)
# NOTE: create symlinks for missing mods
for mod_id in get_local_mod_ids(steam_path):
@@ -30,6 +39,37 @@ def rebuild_symlinks(config: Path) -> None:
clone_symlinks(steam_path)
def create_custom_symlinks(
steam_path: Path, custom_dir: Path, uids: list[str]
) -> list[str]:
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
links = []
for uid in uids:
md5sum = _hash(uid, use_custom=True)
source = dayz_path.joinpath(md5sum)
target = custom_dir.joinpath(uid)
links.append(md5sum)
if source.exists() is False:
source.symlink_to(target)
clone_symlinks(steam_path)
return links
def symlink_mission(steam_path: Path, target: str) -> str:
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
for file in dayz_path.iterdir():
expunge_link(file)
path = Path(target)
stem = path.name
parent = path.parent
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
try:
source.symlink_to(parent)
except Exception:
return ""
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
def clone_symlinks(steam_path: Path) -> None:
"""
Shares symlinks between builds. Used after any symlink operation
@@ -43,11 +83,14 @@ def clone_symlinks(steam_path: Path) -> None:
# TODO: test these two operations
# use less time intensive logic like the above
for file in exp_path.iterdir():
if file.is_symlink():
file.unlink()
try:
for file in exp_path.iterdir():
if file.is_symlink():
file.unlink()
for file in dayz_path.iterdir():
if file.is_symlink():
uid = file.name
Path(exp_path / uid).symlink_to(dayz_path / uid)
for file in dayz_path.iterdir():
if file.is_symlink():
uid = file.name
Path(exp_path / uid).symlink_to(dayz_path / uid)
except Exception as e:
logger.critical(e)
+15 -7
View File
@@ -23,6 +23,8 @@ from dzgui.views.pages.help import Help
from dzgui.views.pages.keys import Keybindings
from dzgui.views.pages.log import Log
from dzgui.views.pages.mods import Mods
from dzgui.views.pages.offline import OfflineLoader
from dzgui.views.pages.options import Options
from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook
@@ -68,7 +70,6 @@ class OuterWindow(Gtk.Window):
MainController.set_start_tab()
self.grid.hide_widgets_on_init()
# TODO: POC, trigger page change here
MainController.loaded = True
MainController.populate_model(MainController.get_active_treeview())
@@ -110,8 +111,11 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
self.connection = PreConnectionAssistant(MainController)
self.offline = OfflineLoader(MainController)
self.pages = {
self.help: NotebookPage.HELP,
self.offline: NotebookPage.OFFLINE,
self.clog: NotebookPage.CHANGELOG,
self.keys: NotebookPage.KEYS,
self.settings: NotebookPage.OPTIONS,
@@ -262,8 +266,8 @@ class Grid(Gtk.Grid):
self.emitter.connect("server_page_toggled", self.toggle_filter_panels)
# TODO: drop, use map/signal
def hide_widgets_on_init(self) -> None:
self.conpan.set_lan_visible(False)
self.right_panel.sel_panel.hide()
def toggle_filter_panels(self, emitter: "Emitter", state: bool) -> None:
@@ -323,8 +327,12 @@ class App(Gtk.Application):
MainController = Controller()
theme = Gtk.IconTheme.get_default()
icons = theme.list_icons(None)
if STEAM_ICON not in icons:
logger.warn(strings.steam_icon_missing)
warnings.warn(strings.steam_icon_missing, stacklevel=2)
try:
theme = Gtk.IconTheme.get_default()
icons = theme.list_icons(None)
if STEAM_ICON not in icons:
logger.warn(strings.steam_icon_missing)
warnings.warn(strings.steam_icon_missing, stacklevel=2)
except Exception as e:
logger.warning(e)
warnings.warn(strings.gtk_theme_missing, stacklevel=2)
+25
View File
@@ -0,0 +1,25 @@
from typing import Sequence
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class GenericBox(Gtk.Box):
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
super().__init__(orientation=orientation, spacing=spacing)
def extend(self, els: Sequence[Gtk.Widget]) -> None:
for el in els:
self.add(el)
class HBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
+57 -19
View File
@@ -1,4 +1,4 @@
from typing import Callable, Self, TYPE_CHECKING
from typing import Callable, Self, TYPE_CHECKING, Union
from dzgui.util.clip import copy_clipboard
from dzgui.util.format import pluralize
@@ -31,57 +31,86 @@ if TYPE_CHECKING:
class Icon(Gtk.Image):
def __init__(self, name: str, l_margin: int = 0) -> None:
def __init__(self, name: str, margin_start: int = 0, margin_end: int = 0) -> None:
super().__init__(
icon_name=name,
icon_size=Gtk.IconSize.BUTTON,
margin_start=l_margin,
margin_start=margin_start,
margin_end=margin_end,
ypad=2,
)
class LargeIcon(Gtk.Image):
def __init__(self, name: str, l_margin: int = 5) -> None:
def __init__(self, name: str, margin_start: int = 5, margin_end: int = 0) -> None:
super().__init__(
icon_name=name, icon_size=Gtk.IconSize.LARGE_TOOLBAR, margin_start=l_margin
icon_name=name,
icon_size=Gtk.IconSize.LARGE_TOOLBAR,
margin_start=margin_start,
margin_end=margin_end,
)
class IconButton(Gtk.Button):
def __init__(self, icon: str, margin: int = 0) -> None:
def __init__(
self,
icon: str,
position: Gtk.PositionType = Gtk.PositionType.RIGHT,
margin_start: int = 0,
margin_end: int = 0,
) -> None:
super().__init__()
self.icon = Icon(icon, l_margin=margin)
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
self.set_image(self.icon)
self.set_image_position(Gtk.PositionType.RIGHT)
self.set_image_position(position)
# self.set_image_position(Gtk.PositionType.RIGHT)
self.set_focus_on_click(False)
class IconTextButton(IconButton):
def __init__(self, icon: str, label: str) -> None:
super().__init__(icon, margin=5)
def __init__(
self, icon: str, label: str, position: Gtk.PositionType = Gtk.PositionType.RIGHT
) -> None:
match position:
case Gtk.PositionType.RIGHT:
super().__init__(icon, position, margin_start=5)
case Gtk.PositionType.LEFT:
super().__init__(icon, position, margin_end=5)
case _:
raise ValueError("Unsupported position type")
self.set_label(label)
class LargeIconTextButton(IconButton):
def __init__(self, icon: str, label: str) -> None:
super().__init__(icon)
def __init__(
self, icon: str, label: str, position: Gtk.PositionType = Gtk.PositionType.RIGHT
) -> None:
super().__init__(icon, position)
self.set_label(label)
self.set_image(LargeIcon(icon))
class ClipboardButton(IconTextButton):
def __init__(self, controller: "Controller", func: Callable) -> None:
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
super().__init__(CLIPBOARD, atomic_buttons.copy)
self.controller = controller
self.connect("clicked", self._on_button_clicked, func)
self.set_tooltip_text("Copy IP to clipboard")
self.set_tooltip_text("Copy to clipboard")
def _on_button_clicked(self, button: Self, func: Callable) -> None:
data = func()
copy_clipboard(data)
# TODO: determine when controller would be passed to this button or drop
class CopyIpButton(ClipboardButton):
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
super().__init__(controller, func)
self.set_tooltip_text("Copy IP to clipboard")
class WebButton(IconTextButton):
def __init__(self, label: str) -> None:
super().__init__(icon=WEB_BROWSER, label=label)
@@ -94,9 +123,11 @@ class RefreshButton(IconTextButton):
icon=REFRESH_ICON,
label=atomic_buttons.refresh,
)
self.controller = controller
emitter = self.controller.get_emitter()
self.loading = False
self.is_clicked = False
self.time = 30
@@ -110,17 +141,21 @@ class RefreshButton(IconTextButton):
def _on_refresh_clicked(self, button: Self) -> None:
"""Spawned in a thread"""
self.loading = True
self.is_clicked = True
self.controller.refresh_tree()
# TODO: get server tab enum
# if LAN tab, reload existing entries in place
def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None:
if self.is_clicked is False:
return
if self.loading:
self.set_sensitive(False)
self.loading = False
self.show_time(True)
GLib.timeout_add_seconds(1, self.decrement)
return
self.set_sensitive(False)
self.loading = True
self.is_clicked = False
self.show_time(True)
GLib.timeout_add_seconds(1, self.decrement)
def decrement(self) -> bool:
self.time -= 1
@@ -128,6 +163,7 @@ class RefreshButton(IconTextButton):
self.time = 30
self.show_time(False)
self.set_sensitive(True)
self.loading = False
return False
self.show_time(True)
return True
@@ -172,6 +208,8 @@ class SteamWorkshopButton(SteamTextButton):
def __init__(self) -> None:
super().__init__(label=buttons.workshop)
self.set_tooltip_text(buttons.workshop_tooltip)
self.set_margin_top(10)
self.set_margin_bottom(10)
class AddButton(IconTextButton):
+68
View File
@@ -0,0 +1,68 @@
from dzgui.strings import preconnect
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk # noqa E402
class ConnectBox(Gtk.Box):
def __init__(self) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
self.back = Gtk.Button(
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
)
# TODO: dynamically update this
self.ok = Gtk.Button(label="Launch", halign=Gtk.Align.END)
for button in self.back, self.ok:
self.add(button)
def get_back_button(self) -> Gtk.Button:
return self.back
def get_ok_button(self) -> Gtk.Button:
return self.ok
class PreconnectBox(Gtk.Box):
def __init__(self) -> None:
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.END,
vexpand=True,
spacing=5,
)
self.button_hbox = ConnectBox()
self.ok = self.button_hbox.get_ok_button()
self.ok.set_label(preconnect.update_mods)
self.connect_last = Gtk.Button(
label=preconnect.connect_last,
halign=Gtk.Align.END,
tooltip_text=preconnect.connect_last_tooltip,
)
self.button_hbox.add(self.connect_last)
self.raise_window = Gtk.CheckButton(
# TODO: strings
label="Foreground DZGUI while downloading",
halign=Gtk.Align.END,
hexpand=True,
valign=Gtk.Align.END,
visible=False,
has_tooltip=True,
sensitive=False,
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
active=True,
)
self.add(self.button_hbox)
self.add(self.raise_window)
def get_back_button(self) -> Gtk.Button:
return self.button_hbox.get_back_button()
def get_ok_button(self) -> Gtk.Button:
return self.ok
+47 -9
View File
@@ -1,12 +1,14 @@
from typing import TYPE_CHECKING
from typing import Self, TYPE_CHECKING
from dzgui.const.constants import UDP_PORT
from dzgui.const.constants import CARET_DOWN, CARET_UP, UDP_PORT
from dzgui.model.servers import ServerModelManager
from dzgui.strings import connect_panel
from dzgui.util.keys import is_ctrl_mask
from dzgui.views.components.buttons import (
AddButton,
ClipboardButton,
CopyIpButton,
Icon,
IconButton,
SteamConnectButton,
)
from dzgui.views.components.entry import IpEntry, PortEntry
@@ -18,12 +20,45 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller, Emitter
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
COLS = 1
ROWS = 1
class DockButton(IconButton):
def __init__(self, child: Gtk.Widget) -> None:
super().__init__(CARET_DOWN)
self.expanded = True
self.child = child
self.connect("clicked", self._on_click)
self.collapse_text = "Collapse the connection panel"
self.expand_text = "Expand the connection panel"
self.set_margin_top(10)
self.set_tooltip_text(self.collapse_text)
def collapse(self) -> None:
self.set_image(Icon(CARET_UP))
self.child.hide()
self.set_tooltip_text(self.expand_text)
def expand(self) -> None:
self.set_image(Icon(CARET_DOWN))
self.child.show()
self.set_tooltip_text(self.collapse_text)
def _on_click(self, button: Self) -> None:
if self.expanded:
self.collapse()
else:
self.expand()
self.expanded = not self.expanded
class LanPanel(Gtk.Frame):
def __init__(self, controller: "Controller") -> None:
super().__init__(margin_top=10, margin_bottom=5)
@@ -151,7 +186,7 @@ class FavPanel(Gtk.Frame):
self.fav_button = SteamConnectButton()
self.fav_button.connect("clicked", self._on_connect_clicked)
self.copy_button = ClipboardButton(self.controller, self.get_fav_ip)
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
if favorite is None:
self.toggle_buttons(False)
@@ -272,15 +307,18 @@ class ConnectPanel(Gtk.Box):
emitter = self.controller.get_emitter()
emitter.connect("lan_tab_toggled", self._on_lan_tab_toggled)
self.panel_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.lan = LanPanel(controller)
self.fav = FavPanel(controller)
self.add_panel = AddPanel(controller)
for el in self.lan, self.fav, self.add_panel:
self.add(el)
self.panel_box.add(el)
def set_lan_visible(self, state: bool) -> None:
self.lan.set_visible(state)
self.add(DockButton(self.panel_box))
self.add(self.panel_box)
self.lan.set_visible(False)
def _on_lan_tab_toggled(self, emitter: "Emitter", state: bool) -> None:
self.set_lan_visible(state)
self.lan.set_visible(state)
+11
View File
@@ -11,6 +11,7 @@ from dzgui.const.constants import (
)
from dzgui.const.enum import FilterMode
from dzgui.model.servers import ServerModelManager
from dzgui.util.format import format_exception
from dzgui.util.strings import all_maps
from dzgui.views.components.maps_combo import MapsCombo
from dzgui.views.components.labels import BoldLabel
@@ -68,6 +69,7 @@ class ButtonGrid(Gtk.Grid):
)
def reload_filters(self) -> None:
# TODO: may be superfluous
checkboxes = self.checks
self.block_toggles(True)
filters = self.controller.get_enabled_filters()
@@ -330,3 +332,12 @@ class FilterPanel(Gtk.Box):
self.block_map_change_propagation = False
return
self.emitter.emit("map_selection_changed", name)
def set_filters(self, filters: dict[str, bool]) -> None:
try:
for check in self.button_grid.checks:
label = check.get_label()
check.set_active(filters[label])
except Exception as e:
msg = format_exception(e)
logger.warning(msg)
+16 -8
View File
@@ -1,3 +1,4 @@
from typing import Self
from dzgui.util import css
import gi
@@ -6,16 +7,23 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
# TODO: subclass Gtk.Frame directly
class HeadingFrame(Gtk.Box):
def __init__(self, widget: Gtk.Widget, heading: str) -> None:
def __init__(self, heading: str = "") -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL)
label = Gtk.Label(label=heading)
label.set_halign(Gtk.Align.START)
css.add_class(label, "settings-subheading")
self.label = Gtk.Label(label=heading)
self.label.set_halign(Gtk.Align.START)
css.add_class(self.label, "settings-subheading")
frame = Gtk.Frame(hexpand=True)
frame.add(widget)
frame.set_label_widget(label)
self.frame = Gtk.Frame(hexpand=True)
self.frame.set_label_widget(self.label)
self.add(frame)
self.add(self.frame)
@classmethod
def new_with_widget_and_label(cls, widget: Gtk.Widget, label: str) -> Self:
n = cls()
n.frame.add(widget)
n.label.set_label(label)
return n
+13 -5
View File
@@ -11,7 +11,8 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller, Emitter
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
class EnumeratedModButton(Gtk.Button):
@@ -22,10 +23,12 @@ class EnumeratedModButton(Gtk.Button):
self.set_label(enum.dict["label"])
self.set_tooltip_text(enum.dict["tooltip"])
class ModPanelButton(EnumeratedModButton):
def __init__(self, enum: ModButton) -> None:
super().__init__(enum=enum)
self.enum = enum
if enum is not ModButton.HIGHLIGHT_STALE:
self.set_sensitive(False)
@@ -46,7 +49,7 @@ class ModSelectionPanel(Gtk.Box):
buttons = (
ModButton.SELECT_ALL,
ModButton.UNSELECT_ALL,
ModButton.DELETE_SELECTED,
ModButton.UNSUB_SELECTED,
)
for button in buttons:
b = EnumeratedModButton(button)
@@ -58,8 +61,13 @@ class ModSelectionPanel(Gtk.Box):
self.unhighlight_stale_button = ModPanelButton(ModButton.UNHIGHLIGHT_STALE)
self.select_stale_button = ModPanelButton(ModButton.SELECT_STALE)
for b in (self.highlight_stale_button, self.unhighlight_stale_button, self.select_stale_button):
for b in (
self.highlight_stale_button,
self.unhighlight_stale_button,
self.select_stale_button,
):
self.stale_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING)
b.connect("clicked", self._on_button_clicked)
for el in header, self.main_panel, self.stale_panel:
self.pack_start(el, NO_EXPAND, FILL, NO_PADDING)
@@ -93,8 +101,8 @@ class ModSelectionPanel(Gtk.Box):
self.controller.toggle_mod_selection(True)
case ModButton.UNSELECT_ALL:
self.controller.toggle_mod_selection(False)
case ModButton.DELETE_SELECTED:
self.controller.delete_mods()
case ModButton.UNSUB_SELECTED:
self.controller.unsub_mods()
case ModButton.HIGHLIGHT_STALE:
self.controller.highlight_stale()
case ModButton.UNHIGHLIGHT_STALE:
+21 -57
View File
@@ -1,10 +1,7 @@
import logging
import os
import requests
import shutil
import subprocess
import tarfile
from pathlib import Path
from typing import Literal, TYPE_CHECKING
from dzgui.const.constants import (
@@ -13,20 +10,15 @@ from dzgui.const.constants import (
NO_FILL,
FILL,
NO_PADDING,
TMP_EXE,
TMP_PATH,
TMP_TARBALL,
)
from dzgui.const.update import ALLOW_UPDATES
from dzgui.const.enum import ServerTab
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import dialogs
from dzgui.managers.update import UpdateManager
from dzgui.util.clip import copy_clipboard
from dzgui.util.file import is_writeable
from dzgui.views.components.buttonbox import ButtonBox
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
import gi
@@ -45,13 +37,13 @@ class RightPanel(Gtk.Box):
def __init__(self, controller: "Controller"):
super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL)
self.thread_man = ThreadingManager(controller)
self.controller = controller
self.controller.register_widget("right_panel", self)
emitter = controller.get_emitter()
emitter.connect("servers_loaded", self._on_servers_loaded)
emitter.connect("server_page_changed", self._on_server_page_changed)
emitter.connect("lan_page_initialized", self._on_lan_page_init)
self.button_vbox = ButtonBox(controller)
self.filters_vbox = FilterPanel(controller)
@@ -95,54 +87,21 @@ class RightPanel(Gtk.Box):
valign=Gtk.Align.END,
spacing=10,
)
if ALLOW_UPDATES and update is not None:
self.update_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.update_button)
self.update_button.connect(
"clicked", self._on_update_button_clicked, update
)
exe_path = os.getenv("PYAPP")
if exe_path and update is not None:
if is_writeable(Path(exe_path)) is True:
self.update_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.update_button)
self.update_button.connect(
"clicked", self._on_update_button_clicked, exe_path, update
)
self.gutter_box.add(eb)
self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING)
def _on_update_success(self) -> None:
msg = dialogs.update_success
dialog = QuitDialog(self.controller, msg)
dialog.run()
def _on_update_failure(self, msg: str) -> None:
dialog = ExceptionDialog(self.controller, msg)
dialog.run()
@call_on_thread(dialogs.fetching_update)
def _on_update_button_clicked(self, button: Gtk.Button, url: str) -> None:
try:
res = requests.get(url)
if res.status_code == 200:
with open(TMP_TARBALL, "wb") as file:
file.write(res.content)
with tarfile.open(TMP_TARBALL) as tar:
tar.extractall(TMP_PATH)
exe_path = os.getenv("PYAPP")
if exe_path is None:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
return
shutil.move(TMP_EXE, exe_path)
proc = subprocess.run([exe_path, "self", "restore"])
if proc.returncode == 0:
func = StoredFunc(self._on_update_success)
self.thread_man.set_cleanup_func(func, destroy_first=True)
else:
msg = dialogs.failed_to_update
func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True)
except Exception as e:
func = StoredFunc(self._on_update_failure, e)
self.thread_man.set_cleanup_func(func, destroy_first=True)
logger.warning(e)
def _on_update_button_clicked(
self, button: Gtk.Button, exe_path: str, url: str
) -> None:
UpdateManager(self.controller).update_version(exe_path, url)
def _on_server_page_changed(
self, emitter: "Emitter", page: "ServerTreeView"
@@ -151,6 +110,11 @@ class RightPanel(Gtk.Box):
if page.loaded is True:
return
self.filters_vbox.set_sensitive(False)
# TODO: unless it is lan page
# self.refresh_button.set_sensitive(False)
def _on_lan_page_init(self, emitter: "Emitter") -> None:
self.filters_vbox.set_sensitive(False)
def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None:
# TODO: similar logic on notebook page change
+9
View File
@@ -0,0 +1,9 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class NoOverlayScrolledWindow(Gtk.ScrolledWindow):
def __init__(self) -> None:
super().__init__(overlay_scrolling=False)
+15 -24
View File
@@ -1,5 +1,4 @@
from typing import Self, Union, TYPE_CHECKING
from warnings import deprecated
from dzgui.const.enum import NotebookPage, ServerTab
from dzgui.util.strings import esc_to_return, question_to_return
@@ -8,7 +7,7 @@ from dzgui.views.components.buttons import LoggerAlertsButton
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject # noqa E402
from gi.repository import Gtk # noqa E402
if TYPE_CHECKING:
from dzgui.const.enum import ServerTab
@@ -26,6 +25,8 @@ class Statusbar(Gtk.Grid):
self.controller.register_widget("statusbar", self)
self.emitter = controller.get_emitter()
self.prior_enum: NotebookPage
self.prior_status: str
self.playercount = ""
self.statusbar = Gtk.Statusbar()
@@ -34,14 +35,6 @@ class Statusbar(Gtk.Grid):
self.attach(self.statusbar, 0, 0, 3, 1)
self.attach_next_to(self.spinner, self.statusbar, Gtk.PositionType.RIGHT, 3, 1)
# TODO: pack version event box in right panel into hbox with update button
# TODO: spawns a modal or just jumps right to install page
# from dzgui.views.components.buttons import IconTextButton
# b = IconTextButton("dialog-information-symbolic", label="Updates available")
# b.set_halign(Gtk.Align.END)
# b.set_hexpand(True)
# self.attach_next_to(b, self.spinner, Gtk.PositionType.RIGHT, 3, 1)
self.players = ""
controller.get_menu().connect("generic_treesel_changed", self._help_row_changed)
@@ -78,7 +71,9 @@ class Statusbar(Gtk.Grid):
NotebookPage.CHANGELOG,
NotebookPage.LOG,
NotebookPage.CONNECTION,
NotebookPage.OFFLINE,
):
self.prior_enum = enum
self.set_by_context(enum, esc_to_return)
return
@@ -88,16 +83,11 @@ class Statusbar(Gtk.Grid):
case NotebookPage.KEYS:
bar = question_to_return
case _:
return
bar = self.prior_status
self.prior_enum = enum
self.set_by_context(enum, bar)
# TODO: unused
def _on_notebook_page_returned(
self, statusbar: Self, prior_context: NotebookPage
) -> None:
self.pop(prior_context)
def _on_server_row_changed(self, statusbar: Self) -> None:
self.spinner.start()
@@ -111,9 +101,14 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage],
) -> None:
self.spinner.stop()
# TODO: CalcDist is being called when table is not loaded
if type(context) is NotebookPage:
self.prior_enum = context
self.prior_status = ""
# FIXME: CalcDist is being called when table is not loaded
if dist is None:
self.set_by_context(context, "")
return
else:
pretty = self.append_distance(dist)
self.set_by_context(context, pretty)
@@ -132,15 +127,11 @@ class Statusbar(Gtk.Grid):
cid = self.statusbar.get_context_id(str(context))
self.statusbar.pop(cid)
@deprecated("currently unused")
# def get_text(self) -> str:
# area = self.statusbar.get_message_area()
# label = area.get_children()[0]
# return str(label.get_text())
def set_by_context(
self, context: Union[NotebookPage, "ServerTab"], string: str
) -> None:
if context != NotebookPage.KEYS:
self.prior_status = string
meta = self.statusbar.get_context_id(str(context))
self.statusbar.push(meta, string)
self.set_cache(string)
+253
View File
@@ -0,0 +1,253 @@
import sys
from typing import Any, Literal, Self, TYPE_CHECKING
# import time
from enum import Enum
# TODO: import dialog titles
from dzgui.api.mods import remove_stale_signatures as remove_stale
from dzgui.config.ipdb import get_ipdb
from dzgui.const.constants import HEX_GREEN, HEX_RED
from dzgui.init.coords import get_local_coords
from dzgui.init.dayz import is_dayz_installed
from dzgui.init.update import check_updates
from dzgui.const.constants import EXPAND, FILL
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import preboot, dialogs
from dzgui.util.format import format_exception
from dzgui.util.strings import dialog_header
from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.components.buttons import ClipboardButton
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk # noqa
if TYPE_CHECKING:
from dzgui.config.xdg import Xdg
from dzgui.util.ip import Coords
class Success(Enum):
OK = 1
FAIL = 2
# TODO: strings for "Running", "Failed", etc.
# TODO: add margins to tree
class BootDialog(Gtk.Dialog):
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
super().__init__(
title=dialog_header,
parent=parent,
modal=True,
)
self.parent = parent
self.set_modal(True)
self.xdg = xdg
self.version = version
self.thread_man = ThreadingManager(None)
self.set_size_request(700, 500)
self.store = Gtk.ListStore(str, str, bool, int)
self.view = Gtk.TreeView(enable_search=False, headers_visible=False)
self.view.set_model(self.store)
# TODO: capture sigint in early dialogs
self.connect("key-press-event", self._on_keypress)
for i, column_title in enumerate(["Task", "State"]):
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
column.set_resizable(False)
column.set_sort_column_id(i)
column.set_cell_data_func(renderer, self._format_color, func_data=None)
self.view.append_column(column)
if i == 0:
column.set_fixed_width(300)
self.spinner_renderer = Gtk.CellRendererSpinner()
self.spinner_renderer.set_property("size", Gtk.IconSize.LARGE_TOOLBAR)
col_bool = Gtk.TreeViewColumn("Spinner", self.spinner_renderer, active=3)
col_bool.set_alignment(0.0)
col_bool.set_cell_data_func(
self.spinner_renderer, self._set_spinner_vis, func_data=None
)
self.view.append_column(col_bool)
col_int = Gtk.TreeViewColumn("Int", Gtk.CellRendererText(), text=3)
col_int.set_visible(False)
self.view.append_column(col_int)
self.view.get_selection().set_mode(Gtk.SelectionMode.NONE)
self.connect("delete-event", self._on_delete)
self.scrollable_tree = Gtk.ScrolledWindow(overlay_scrolling=False)
self.scrollable_tree.add(self.view)
self.scrollable_tree.set_size_request(700, 400)
self.error_box = Gtk.Box(
halign=Gtk.Align.CENTER,
orientation=Gtk.Orientation.VERTICAL,
spacing=20,
margin_top=30,
margin_bottom=30,
)
self.error_label = Gtk.Label()
# TODO: abstract class
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
self.button_hbox = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER, spacing=10
)
self.exit_button = Gtk.Button(label="Exit", halign=Gtk.Align.CENTER)
self.exit_button.connect("clicked", lambda _: sys.exit(1))
self.button_hbox.add(self.copy_button)
self.button_hbox.add(self.exit_button)
self.error_box.add(self.error_label)
self.error_box.add(self.button_hbox)
self.content = self.get_content_area()
self.content.pack_start(self.scrollable_tree, EXPAND, FILL, 0)
self.content.pack_start(self.error_box, EXPAND, FILL, 0)
self.show_all()
self.error_box.hide()
steps = [
(StoredFunc(is_dayz_installed, self.xdg.config), preboot.dayz, False),
(StoredFunc(rebuild_symlinks, self.xdg.config), preboot.symlinks, False),
(
StoredFunc(remove_stale, self.xdg.config, self.xdg.version),
preboot.signatures,
False,
),
(StoredFunc(get_ipdb, self.xdg.ips), preboot.geo, False),
# TODO: handle IP DB failure and use coords fallback
(StoredFunc(get_local_coords, self.xdg.ips), preboot.coords, True),
(StoredFunc(check_updates, self.version), preboot.updates, True),
]
self.results: list[Any] = []
self.failed = False
self.steps = iter(steps)
GLib.timeout_add(100, self.pulse_spinner)
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
self.exit_button.emit("clicked")
def pulse_spinner(self) -> Literal[True]:
for row in self.store:
if row[2]:
if row[3] == 150:
row[3] = 0
else:
row[3] += 1
self.spinner_renderer.set_property("pulse", row[3])
return True
def run(self) -> None:
self.iter_step()
def _on_delete(self, widget: Self, event: Gdk.Event) -> Literal[True]:
return True
def iter_step(self) -> None:
if self.failed:
self.error_box.show()
self.error_label.set_text(self.exception)
return
try:
step, label, store_output = next(self.steps)
self.update_task(label)
self.background(step, store_output)
except StopIteration:
self.parent.set_results(self.results)
self.destroy()
@call_on_thread("", show_dialog=False)
def background(self, func: StoredFunc, store_output: bool) -> None:
try:
if store_output:
res = func.call()
self.results.append(res)
else:
func.call()
callback = StoredFunc(self.update_status, Success.OK)
self.thread_man.set_cleanup_func(callback)
except Exception as e:
self.failed = True
self.exception = format_exception(e)
callback = StoredFunc(self.update_status, Success.FAIL)
self.thread_man.set_cleanup_func(callback)
def update_task(self, task: str) -> None:
self.store.append((task, dialogs.running, True, 0))
def update_status(self, state: Success) -> None:
d = {Success.OK: dialogs.ok, Success.FAIL: dialogs.failed}
msg = d[state]
self.store[len(self.store) - 1][1] = msg
self.iter_step()
def _set_spinner_vis(
self,
column: Gtk.TreeViewColumn,
cell: Gtk.CellRendererSpinner,
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
) -> None:
if model[it][1] == dialogs.running:
cell.set_property("visible", True)
else:
cell.set_property("visible", False)
def _format_color(
self,
column: Gtk.TreeViewColumn,
cell: Gtk.CellRendererText,
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
) -> None:
prop = "foreground"
state = model[it][1]
if column.get_sort_column_id() != 1:
return
if state == dialogs.ok:
cell.set_property(prop, HEX_GREEN)
elif state == dialogs.failed:
cell.set_property(prop, HEX_RED)
else:
cell.set_property(prop, None)
class BootWindow(Gtk.Window):
def __init__(self, xdg: "Xdg", version: str) -> None:
super().__init__()
self.results: list[Any] = []
dialog = BootDialog(self, xdg, version)
dialog.run()
dialog.connect("destroy", self._on_destroy)
Gtk.main()
def set_results(self, res: list[Any]) -> None:
self.results = res
def get_results(self) -> tuple["Coords", str]:
coords, version_url = self.results
return (coords, version_url)
def _on_destroy(self, dialog: BootDialog) -> None:
Gtk.main_quit()
+2
View File
@@ -53,3 +53,5 @@ class EarlyIgnoreDialog(EarlyAlertDialog):
# TODO: reverse order
self.add_button("Ignore", Gtk.ResponseType.CANCEL)
# TODO: if exit, sys.exit(1), else pass
+33
View File
@@ -33,3 +33,36 @@ class FilePicker(Gtk.FileChooserDialog):
self.destroy()
return Path(file)
return None
class FolderPicker(Gtk.FileChooserDialog):
def __init__(self, parent: Gtk.Window, title: str) -> None:
super().__init__(
title=title,
action=Gtk.FileChooserAction.SELECT_FOLDER,
parent=parent,
resizable=True,
)
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
self.add_buttons("_OK", Gtk.ResponseType.OK)
self.set_default_response(Gtk.ResponseType.OK)
def pick_folder(self) -> Path | None:
res = self.run()
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
self.destroy()
return None
if res == Gtk.ResponseType.OK:
folder = self.get_current_folder()
if folder is None:
try:
uri = self.get_uri()
if uri is None:
return None
return Path.from_uri(uri)
except Exception:
# FIXME: edge cases
return Path("/")
self.destroy()
return Path(folder)
return None
+7 -2
View File
@@ -31,8 +31,6 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
# TODO: currently unused, corresponds to linear index
# TODO: use of pagenum enum and page_type attribute is redundant
class PageNum(Enum):
INTRO = 1
HAS_CONFIG = 2
@@ -93,6 +91,9 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
self.connect("map", self._on_map)
def get_enum(self) -> PageNum:
return self.enum
def get_progress_bar(self) -> Progress:
return self.prog
@@ -474,6 +475,7 @@ class Assistant(Gtk.Assistant):
def _advance_page(self, index: int) -> int:
page = self.get_nth_page(index)
# TODO: use enums
match page:
case self.page1:
pass
@@ -534,6 +536,9 @@ class Assistant(Gtk.Assistant):
bar.set_text(f"{page_num}/{total}")
# NOTE: disable forward action
# TODO: use page enums
if page == self.page5:
return
if page != self.page1:
EMITTER.emit("step_pending")
+2
View File
@@ -44,6 +44,8 @@ class ContextMixin(TreeView):
self.context_menu.connect("key-press-event", self._on_key)
for row in group.value:
if row is None:
return False
item = self._process_dynamic_row(row)
self.context_menu.append(item)
+2 -2
View File
@@ -11,5 +11,5 @@ class ModsMixin:
self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn
) -> None:
path = self.get_focused_row_path() # type: ignore
self.controller.open_mod_page(path) # type: ignore
mod = self.get_value_at_index(2) # type: ignore
self.controller.open_workshop_page(mod) # type: ignore
+2 -1
View File
@@ -1,5 +1,6 @@
from typing import TYPE_CHECKING
from dzgui.const.constants import LOG_FILTERS
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.mixins.cursor_mixin import CursorMixin
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
from dzgui.views.trees.tree_log import LogTreeView
@@ -21,7 +22,7 @@ class Log(CursorMixin, HelpMenuMixin, Gtk.Box): # type: ignore
NOTE: LogTreeView is kept in a separate ScrolledWindow so that
checkboxes will be flush on bottom
"""
self.scrolled = Gtk.ScrolledWindow()
self.scrolled = NoOverlayScrolledWindow()
self.treeview = LogTreeView(controller)
self.scrolled.add(self.treeview)
+51 -5
View File
@@ -1,4 +1,11 @@
from pathlib import Path
from typing import Self, TYPE_CHECKING
from dzgui.api.steam import find_user_id
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.views.components.box import HBox
from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView
import gi
@@ -10,25 +17,64 @@ if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class Mods(Gtk.ScrolledWindow):
class Mods(Gtk.Box):
def __init__(self, controller: "Controller") -> None:
super().__init__()
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.controller = controller
self.emitter = controller.get_emitter()
self.box = NoOverlayScrolledWindow()
self.tree = ModTreeView(controller)
self.tree.set_vexpand(True)
self.box.add(self.tree)
self.add(self.tree)
self.statusbar_cache = ""
self.controller = controller
self.controller.register_widget("modtreeview", self.tree)
self.emitter = controller.get_emitter()
# TODO: move
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam_path = Path(default_steam_path)
uid = find_user_id(steam_path)
pretty_uid = "" if uid is None else uid
hbox = HBox(spacing=10)
workshop_button = SteamWorkshopButton()
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(pretty_uid)
)
self.offline_button = Gtk.Button(
label="Play offline",
halign=Gtk.Align.START,
margin_top=10,
margin_bottom=10,
)
self.offline_button.connect("clicked", self._on_offline_clicked)
hbox.add(workshop_button)
hbox.add(self.offline_button)
self.add(hbox)
self.add(self.box)
self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap)
def _on_offline_clicked(self, button: Gtk.Button) -> None:
self.statusbar_cache = self.controller.get_statusbar().get_cache()
self.controller.open_offline()
def _on_unmap(self, widget: Self) -> None:
self.emitter.emit("mod_page_toggled", False)
def _on_map(self, widget: Self) -> None:
self.emitter.emit("mod_page_toggled", True)
# TODO: delegation
# NOTE: handles going back from offline mods page
# more generic cache restoration method
if len(self.statusbar_cache) > 0:
self.controller.get_statusbar().set_by_context(
NotebookPage.MODS, self.statusbar_cache
)
def grab_content_area(self) -> None:
self.tree.grab_focus()
+513
View File
@@ -0,0 +1,513 @@
from __future__ import annotations
from enum import Enum
from typing import Self, TYPE_CHECKING, Union
from dzgui.util import css
from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
EDIT_DELETE,
ERROR,
FOLDER,
WARNING,
)
from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline
from dzgui.views.components.box import HBox, VBox
from dzgui.views.components.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import OfflineModTreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GObject # noqa
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
from dzgui.model.model_factory import FastInsertListStore
class FolderError(Enum):
NO_VALID_MODS = 1
NO_VALID_MISSION = 2
FOLDER_CHANGED = 3
class PageHeading(Gtk.Label):
def __init__(self, label: str) -> None:
super().__init__(label=label, halign=Gtk.Align.CENTER)
css.add_class(self, "page-heading")
class ErrorPopover(Gtk.Popover):
def __init__(self) -> None:
super().__init__(position=Gtk.PositionType.RIGHT)
self.hbox = HBox()
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
error_icon = Icon(ERROR, margin_start=10)
self.hbox.extend([error_icon, self.label])
self.add(self.hbox)
self.show_all()
self.popdown()
def set_label(self, error: FolderError, msg: str) -> None:
match error:
case FolderError.NO_VALID_MODS:
prefix = offline.no_mods
case FolderError.NO_VALID_MISSION:
prefix = offline.no_mission
case FolderError.FOLDER_CHANGED:
prefix = offline.folder_changed
self.label.set_label(prefix)
return
self.label.set_label(f"{prefix}: '{msg}'")
class FolderHBox(HBox):
def __init__(self, controller: "Controller", btn_label: str, eb_text: str) -> None:
super().__init__(spacing=10)
self.set_margin_start(5)
self.set_margin_end(10)
self.set_margin_bottom(5)
self.folder = ""
self.controller = controller
self.emitter = controller.get_emitter()
self.eb = InfoEventBox(eb_text, controller)
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
self.button.set_halign(Gtk.Align.START)
self.button.set_image_position(Gtk.PositionType.LEFT)
self.set_can_focus(True)
self.scrolled_label = Gtk.ScrolledWindow(
propagate_natural_width=True, halign=Gtk.Align.START
)
self.label = Gtk.Label()
self.scrolled_label.add(self.label)
self.unset_button = IconTextButton(EDIT_DELETE, offline.unset_button)
self.unset_button.connect("clicked", self._on_unset_clicked)
self.spinner = Gtk.Spinner()
self.extend(
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
)
self.pop = ErrorPopover()
self.pop.set_relative_to(self.button)
self.pop.connect("unmap", lambda _: self.grab_focus())
self.sidepop = ErrorPopover()
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
self.sidepop.set_relative_to(self.scrolled_label)
self.sidepop.connect("unmap", lambda _: self.grab_focus())
self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap)
def start_spinner(self) -> None:
self.spinner.show()
self.spinner.start()
def stop_spinner(self) -> None:
self.spinner.hide()
self.spinner.stop()
def _on_unmap(self, widget: Self) -> None:
self.unset_button.hide()
self.label.set_label("")
self.scrolled_label.hide()
def _on_map(self, widget: Self) -> None:
self.spinner.hide()
self.scrolled_label.show()
self.unset_button.hide()
def _on_unset_clicked(self, button: Gtk.Button) -> None:
self.folder = ""
self.label.set_label("")
self.unset_button.hide()
# FIXME: rename signal
self.emitter.emit("custom_mods_unloaded", self)
def get_button(self) -> Gtk.Button:
return self.button
def hide_label(self) -> None:
self.label.hide()
def get_folder(self) -> str:
return self.folder
def set_folder(self, folder: str) -> None:
prefix = offline.folder_prefix
self.folder = folder
self.label.set_markup(prefix + folder)
self.label.show()
self.unset_button.show()
def present_error(self, error: FolderError, msg: str) -> None:
if error == FolderError.FOLDER_CHANGED:
self.sidepop.set_label(error, msg)
self.sidepop.popup()
return
self.folder = ""
self.label.set_text("")
self.unset_button.hide()
self.pop.set_label(error, msg)
self.pop.popup()
class ModFrame(HeadingFrame):
def __init__(
self, parent: OfflineLoader, controller: "Controller", label: str
) -> None:
super().__init__(heading=label)
self.parent = parent
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.tree = OfflineModTreeView(controller)
self.tree.set_model(None)
self.tree.set_valign(Gtk.Align.FILL)
self.scrolled = NoOverlayScrolledWindow()
self.scrolled.set_margin_end(5)
self.scrolled.set_vexpand(True)
self.scrolled.add(self.tree)
self.status = Gtk.Label(
halign=Gtk.Align.START, margin_start=5, margin_top=3, margin_bottom=3
)
self.tree_vbox = VBox()
self.tree_vbox.extend([self.scrolled, self.status])
self.no_mods = HBox(spacing=10)
no_mods_icon = Icon(WARNING, margin_start=10)
no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START)
self.no_mods.extend([no_mods_icon, no_mods_label])
self.vbox.pack_end(self.no_mods, expand=True, fill=True, padding=3)
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
self.frame.add(self.vbox)
sel = self.tree.get_selection()
sel.connect("changed", self._on_selection_changed)
self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap)
def start_empty(self) -> None:
self.tree_vbox.hide()
self.no_mods.show()
def _on_map(self, widget: Self) -> None:
self.no_mods.hide()
self.tree.set_model(None)
self.tree_vbox.hide()
def _on_unmap(self, widget: Self) -> None:
self.tree.set_model(None)
self.tree_vbox.hide()
self.no_mods.hide()
def start(self, store: "FastInsertListStore") -> None:
# TODO: this manipulates mod man store out of band
self.tree.set_model(store)
self.tree.mod_man.set_store(store)
self.show_tree()
def get_mods(self) -> list[str]:
model, treeiters = self.tree.get_selection().get_selected_rows()
if model is None:
return []
if type(self) is CustomModFrame:
# NOTE: custom mod dirs to be hashed
return [model[_iter][2] for _iter in treeiters]
else:
# NOTE: pre-existing, canonical symlinks to published mods
return [model[_iter][1] for _iter in treeiters]
def show_tree(self) -> None:
self.tree_vbox.show()
def collapse_tree(self) -> None:
self.tree_vbox.hide()
def get_tree(self) -> OfflineModTreeView:
return self.tree
def pack(self, widget: Gtk.Widget) -> None:
self.vbox.pack_start(widget, expand=False, fill=False, padding=5)
def set_model(self, model: Union["FastInsertListStore", None]) -> None:
self.tree.set_model(model)
self.tree.mod_man.store = model
self.set_cursor()
def _on_selection_changed(self, sel: Gtk.TreeSelection) -> None:
model, rows = sel.get_selected_rows()
if len(rows) == 0:
status = generic.selectable_tree
else:
status = f"Mods selected: {len(rows)}"
self.status.set_label(status)
# TODO: cleaner delegation
# simply send int value or use emitter
self.parent.check_button()
def set_cursor(self) -> None:
path = Gtk.TreePath.new_from_indices([0])
self.tree.set_cursor(path)
self.tree.get_selection().unselect_all()
class CustomModFrame(ModFrame):
def __init__(
self, parent: OfflineLoader, controller: "Controller", heading: str
) -> None:
super().__init__(parent, controller, heading)
self.tree.set_menu(ContextMenuGroup.MOD_OFFLINE)
self.parent = parent
self.controller = controller
self.emitter = controller.get_emitter()
self.custom_hbox = FolderHBox(
controller, offline.custom_button, offline.custom_eventbox
)
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
self.pack(self.custom_hbox)
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
self.emitter.connect("custom_mods_unloaded", self._on_custom_mods_unloaded)
self.connect("map", self._on_map)
def _on_map(self, widget: Self) -> None:
self.hide_tree()
def _on_custom_mods_unloaded(self, emitter: "Emitter", widget: FolderHBox) -> None:
# TODO: kludgy workaround for generic button emitting global signal
if widget == self.custom_hbox:
self.hide_tree()
def hide_tree(self) -> None:
self.custom_hbox.hide_label()
self.tree.set_model(None)
self.tree_vbox.hide()
self.no_mods.hide()
def present_folder_changed(self, folder: str) -> None:
self.custom_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
def present_error(self, folder: str) -> None:
self.hide_tree()
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
def present_tree(self, store: "FastInsertListStore", folder: str) -> None:
self.custom_hbox.set_folder(folder)
self.tree.set_model(store)
self.tree_vbox.show()
def _on_custom_mods_loaded(
self,
emitter: "Emitter",
store: "FastInsertListStore",
folder: str,
) -> None:
self.custom_hbox.stop_spinner()
if len(store) == 0:
self.present_error(folder)
else:
self.present_tree(store, folder)
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
callback = self.custom_hbox.start_spinner
# TODO: cleaner delegation
self.parent.offline_man.find_custom_mods(callback)
def get_folder(self) -> str:
return self.custom_hbox.get_folder()
class MissionFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
super().__init__(heading=offline.mission_frame)
self.parent = parent
self.controller = controller
self.emitter = controller.get_emitter()
self.mission_hbox = FolderHBox(
controller, offline.mission_button, offline.mission_eventbox
)
self.mission_button = self.mission_hbox.get_button()
self.mission_button.connect("clicked", self._on_mission_button_clicked)
self.frame.add(self.mission_hbox)
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
def _on_mission_loaded(
self, emitter: "Emitter", folder: str, is_valid: bool
) -> None:
if is_valid:
self.mission_hbox.set_folder(folder)
else:
self.mission_hbox.present_error(FolderError.NO_VALID_MISSION, folder)
self.parent.check_button()
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
self.parent.offline_man.get_mission()
def get_mission(self) -> str:
return self.mission_hbox.get_folder()
def present_folder_changed(self, folder: str) -> None:
self.mission_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
class RadioFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
super().__init__(heading=offline.version)
self.controller = controller
self.id_map = {APPNAME_DAYZ: APPID_DAYZ, APPNAME_DAYZ_EXP_HUMAN: APPID_DAYZ_EXP}
self.appid = APPID_DAYZ
self.dayz = Gtk.RadioButton.new_with_label(None, APPNAME_DAYZ)
self.dayz_exp = Gtk.RadioButton.new_with_label_from_widget(
self.dayz, APPNAME_DAYZ_EXP_HUMAN
)
self.radio_box = HBox()
self.radio_box.extend([self.dayz, self.dayz_exp])
self.radio_box.set_margin_start(10)
self.radio_box.set_margin_bottom(5)
self.frame.add(self.radio_box)
# TODO: cleaner delegation
has_dayz_exp = parent.offline_man.has_dayz_exp()
self.dayz.connect("toggled", self._on_radio_toggled)
if has_dayz_exp is False:
self.dayz_exp.set_sensitive(False)
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
for el in self.dayz, self.dayz_exp:
if el.get_active():
label = el.get_label()
self.appid = self.id_map[label]
def get_appid(self) -> int:
return self.appid
class OfflineLoader(Gtk.Box):
def __init__(self, controller: "Controller"):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
margin_start=10,
margin_end=10,
)
self.controller = controller
self.controller.register_widget("offline_loader", self)
self.emitter = controller.get_emitter()
self.offline_man = OfflineManager(controller)
self.add(PageHeading(offline.heading))
self.local_frame = ModFrame(self, controller, offline.local_frame)
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
self.custom_tree = OfflineModTreeView(controller)
self.mission_frame = MissionFrame(self, controller)
self.radio_frame = RadioFrame(self, controller)
self.scrollable = Gtk.ScrolledWindow(
vexpand=True, propagate_natural_height=True
)
self.content_box = VBox(spacing=5)
self.content_box.extend(
[
self.local_frame,
self.custom_frame,
self.mission_frame,
self.radio_frame,
]
)
# TODO: share ConnectBox class with preconnect dialog?
self.button_box = HBox(spacing=5)
self.button_box.set_halign(Gtk.Align.END)
self.button_box.set_margin_top(15)
self.back = Gtk.Button(label="Back")
self.ok = Gtk.Button(label="Launch", sensitive=False)
self.back.connect("clicked", self._on_back_clicked)
self.ok.connect("clicked", self._on_ok_clicked)
self.connect("key-press-event", self._on_keypress)
self.emitter.connect("custom_mods_unloaded", lambda _, __: self.check_button())
self.button_box.extend([self.back, self.ok])
self.scrollable.add(self.content_box)
self.add(self.scrollable)
self.add(self.button_box)
def check_button(self) -> None:
local_mods = self.local_frame.get_mods()
custom_mods = self.custom_frame.get_mods()
mission = self.mission_frame.get_mission()
if len(local_mods) == 0 and len(custom_mods) == 0 and len(mission) == 0:
self.ok.set_sensitive(False)
else:
self.ok.set_sensitive(True)
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
self.back.emit("clicked")
def populate(self, store: Union["FastInsertListStore", None]) -> None:
if store is None:
self.local_frame.start_empty()
return
self.local_frame.start(store)
def _on_back_clicked(self, button: Gtk.Button) -> None:
self.controller.open_page(NotebookPage.MODS)
def _on_ok_clicked(self, button: Gtk.Button) -> None:
appid = self.radio_frame.get_appid()
mission = self.mission_frame.get_mission()
local_mods = self.local_frame.get_mods()
custom_folder = self.custom_frame.get_folder()
custom_mods = self.custom_frame.get_mods()
if custom_folder and not self.offline_man.is_custom_folder_valid(custom_folder):
self.custom_frame.present_folder_changed(mission)
return
if mission and not self.offline_man.is_mission_valid(mission):
self.mission_frame.present_folder_changed(mission)
return
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
+8 -22
View File
@@ -2,7 +2,7 @@ from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api import pefile as PeFile
from dzgui.api.steam import find_user_id
from dzgui.config import query
from dzgui.const.constants import (
APPID_DAYZ,
@@ -19,9 +19,8 @@ from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab
from dzgui.strings import errors, options
from dzgui.util import strings, css, open_links
from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo
@@ -134,16 +133,6 @@ class Options(Gtk.Box):
[LeftLabel(strings.options.name), self.player_box],
]
eb = InfoEventBox(options.workshop_eventbox, controller)
workshop_button = SteamWorkshopButton()
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(self.uid)
)
mod_rows = [
[LeftLabel(options.workshop_label), workshop_button, eb],
]
self.dayz_version_label = Gtk.Label(label=strings.null)
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
@@ -169,7 +158,6 @@ class Options(Gtk.Box):
api_box.add(api_links_box)
prefs_grid = self._make_grid(pref_rows)
mods_grid = self._make_grid(mod_rows)
version_grid = self._make_grid(version_rows)
col = 1
@@ -187,12 +175,13 @@ class Options(Gtk.Box):
if prefs.is_debug:
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
for frame in [
HeadingFrame(api_box, strings.options.api_keys),
HeadingFrame(prefs_grid, strings.options.prefs),
HeadingFrame(mods_grid, strings.options.mods),
HeadingFrame(version_grid, strings.options.version),
for pair in [
(api_box, strings.options.api_keys),
(prefs_grid, strings.options.prefs),
(version_grid, strings.options.version),
]:
frame = HeadingFrame.new_with_widget_and_label(*pair)
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
row += 1
@@ -414,9 +403,6 @@ class Options(Gtk.Box):
bm = self.controller.query_config(Preferences.BM)
steam_path = Path(default_steam_path)
# NOTE: this is a best effort guess at the most recent user
uid = find_user_id(steam_path)
self.uid = "" if uid is None else uid
self.old_steam = steam
self.old_bm = bm
+38 -36
View File
@@ -17,7 +17,7 @@ from dzgui.views.trees.tree_server_mods import ServerModTreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk # type: ignore # noqa E402
from gi.repository import Gdk, Gtk # noqa E402
if TYPE_CHECKING:
@@ -85,9 +85,9 @@ class MaskedTree(Gtk.TreeView):
self.store.append([self.icon, item])
class PreConnectionAssistant(Gtk.ScrolledWindow):
class PreConnectionAssistant(Gtk.Box):
def __init__(self, controller: "Controller") -> None:
super().__init__()
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.controller = controller
@@ -106,19 +106,12 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
)
self.ok = Gtk.Button(label=preconnect.update_mods, halign=Gtk.Align.END)
# TODO: abstract
self.raise_window = Gtk.CheckButton(
label="Foreground DZGUI while downloading",
self.connect_last = Gtk.Button(
label=preconnect.connect_last,
halign=Gtk.Align.END,
hexpand=True,
valign=Gtk.Align.END,
visible=False,
has_tooltip=True,
sensitive=False,
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
active=True,
tooltip_text=preconnect.connect_last_tooltip,
)
self.button_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.END,
@@ -126,13 +119,13 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
spacing=5,
)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
for button in self.back, self.ok:
for button in self.back, self.ok, self.connect_last:
box.add(button)
for el in self.raise_window, box:
self.button_box.add(el)
self.button_box.add(box)
self.back.connect("clicked", self._on_back_clicked)
self.ok.connect("clicked", self._on_ok_clicked)
self.connect_last.connect("clicked", self._on_connect_last_clicked)
self.title = Gtk.Label(label="")
add_class(self.title, "preconnect-heading")
@@ -148,7 +141,7 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.progress_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
self.progress_box.add(self.mod_count)
self.scrolled = Gtk.ScrolledWindow()
self.scrolled = Gtk.ScrolledWindow(margin_end=10)
self.scrolled.add(self.tree)
self.scrolled.set_size_request(600, 400)
@@ -160,33 +153,43 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
self.mods_placeholder = Placeholder("This server has no mods.")
self.tree_box.add(self.mods_placeholder)
self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods)
self.tree_frame = HeadingFrame.new_with_widget_and_label(
self.tree_box, preconnect.mods
)
# TODO: abstract into components
# TODO: strings
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.warning_tree = MaskedTree(WARNING)
# TODO: strings
self.warning_placeholder = Placeholder("No warnings.")
self.warning_box.add(self.warning_tree)
self.warning_box.add(self.warning_placeholder)
self.warning_frame = HeadingFrame(self.warning_box, preconnect.warnings)
self.warning_frame = HeadingFrame.new_with_widget_and_label(
self.warning_box, preconnect.warnings
)
# TODO: strings
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.error_tree = MaskedTree(ERROR)
# TODO: strings
self.error_placeholder = Placeholder("No errors.")
self.error_box.add(self.error_tree)
self.error_box.add(self.error_placeholder)
self.error_frame = HeadingFrame(self.error_box, preconnect.errors)
self.error_frame = HeadingFrame.new_with_widget_and_label(
self.error_box, preconnect.errors
)
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.box.add(self.title)
self.box.add(self.tree_frame)
self.box.add(self.warning_frame)
self.box.add(self.error_frame)
self.box.add(self.button_box)
self.add(self.box)
self.scrolled_box = Gtk.ScrolledWindow(
vexpand=True, propagate_natural_height=True
)
self.scrolled_box.add(self.box)
self.add(self.scrolled_box)
self.add(self.button_box)
self.connect("key-press-event", self._on_keypress)
self.connect("map", self._on_map)
@@ -201,8 +204,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
for child in widgets:
child.set_visible(True)
self.raise_window.set_visible(False)
self.raise_window.set_sensitive(False)
self.ok.set_sensitive(True)
self.ok.set_label(preconnect.update_mods)
@@ -213,8 +214,11 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
if event.keyval == Gdk.KEY_u:
self.ok.emit("clicked")
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_load_to_menu()
def _on_ok_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_connect(self.raise_window.get_active())
self.controller.update_and_connect()
def _on_back_clicked(self, button: Gtk.Button) -> None:
self.controller.open_page(NotebookPage.SERVERS)
@@ -298,21 +302,19 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
if prereqs.required_space == 0:
self.ok.set_label(preconnect.connect)
self.raise_window.set_visible(False)
else:
self.raise_window.set_visible(True)
self.raise_window.set_sensitive(True)
pretty = number(prereqs.required_space)
suffix = f" Need to download {pretty} MiB of mod updates."
prefix = preconnect.total_mods
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
self._process_warnings(prereqs)
if self.tree.is_visible():
self.tree.grab_focus()
if prereqs.is_last_server:
self.connect_last.show()
else:
self.grab_focus()
self.connect_last.hide()
self._process_warnings(prereqs)
self.tree.grab_focus()
def mark_finished(self) -> None:
self.mod_count.set_label(preconnect.all_updated)
+8 -15
View File
@@ -16,7 +16,7 @@ logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.controllers.mc import Emitter
from dzgui.controllers.emitter import Emitter
class ScrollableTree(Gtk.ScrolledWindow):
@@ -47,7 +47,13 @@ class ServerNotebook(Gtk.ScrolledWindow):
self.recent = ServerTreeView(
controller, ServerTab.RECENT, ContextMenuGroup.RECENT
)
self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN)
self.lan = ServerTreeView(
controller,
ServerTab.LAN,
ContextMenuGroup.SCAN_LAN,
show_empty=True,
show_full=True,
)
tabs = [
(self.browser, server_labels.browser),
@@ -162,19 +168,6 @@ class ServerNotebook(Gtk.ScrolledWindow):
return tv
raise ValueError("No treeview set")
def add_notification(self) -> None:
saved = self.notebook.get_nth_page(1)
if saved is None:
return
text = self.notebook.get_tab_label_text(saved)
if text is None:
return
# TODO: strings
if "*" in text:
return
text += "*"
self.notebook.set_tab_label_text(saved, text)
def update_tab_widths(self, col: Gtk.TreeViewColumn) -> None:
# TODO: may cause pixel offsets when application is maximized
width = col.get_width()
+11 -4
View File
@@ -4,7 +4,7 @@ from typing import Any, Optional, TYPE_CHECKING
from warnings import deprecated
from dzgui.const.constants import APP_NAME, SEPARATOR
from dzgui.util.keys import is_navkey
from dzgui.util.keys import is_ctrl_mask, is_navkey
from dzgui.views.mixins.cursor_mixin import CursorMixin
import gi
@@ -85,10 +85,17 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
model, rows = sel.get_selected_rows()
return [model[row] for row in rows]
def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool:
from dzgui.views.trees.tree_servers import ServerTreeView
# NOTE: suppress popup search and delegate binding to server view (cf. request_keyword_focus)
if not isinstance(treeview, ServerTreeView):
if is_ctrl_mask(event) and event.keyval == Gdk.KEY_f:
return True
if is_navkey(event.keyval):
if self.get_model() is None:
return
return True
if self.sel_blocked is False:
self.sel_blocked = True
self.controller.suppress_signal(
@@ -98,7 +105,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
True,
)
self._vim_nav(event)
return
return False
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
"""
+48 -8
View File
@@ -3,6 +3,7 @@ from typing import Any, TYPE_CHECKING
from dzgui.const.constants import APP_NAME, HEX_RED
from dzgui.const.enum import ContextMenuGroup
from dzgui.managers.mods import ModManager
from dzgui.util import strings, localize
from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.mixins.mods_mixin import ModsMixin
@@ -22,16 +23,19 @@ logger = logging.getLogger(APP_NAME)
class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
def __init__(self, controller: "Controller") -> None:
super().__init__(controller, menu=ContextMenuGroup.MOD)
def __init__(
self, controller: "Controller", menu: ContextMenuGroup = ContextMenuGroup.MOD
) -> None:
super().__init__(controller, menu=menu)
self.controller = controller
self.mod_man = ModManager(self, controller)
emitter = self.controller.get_emitter()
emitter.connect("mods_updated", self._on_mods_updated)
emitter.connect("mods_highlighted", self._on_mods_highlighted)
self.set_fixed_height_mode(True)
self.set_headers_visible(True)
self.set_model(None)
for i, column_title in enumerate(strings.mod_cols):
@@ -58,12 +62,40 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
self.connect("button-press-event", self.present_menu)
self.connect("key-press-event", self.present_menu)
self.connect("button-press-event", self.on_row_click)
def on_row_click(self, widget: Gtk.TreeView, event: Gdk.EventButton) -> bool:
if event.state is Gdk.ModifierType.CONTROL_MASK and event.button == 1:
path_info = widget.get_path_at_pos(int(event.x), int(event.y))
if path_info is None:
return False
path, column, cellx, celly = path_info
model = self.get_model()
if model is None:
return False
if path is None:
return False
_iter = model.get_iter(path)
if self.get_selection().iter_is_selected(_iter):
self.get_selection().unselect_iter(_iter)
else:
self.get_selection().select_iter(_iter)
return True
return False
def get_mod_man(self) -> ModManager:
return self.mod_man
def load_mods(self) -> None:
self.mod_man.load_mods()
def _on_mods_highlighted(self, emitter: "Emitter") -> None:
self.get_selection().unselect_all()
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
if mods < 1:
return
self.set_headers_visible(True)
path = Gtk.TreePath.new_from_indices([0])
self.set_cursor(path)
@@ -88,13 +120,12 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
) -> Any:
) -> None:
state = model[it][4]
if state is True:
cell.set_property("foreground", HEX_RED)
else:
cell.set_property("foreground", None)
return
def _format_float(
self,
@@ -103,8 +134,17 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
) -> Any:
) -> None:
val = model[it][3]
formatted = localize.number(val)
cell.set_property("text", formatted)
return
# NOTE: reapply color property after float conversion
self._format_color(column, cell, model, it, data)
def set_menu(self, context: ContextMenuGroup) -> None:
self.menu = context
class OfflineModTreeView(ModTreeView):
def __init__(self, controller: "Controller") -> None:
super().__init__(controller)
+13 -8
View File
@@ -38,7 +38,12 @@ QUEUE_CHECK_DELAY = 200
class ServerTreeView(ContextMixin, TreeView): # type: ignore
def __init__(
self, controller: "Controller", enum: ServerTab, menu: ContextMenuGroup
self,
controller: "Controller",
enum: ServerTab,
menu: ContextMenuGroup,
show_empty: bool = False,
show_full: bool = False,
) -> None:
super().__init__(controller, menu=menu)
@@ -48,7 +53,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
self.loaded = False
self.filter_man = FilterManager()
self.filter_man = FilterManager(show_empty, show_full)
self.proxy_man = ProxyModelManager(self.filter_man)
model = self.proxy_man.get_proxy_model()
self.set_model(model)
@@ -72,6 +77,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
logger.critical(e)
valid_json = False
# NOTE: reasonable defaults for long columns
width_map = {
"Name": 800,
"Map": 300,
@@ -96,8 +102,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
column.set_fixed_width(saved_size)
column.set_expand(True)
else:
w = width_map[column_title]
column.set_fixed_width(w)
if column_title in width_map:
w = width_map[column_title]
column.set_fixed_width(w)
if column_title == "Ping":
column.set_cell_data_func(renderer, self._get_ping)
@@ -162,8 +169,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _on_map(self, widget: Self) -> None:
# TODO: disable filter panel if current model is None
if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", True)
state = self.get_enum() is ServerTab.LAN
self.emitter.emit("lan_tab_toggled", state)
store = self.filter_man.get_map_store()
@@ -178,8 +185,6 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
# NOTE: removes queue checker for this tab
GLib.Source.remove(self.queue_id)
self.emitter.disconnect(self.handler_id)
if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", False)
def _on_col_width_changed(
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b2"
version = "7.0.0b12"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
+55
View File
@@ -0,0 +1,55 @@
from pathlib import Path
BOILERPLATE = """
This file includes the combined licenses to the following components:
1) The DZGUI utility
2) The Python interpreter
3) Third-party dependencies
- dayzquery
- requests
- packaging
- psutil
- PyGObject
- python-a2s
When shipped as a binary installer, these components are collectively bundled
as a turnkey application and used to run the DZGUI utility.\n\n
"""
DZGUI_HEADER = "DZGUI application"
LICENSE_FILE = "LICENSE"
class CombinedLicense:
def __init__(self) -> None:
self.text = BOILERPLATE
def append(self, text: str) -> None:
self.text += text + "\n\n"
def add_header(self, text: str) -> None:
self.append(f"--- License for the component '{text}' follows ---")
def add_dependency(self, dep: str) -> None:
self.add_header(dep)
def get_text(self) -> str:
return self.text
def concat_license() -> str:
combined = CombinedLicense()
root = Path(__file__).resolve().parents[1]
licenses = root.joinpath("docs/licenses")
main_license = root.joinpath(LICENSE_FILE)
combined.add_header(DZGUI_HEADER)
combined.append(main_license.read_text())
for _dir in licenses.iterdir():
# NOTE: expects a single file
docs = _dir.glob("*")
doc = list(docs)[0]
combined.add_dependency(_dir.name)
combined.append(doc.read_text())
return combined.get_text()
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env bash
echo "$PWD"
url="https://github.com/ofek/pyapp/releases/latest/download/source.tar.gz"
output="${PWD}/pyapp-source.tar.gz"
curl "$url" -Lo "$output"
tar -xzf "$output"
mv ${PWD}/pyapp-v* ${PWD}/pyapp-latest
rm "$output"
+12
View File
@@ -0,0 +1,12 @@
import tomllib
from pathlib import Path
root = Path(__file__).resolve().parents[1]
def get_entrypoint() -> None:
p = root.joinpath("pyproject.toml")
with open(p, "rb") as f:
t = tomllib.load(f)
name = t["project"]["name"]
return t["project"]["scripts"][name]
+84
View File
@@ -0,0 +1,84 @@
import os
import requests
import subprocess
import tarfile
from pathlib import Path
from urllib.parse import unquote
BUILD = "3.13.9"
DATE = "20251014"
CPYTHON_BUILD = f"cpython-{BUILD}"
root = Path(__file__).resolve().parents[1]
build_dir = root.joinpath("build")
cpython_dir = build_dir.joinpath(CPYTHON_BUILD)
def fetch(url: str, destination: str) -> None:
print(f"Fetching URL '{url}'")
res = requests.get(url)
code = res.status_code
if code != 200:
raise Exception(f"URL '{url}' returned status code {code}")
with open(build_dir.joinpath(destination), "wb") as f:
f.write(res.content)
def get_cpython() -> None:
source = f"https://github.com/astral-sh/python-build-standalone/releases/download/{DATE}/"
build = f"{CPYTHON_BUILD}%2B{DATE}-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz"
url = f"{source}{build}"
outfile = unquote(build)
fetch(url, outfile)
version = build.split("%")[0]
untar(outfile, "python", version)
def get_pyapp() -> None:
url = "https://github.com/ofek/pyapp/releases/latest/download/source.tar.gz"
outfile = "pyapp-latest.tar.gz"
fetch(url, outfile)
untar(outfile, "pyapp-*", "pyapp-latest")
def untar(file: str, glob: str, renamed: str) -> None:
full_path = build_dir.joinpath(file)
with tarfile.open(full_path, "r:gz") as tar:
tar.extractall(path=build_dir)
full_path.unlink()
for _dir in build_dir.glob(glob):
os.rename(_dir, build_dir.joinpath(renamed))
def install_deps() -> None:
pip = build_dir.joinpath(f"{CPYTHON_BUILD}/bin/pip")
proc = subprocess.run([pip, "install", root])
return proc.returncode
def repackage() -> None:
print("Preparing tar archive...")
outfile = build_dir.joinpath("airgapped.tar.gz")
with tarfile.open(outfile, "w:gz") as tar:
tar.add(build_dir.joinpath(CPYTHON_BUILD), arcname="python")
def rebuild_cpython() -> None:
if cpython_dir.is_dir() is False:
get_cpython()
code = install_deps()
if code != 0:
return
repackage()
return get_packaged_version()
def get_packaged_version() -> str:
proc = subprocess.run(
[cpython_dir.joinpath("bin/dzgui"), "-v"],
capture_output=True,
text=True,
check=True,
)
return proc.stdout.rstrip()
+76 -16
View File
@@ -1,41 +1,101 @@
import build
import os
import subprocess
import sys
import tarfile
from pathlib import Path
root = Path(__file__).resolve().parents[1]
output = root.joinpath("dist")
from concat_licenses import concat_license
from parse_toml import get_entrypoint
from prebuild import get_pyapp, rebuild_cpython
pyapp_dir = root.joinpath("pyapp-latest")
def update_uname(info: tarfile.TarInfo) -> tarfile.TarInfo:
info.uname = "dzgui"
info.gname = "users"
return info
root = Path(__file__).resolve().parents[1]
dist_dir = root.joinpath("dist")
build_dir = root.joinpath("build")
builder = build.ProjectBuilder(root)
wheel = builder.build("wheel", output_directory=output)
wheel = builder.build("wheel", output_directory=dist_dir)
stem = Path(wheel).stem
tarname = f"{stem}.tar.gz"
metadata = stem.split("-")
appname = metadata[0]
version = metadata[1]
entrypoint = "dzgui.main:main"
pyapp_dir = build_dir.joinpath("pyapp-latest")
if pyapp_dir.is_dir() is False:
get_pyapp()
# TODO: get resulting filename from this function
packaged_version = rebuild_cpython()
assert packaged_version == version
cpython = build_dir.joinpath("airgapped.tar.gz")
entrypoint = get_entrypoint()
env = os.environ
env["PYAPP_PROJECT_VERSION"] = version
env["PYAPP_PROJECT_NAME"] = appname
env["PYAPP_EXEC_SPEC"] = entrypoint
env["PYAPP_PROJECT_PATH"] = wheel
env["PYAPP_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_PYTHON_VERSION"] = "3.13"
env["PYAPP_PASS_LOCATION"] = "true"
proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir)
if proc.returncode == 0:
output_exe = root.joinpath("pyapp-latest/target/release/pyapp")
release_exe = output.joinpath(appname)
output_exe.rename(release_exe)
tarpath = output.joinpath(tarname)
with tarfile.open(tarpath, "w:gz") as tar:
tar.add(release_exe, arcname=appname)
# NOTE: explicitly install all dependencies into distribution
env["PYAPP_SKIP_INSTALL"] = "true"
env["PYAPP_DISTRIBUTION_EMBED"] = "true"
env["PYAPP_FULL_ISOLATION"] = "true"
env["PYAPP_DISTRIBUTION_PATH"] = str(cpython)
env["PYAPP_DISTRIBUTION_PYTHON_PATH"] = "python/bin/python3"
platform = "x86_64-unknown-linux-musl"
build_name = f"{appname}-{version}-{platform}"
tarname = f"{build_name}.tar.gz"
tarpath = dist_dir.joinpath(tarname)
build_params = [
"cargo",
"build",
"--release",
"--target",
platform,
]
proc = subprocess.run(
build_params,
env=env,
cwd=pyapp_dir,
)
if proc.returncode != 0:
sys.exit(1)
subfolder = dist_dir.joinpath(build_name)
subfolder.mkdir(parents=True)
output_exe = pyapp_dir.joinpath(f"target/{platform}/release/pyapp")
release_exe = subfolder.joinpath(appname)
output_exe.rename(release_exe)
combined_licenses = concat_license()
license_file = subfolder.joinpath("LICENSE")
license_file.write_text(combined_licenses)
with tarfile.open(tarpath, "w:gz") as tar:
info = tar.gettarinfo(subfolder)
tar.add(subfolder, arcname=appname, filter=update_uname)
print(f"Wrote tarfile to '{tarpath}'")
proc = subprocess.run([release_exe, "-v"], capture_output=True, text=True)
assert proc.stdout.rstrip() == version
# TODO: clean up staging directory
release_exe.unlink()
license_file.unlink()
subfolder.rmdir()
Path(wheel).unlink()
+14
View File
@@ -0,0 +1,14 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"Maximum": 1,
"Perspective": 1
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"cols": {
"Name": 398,
"Map": 181,
"Gametime": 151,
"Players": 119,
"Queue": 96,
"IP": 247,
"Qport": 94,
"Ping": 253
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"cols": {
"Name": 1,
"Map": 1,
"Gametime": 1,
"Players": 1,
"Queue": 1,
"IP": 1,
"Qport": 1,
"Ping": 1,
"View": 1
}
}
+5
View File
@@ -0,0 +1,5 @@
protocol = 1;
publishedid = 1234567890;
name = "ModName";
timestamp = 638278452000000000;
hash = "a1b2c3d4e5f6g7h8i9j0";
+5
View File
@@ -0,0 +1,5 @@
protocol = 1
publishedid = 1234567890
name = "ModName"
timestamp = 638278452000000000
hash = "a1b2c3d4e5f6g7h8i9j0"
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more