mirror of
https://github.com/aclist/dztui.git
synced 2026-11-04 17:12:09 +01:00
Compare commits
103
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c080847efc | ||
|
|
40e244e05a | ||
|
|
8edb38f175 | ||
|
|
485d0721da | ||
|
|
5b90068fe4 | ||
|
|
58a2bf0bfa | ||
|
|
88ebbf4b49 | ||
|
|
3f6c7008ae | ||
|
|
63a853af7c | ||
|
|
1bbc99100a | ||
|
|
41b0e86fda | ||
|
|
460b07a4c5 | ||
|
|
abdfd2e995 | ||
|
|
fa486c2b76 | ||
|
|
5776f4c49f | ||
|
|
48cb062d90 | ||
|
|
3b35d75b72 | ||
|
|
8eb0a17b78 | ||
|
|
ad1598ee0d | ||
|
|
f4b653e9dd | ||
|
|
bddf792280 | ||
|
|
3e3a86f985 | ||
|
|
9e2d528436 | ||
|
|
dac02d75f6 | ||
|
|
3a5c061dbf | ||
|
|
3851b76ca0 | ||
|
|
387930b147 | ||
|
|
c0c3ee8c64 | ||
|
|
1212c09110 | ||
|
|
86e2249a23 | ||
|
|
302a43e65e | ||
|
|
3dc58c1a69 | ||
|
|
9af2145ee8 | ||
|
|
7b4382b178 | ||
|
|
ea3631072b | ||
|
|
67bae3c3e0 | ||
|
|
1ea30270ce | ||
|
|
2c9b36ba84 | ||
|
|
fd9eb33714 | ||
|
|
15d928026a | ||
|
|
24d3bc8209 | ||
|
|
881847cee9 | ||
|
|
d3a06cf970 | ||
|
|
741344e75e | ||
|
|
a8a7aba338 | ||
|
|
fafd4c6000 | ||
|
|
7e5e326ebe | ||
|
|
e7644b49f5 | ||
|
|
88aa1c31d5 | ||
|
|
dbeaad73c6 | ||
|
|
be6be2af7c | ||
|
|
63889c2299 | ||
|
|
78e17121b6 | ||
|
|
69ab3723a1 | ||
|
|
c82d42da09 | ||
|
|
9266473853 | ||
|
|
b5d9c030dd | ||
|
|
4852e4a31f | ||
|
|
d56cfdd0b2 | ||
|
|
760208917c | ||
|
|
26006daa74 | ||
|
|
97244e783c | ||
|
|
162839672b | ||
|
|
ac9f582def | ||
|
|
fe40346a01 | ||
|
|
d1cf2a9850 | ||
|
|
4face6f115 | ||
|
|
e3027fa830 | ||
|
|
92699ebaa9 | ||
|
|
ef2709561d | ||
|
|
89a6996695 | ||
|
|
41b3ad8eca | ||
|
|
d57277f527 | ||
|
|
c45678ca88 | ||
|
|
c95ad53f6f | ||
|
|
3e144dc7d2 | ||
|
|
afdfa76fdf | ||
|
|
00c27ad020 | ||
|
|
d60204b676 | ||
|
|
3d2e27bfe1 | ||
|
|
8a66bafe4c | ||
|
|
2963c4e01c | ||
|
|
f849cbc183 | ||
|
|
77505db621 | ||
|
|
ba7a2b7ab4 | ||
|
|
6d7ed80996 | ||
|
|
72024c5d43 | ||
|
|
0426d50d3e | ||
|
|
26ee560cb4 | ||
|
|
6ec77f6542 | ||
|
|
59959ae3a1 | ||
|
|
05fd95cd8e | ||
|
|
86ba8d9187 | ||
|
|
e715e6da1c | ||
|
|
18ba851b7e | ||
|
|
d2643d08e9 | ||
|
|
6274f62c93 | ||
|
|
24609a1c71 | ||
|
|
1b344c1260 | ||
|
|
e72e09eec1 | ||
|
|
00e78b1228 | ||
|
|
efddd61ea1 | ||
|
|
d006908bbc |
No files matched your search
@@ -8,7 +8,7 @@ jobs:
|
||||
mirror-to-codeberg:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
|
||||
+5
-1
@@ -34,6 +34,9 @@
|
||||
- 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
|
||||
|
||||
|
||||
## Changed
|
||||
@@ -49,6 +52,8 @@
|
||||
- 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
|
||||
|
||||
## Dropped
|
||||
- Debug mode
|
||||
@@ -64,7 +69,6 @@
|
||||
|
||||
## Unreleased
|
||||
- Load offline mods
|
||||
- Choose to jump into splash screen instead of server
|
||||
- Local documentation
|
||||
- Raw debug command in context menu
|
||||
|
||||
|
||||
+22
-15
@@ -1,23 +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`
|
||||
# TODO: secondary script that packs this archive
|
||||
2. Download cpython 3.13, linux, gnu, v3
|
||||
https://github.com/astral-sh/python-build-standalone/releases/download/20251014/cpython-3.13.9%2B20251014-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz
|
||||
3. Explicitly install dependencies into site-packages
|
||||
4. Pack back into a tarball
|
||||
5. Run `python3 scripts/release.py`
|
||||
6. Generate a release and tag against the bundled `dzgui` executable
|
||||
### 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 resulting tarfile generated in `dist`
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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:
|
||||
|
||||
+1
-2
@@ -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
|
||||
@@ -61,6 +61,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
|
||||
"""
|
||||
@@ -472,6 +474,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 +511,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"]
|
||||
|
||||
@@ -146,6 +146,23 @@ def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> i
|
||||
return proc.returncode
|
||||
|
||||
|
||||
def load_to_menu(client: str, addr: 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 find_user_id(path: Path) -> str | None:
|
||||
resolved_path = path / "config" / "loginusers.vdf"
|
||||
try:
|
||||
|
||||
+50
-39
@@ -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
@@ -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
|
||||
)
|
||||
@@ -28,6 +28,7 @@ APP_NAME = "DZGUI"
|
||||
APP_NAME_LOWER = "dzgui"
|
||||
APP_NAME_ABBR = "dzg"
|
||||
|
||||
HEX_GREEN = "#32CD32"
|
||||
HEX_RED = "#FF0000"
|
||||
HEX_ORANGE = "#FFAC1C"
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
|
||||
|
||||
# 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 +151,6 @@ 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
|
||||
+38
-13
@@ -84,7 +84,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
|
||||
@@ -260,6 +259,21 @@ class Controller(GObject.GObject):
|
||||
def select_colorized(self) -> None:
|
||||
self.mod_man.select_colorized()
|
||||
|
||||
def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
|
||||
servers = self.get_servers()
|
||||
all_filters: dict[str, dict[str, bool]] = {}
|
||||
trees = (
|
||||
servers.get_browser(),
|
||||
servers.get_saved(),
|
||||
servers.get_recent(),
|
||||
servers.get_lan(),
|
||||
)
|
||||
for tree in trees:
|
||||
enum = str(tree.get_enum())
|
||||
filters = tree.get_filter_man().get_filters()
|
||||
all_filters[enum] = filters
|
||||
return all_filters
|
||||
|
||||
def get_filter_man(self) -> "FilterManager":
|
||||
"""Each ServerTreeView has an atomic FilterManager"""
|
||||
return self.get_active_treeview().get_filter_man()
|
||||
@@ -303,8 +317,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 +347,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 +409,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,6 +511,9 @@ class Controller(GObject.GObject):
|
||||
ind = self.config_man.get_start_tab()
|
||||
self.get_servers().notebook.set_current_page(ind)
|
||||
|
||||
def update_and_load_to_menu(self, raise_window: bool) -> None:
|
||||
self.connection_man.update_and_connect(raise_window, menu_only=True)
|
||||
|
||||
def update_and_connect(self, raise_window: bool) -> None:
|
||||
self.connection_man.update_and_connect(raise_window)
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
- 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
|
||||
|
||||
|
||||
## Changed
|
||||
@@ -49,6 +52,8 @@
|
||||
- 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
|
||||
|
||||
## Dropped
|
||||
- Debug mode
|
||||
@@ -64,7 +69,6 @@
|
||||
|
||||
## Unreleased
|
||||
- Load offline mods
|
||||
- Choose to jump into splash screen instead of server
|
||||
- Local documentation
|
||||
- Raw debug command in context menu
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+21
-9
@@ -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:
|
||||
|
||||
+13
-16
@@ -6,14 +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 (
|
||||
@@ -24,17 +21,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
|
||||
@@ -121,8 +117,6 @@ def main() -> None:
|
||||
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()
|
||||
@@ -130,16 +124,19 @@ def main() -> None:
|
||||
if has_steam_client() is False:
|
||||
EarlyAlertDialog(init.requires_steam)
|
||||
|
||||
# NOTE: clears versions file of unlinked mods
|
||||
rebuild_symlinks(XDG.config)
|
||||
remove_stale_signatures(XDG.config, XDG.version)
|
||||
|
||||
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)
|
||||
|
||||
bootwin = BootWindow(XDG, version)
|
||||
local_coords, latest_release = bootwin.get_results()
|
||||
|
||||
# rebuild_symlinks(XDG.config)
|
||||
# remove_stale_signatures(XDG.config, XDG.version)
|
||||
|
||||
## TODO: handle IP DB failure and use coords fallback
|
||||
# local_coords = get_local_coords(XDG.ips)
|
||||
# latest_release = check_updates(version)
|
||||
|
||||
use_miles = lookup(XDG.config, Preferences.DIST)
|
||||
prefs = UserPrefs(
|
||||
is_steam_deck=_is_steam_deck,
|
||||
is_game_mode=_is_game_mode,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from collections import deque
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import (
|
||||
@@ -52,11 +53,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)
|
||||
@@ -159,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)
|
||||
@@ -173,6 +191,7 @@ class ConfigManager:
|
||||
|
||||
try:
|
||||
write_json(data, columns_file)
|
||||
write_json(filters, filters_file)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
|
||||
@@ -216,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
|
||||
@@ -15,6 +15,7 @@ from dzgui.api.steam import (
|
||||
enqueue_mod,
|
||||
get_remote_signatures,
|
||||
get_needs_update,
|
||||
load_to_menu,
|
||||
)
|
||||
|
||||
from dzgui.api.mods import (
|
||||
@@ -74,6 +75,7 @@ class Prerequisites:
|
||||
steam_proc: SteamProcess
|
||||
mods: list[list[str]]
|
||||
game_mode: bool
|
||||
is_last_server: bool
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
@@ -161,7 +163,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()
|
||||
|
||||
@@ -172,6 +174,8 @@ class ConnectionManager:
|
||||
|
||||
game_mode = prefs.is_game_mode
|
||||
|
||||
is_last = self.is_last_server()
|
||||
|
||||
prereqs = Prerequisites(
|
||||
name=info.server_name,
|
||||
appid=info.game_id,
|
||||
@@ -186,11 +190,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)
|
||||
@@ -222,7 +238,8 @@ class ConnectionManager:
|
||||
def query_modlist_and_present(self, record: Servers.Record) -> None:
|
||||
try:
|
||||
mods = self._query_modlist(record)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(f"{type(e).__name__}: {e}")
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._server_timeout), destroy_first=True
|
||||
)
|
||||
@@ -244,11 +261,14 @@ 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(client, addr, self.appid, playername, self.remote_mod_ids)
|
||||
else:
|
||||
rc = connect(client, addr, self.appid, playername, self.remote_mod_ids)
|
||||
if rc != 0:
|
||||
# TODO: log/pop the error
|
||||
func = StoredFunc(self.controller.update_status)
|
||||
@@ -270,17 +290,22 @@ 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:
|
||||
def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None:
|
||||
# NOTE: fast enqueue all mods in auto mode
|
||||
prefs = self.controller.get_prefs()
|
||||
|
||||
for title, mod, stamp, size in self.missing_mods:
|
||||
# TODO: check cancel and exit events
|
||||
if self.controller.get_exit_event().is_set():
|
||||
return
|
||||
if self.controller.get_cancel_event().is_set():
|
||||
self.controller.clear_cancel_event()
|
||||
return
|
||||
enqueue_mod(mod, self.appid)
|
||||
time.sleep(2)
|
||||
time.sleep(3)
|
||||
|
||||
if raise_window is True:
|
||||
logger.info("Bringing window to foreground")
|
||||
@@ -306,11 +331,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, raise_window: bool, menu_only: bool = False) -> None:
|
||||
if len(self.missing_mods) > 0:
|
||||
self._update_mods(raise_window)
|
||||
self._update_mods(raise_window, menu_only)
|
||||
else:
|
||||
self._connect_steam()
|
||||
self._connect_steam(menu_only)
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import logging
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
TMP_EXE,
|
||||
TMP_PATH,
|
||||
TMP_TARBALL,
|
||||
)
|
||||
from dzgui.strings import dialogs
|
||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
class UpdateManager:
|
||||
def __init__(self, controller: "Controller") -> None:
|
||||
|
||||
self.thread_man = ThreadingManager(controller)
|
||||
self.controller = controller
|
||||
|
||||
@call_on_thread(dialogs.fetching_update)
|
||||
def update_version(self, exe_path: str, url: str) -> None:
|
||||
try:
|
||||
res = requests.get(url)
|
||||
if res.status_code == 200:
|
||||
with open(TMP_TARBALL, "wb") as file:
|
||||
file.write(res.content)
|
||||
with tarfile.open(TMP_TARBALL) as tar:
|
||||
tar.extractall(TMP_PATH)
|
||||
|
||||
shutil.move(TMP_EXE, exe_path)
|
||||
|
||||
proc = subprocess.run([exe_path, "self", "restore"])
|
||||
if proc.returncode == 0:
|
||||
func = StoredFunc(self._on_update_success)
|
||||
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
||||
else:
|
||||
msg = dialogs.failed_to_update
|
||||
func = StoredFunc(self._on_update_failure, msg)
|
||||
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
||||
except Exception as e:
|
||||
func = StoredFunc(self._on_update_failure, e)
|
||||
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
||||
logger.warning(e)
|
||||
|
||||
def _on_update_success(self) -> None:
|
||||
msg = dialogs.update_success
|
||||
dialog = QuitDialog(self.controller, msg)
|
||||
dialog.run()
|
||||
|
||||
def _on_update_failure(self, msg: str) -> None:
|
||||
dialog = ExceptionDialog(self.controller, msg)
|
||||
dialog.run()
|
||||
@@ -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)
|
||||
|
||||
|
||||
+39
-16
@@ -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,13 +125,13 @@ 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:
|
||||
@@ -137,6 +139,11 @@ class ServerModelManager:
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
if self.controller.get_exit_event().is_set():
|
||||
event.set()
|
||||
return
|
||||
if self.controller.get_cancel_event().is_set():
|
||||
event.set()
|
||||
self.controller.clear_cancel_event()
|
||||
return
|
||||
res = future.result(timeout=LAN_TIMEOUT)
|
||||
if res is None:
|
||||
@@ -219,15 +226,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)
|
||||
@@ -237,19 +241,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()
|
||||
@@ -316,6 +327,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:
|
||||
@@ -366,33 +378,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)
|
||||
|
||||
@@ -4,3 +4,5 @@ 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."
|
||||
@@ -0,0 +1,5 @@
|
||||
symlinks = "Rebuilding symlinks"
|
||||
signatures = "Updating mod signatures"
|
||||
geo = "Checking geolocation records"
|
||||
coords = "Checking local coordinates"
|
||||
updates = "Checking for updates"
|
||||
@@ -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"
|
||||
|
||||
+2
-1
@@ -12,8 +12,9 @@ def read_json(path: Path) -> Any:
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
@@ -360,12 +360,14 @@ thanks = Thanks(
|
||||
"bongjutsu",
|
||||
"Deku",
|
||||
"dj3hac",
|
||||
"finntux",
|
||||
"GaryBlackbourne",
|
||||
"jiriks74",
|
||||
"Johnofwrong",
|
||||
"MatheusLasserr",
|
||||
"nolan-perez",
|
||||
"scandalouss",
|
||||
"SnackSBR",
|
||||
"StevelDusa",
|
||||
"Thoughtduck216",
|
||||
],
|
||||
@@ -609,6 +611,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"
|
||||
|
||||
+10
-7
@@ -43,11 +43,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)
|
||||
+9
-5
@@ -323,8 +323,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)
|
||||
@@ -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
|
||||
@@ -70,18 +70,26 @@ class LargeIconTextButton(IconButton):
|
||||
|
||||
|
||||
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 +102,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 +120,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 +142,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
|
||||
|
||||
@@ -6,7 +6,7 @@ from dzgui.strings import connect_panel
|
||||
from dzgui.util.keys import is_ctrl_mask
|
||||
from dzgui.views.components.buttons import (
|
||||
AddButton,
|
||||
ClipboardButton,
|
||||
CopyIpButton,
|
||||
SteamConnectButton,
|
||||
)
|
||||
from dzgui.views.components.entry import IpEntry, PortEntry
|
||||
@@ -151,7 +151,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)
|
||||
|
||||
|
||||
@@ -68,6 +68,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 +331,8 @@ 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:
|
||||
for check in self.button_grid.checks:
|
||||
label = check.get_label()
|
||||
check.set_active(filters[label])
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -111,9 +111,9 @@ class Statusbar(Gtk.Grid):
|
||||
context: Union["ServerTab", NotebookPage],
|
||||
) -> None:
|
||||
self.spinner.stop()
|
||||
# TODO: CalcDist is being called when table is not loaded
|
||||
# FIXME: CalcDist is being called when table is not loaded
|
||||
if dist is None:
|
||||
self.set_by_context(context, "")
|
||||
return
|
||||
else:
|
||||
pretty = self.append_distance(dist)
|
||||
self.set_by_context(context, pretty)
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import sys
|
||||
from typing import Literal, TYPE_CHECKING
|
||||
|
||||
# import time
|
||||
from enum import Enum
|
||||
from typing import Any, Self
|
||||
|
||||
# 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.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
|
||||
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)
|
||||
|
||||
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(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),
|
||||
(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 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
|
||||
# TODO: generic exception wrapper
|
||||
self.exception = f"{type(e).__name__}: {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, "Running", True, 0))
|
||||
|
||||
def update_status(self, state: Success) -> None:
|
||||
d = {Success.OK: "OK", Success.FAIL: "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] == "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 == "OK":
|
||||
cell.set_property(prop, HEX_GREEN)
|
||||
elif state == "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()
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@ class ModsMixin:
|
||||
self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn
|
||||
) -> None:
|
||||
|
||||
mod = self.get_value_at_index(2)
|
||||
mod = self.get_value_at_index(2) # type: ignore
|
||||
self.controller.open_workshop_page(mod) # type: ignore
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Self, TYPE_CHECKING
|
||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||
from dzgui.views.trees.tree_mods import ModTreeView
|
||||
|
||||
import gi
|
||||
@@ -10,7 +11,8 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
|
||||
class Mods(Gtk.ScrolledWindow):
|
||||
|
||||
class Mods(NoOverlayScrolledWindow):
|
||||
def __init__(self, controller: "Controller") -> None:
|
||||
super().__init__()
|
||||
|
||||
|
||||
@@ -106,6 +106,9 @@ 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)
|
||||
self.connect_last = Gtk.Button(
|
||||
label=preconnect.connect_last, halign=Gtk.Align.END, tooltip_text=preconnect.connect_last_tooltip
|
||||
)
|
||||
|
||||
# TODO: abstract
|
||||
self.raise_window = Gtk.CheckButton(
|
||||
@@ -126,13 +129,14 @@ 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.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")
|
||||
@@ -213,6 +217,9 @@ 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(self.raise_window.get_active())
|
||||
|
||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.update_and_connect(self.raise_window.get_active())
|
||||
|
||||
@@ -308,6 +315,11 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
prefix = preconnect.total_mods
|
||||
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
||||
|
||||
if prereqs.is_last_server:
|
||||
self.connect_last.show()
|
||||
else:
|
||||
self.connect_last.hide()
|
||||
|
||||
self._process_warnings(prereqs)
|
||||
if self.tree.is_visible():
|
||||
self.tree.grab_focus()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -88,13 +88,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 +102,7 @@ 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
||||
authors = [
|
||||
{name = "aclist"}
|
||||
]
|
||||
version = "7.0.0b3"
|
||||
version = "7.0.0b8"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
+71
-16
@@ -1,34 +1,50 @@
|
||||
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")
|
||||
cpython = root.joinpath("build/cpython/airgapped.tar.gz")
|
||||
|
||||
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_PASS_LOCATION"] = "true"
|
||||
# env["PYAPP_PYTHON_VERSION"] = "3.13"
|
||||
|
||||
# NOTE: explicitly install all dependencies into distribution
|
||||
env["PYAPP_SKIP_INSTALL"] = "true"
|
||||
@@ -37,12 +53,51 @@ env["PYAPP_FULL_ISOLATION"] = "true"
|
||||
env["PYAPP_DISTRIBUTION_PATH"] = str(cpython)
|
||||
env["PYAPP_DISTRIBUTION_PYTHON_PATH"] = "python/bin/python3"
|
||||
|
||||
proc = subprocess.run(["cargo", "build", "--release"], env=env, cwd=pyapp_dir)
|
||||
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)
|
||||
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-dir",
|
||||
str(dist_dir),
|
||||
"--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 = dist_dir.joinpath("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()
|
||||
Vendored
+14
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"cols": {
|
||||
"Name": 398,
|
||||
"Map": 181,
|
||||
"Gametime": 151,
|
||||
"Players": 119,
|
||||
"Queue": 96,
|
||||
"IP": 247,
|
||||
"Qport": 94,
|
||||
"Ping": 253
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"cols": {
|
||||
"Name": 1,
|
||||
"Map": 1,
|
||||
"Gametime": 1,
|
||||
"Players": 1,
|
||||
"Queue": 1,
|
||||
"IP": 1,
|
||||
"Qport": 1,
|
||||
"Ping": 1,
|
||||
"View": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from dzgui.init.migrate import convert_cols_file
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
@pytest.fixture
|
||||
def columns_with_perspective():
|
||||
return fixture_path("columns_1")
|
||||
|
||||
@pytest.fixture
|
||||
def columns_without_perspective():
|
||||
return fixture_path("columns_2")
|
||||
|
||||
@pytest.fixture
|
||||
def columns_with_view():
|
||||
return fixture_path("columns_3")
|
||||
|
||||
@pytest.mark.config
|
||||
def test_columns_with_perspective(columns_with_perspective):
|
||||
j = convert_cols_file(columns_with_perspective)
|
||||
assert "View" in j["cols"]
|
||||
assert "Max" in j["cols"]
|
||||
|
||||
@pytest.mark.config
|
||||
def test_columns_without_perspective(columns_without_perspective):
|
||||
j = convert_cols_file(columns_without_perspective)
|
||||
assert "View" not in j["cols"]
|
||||
assert "Max" not in j["cols"]
|
||||
|
||||
@pytest.mark.config
|
||||
def test_columns_with_view(columns_with_view):
|
||||
j = convert_cols_file(columns_with_view)
|
||||
assert j is None
|
||||
@@ -0,0 +1,27 @@
|
||||
from dzgui.config.ipdb import get_ipdb
|
||||
from pathlib import Path
|
||||
import os
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_stripping():
|
||||
d = tempfile.TemporaryDirectory()
|
||||
path = Path(d.name).joinpath("ips.csv")
|
||||
get_ipdb(path)
|
||||
with open(path, "rb") as f:
|
||||
first = f.readline().decode().rstrip()
|
||||
try:
|
||||
f.seek(-2, os.SEEK_END)
|
||||
while f.read(1) != b"\n":
|
||||
f.seek(-2, os.SEEK_CUR)
|
||||
except OSError:
|
||||
f.seek(0)
|
||||
last = f.read().decode().rstrip()
|
||||
|
||||
first_els = first.split(",")
|
||||
last_els = last.split(",")
|
||||
|
||||
assert first_els[0] == "0.0.0.0"
|
||||
assert last_els[0] == "224.0.0.0"
|
||||
@@ -2,14 +2,14 @@ from dzgui.util import localize
|
||||
|
||||
|
||||
def test_de(monkeypatch):
|
||||
monkeypatch.setenv("LC_CTYPE", "de_DE.UTF-8")
|
||||
monkeypatch.setenv("LC_ALL", "de_DE.UTF-8")
|
||||
localize.set_locale()
|
||||
assert localize.number(9900) == "9.900"
|
||||
assert localize.number(99.88) == "99,88"
|
||||
|
||||
|
||||
def test_en(monkeypatch):
|
||||
monkeypatch.setenv("LC_CTYPE", "en_US.UTF-8")
|
||||
monkeypatch.setenv("LC_ALL", "en_US.UTF-8")
|
||||
localize.set_locale()
|
||||
assert localize.number(9900) == "9,900"
|
||||
assert localize.number(99.88) == "99.88"
|
||||
@@ -22,7 +22,7 @@ def test_none(monkeypatch):
|
||||
|
||||
|
||||
def test_empty(monkeypatch):
|
||||
monkeypatch.setenv("LC_CTYPE", "")
|
||||
monkeypatch.setenv("LC_ALL", "")
|
||||
localize.set_locale()
|
||||
assert localize.number(9900) == "9,900"
|
||||
assert localize.number(99.88) == "99.88"
|
||||
Reference in new issue
Block a user