Compare commits

...
18 Commits
Author SHA1 Message Date
aclist 7f5559121d Merge pull request #397 from aclist/aclist-patch-2
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: markdown syntax
2026-07-07 16:51:44 +09:00
aclist 019ea82004 Update CONTRIBUTING.md 2026-07-07 16:51:14 +09:00
aclist df291c867c Merge pull request #396 from aclist/aclist-patch-1
fix: typos in CONTRIBUTING file
2026-07-07 16:48:40 +09:00
aclist 9db3ac1f80 Update CONTRIBUTING.md 2026-07-07 16:48:12 +09:00
aclist bdb7da9440 Update CONTRIBUTING.md 2026-07-07 16:46:54 +09:00
aclist cc623914da Merge pull request #334 from aclist/chore/update-actions-dzgui
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: update checkout action to v5
2026-06-05 18:59:03 +09:00
aclist fa9c58f1e2 chore: update checkout action to v5 2026-06-05 18:57:40 +09:00
aclist d7ba49ea97 Merge pull request #286 from aclist/prerelease/6.0.5
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: update symlink hash logic
2026-05-17 23:42:15 +09:00
aclist bab2379fad chore: update symlink hash logic 2026-05-17 23:28:05 +09:00
aclist 98b4f37e54 Merge pull request #283 from aclist/prerelease/6.0.4
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: update module checksums
2026-04-13 16:50:05 +09:00
aclist 8ca8dbb6ff fix: update module checksums 2026-04-13 16:45:50 +09:00
aclist 7d6e089c03 Merge pull request #282 from aclist/prerelease/6.0.3
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: use GLibUnix namespace
2026-04-13 06:05:14 +09:00
aclist 3c75b9362b fix: use GLibUnix namespace 2026-04-13 04:47:33 +09:00
aclist dc24ef6e85 Merge pull request #275 from aclist/chore/update-readme
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: update readme
2026-03-30 20:25:14 +09:00
aclist f76985c79d chore: reword readme 2026-03-30 20:14:45 +09:00
aclist 3d3cd19892 chore: update readme 2026-03-30 20:13:20 +09:00
aclist c14fb10d23 Merge pull request #270 from aclist/prerelease/6.0.2
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: dialogs in thread
2026-01-26 18:25:52 +09:00
aclist a8417d27d7 fix: dialogs in thread 2026-01-26 17:00:55 +09:00
9 changed files with 324 additions and 344 deletions

No files matched your search

+1 -1
View File
@@ -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
+3
View File
@@ -1 +1,4 @@
tags
__pycache__
pre-commit
tests/
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## [6.0.5] 2026-05-17
## Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.4] 2026-04-13
## Fixed
- Update A2S module checksums to support servers with missing description field
## [6.0.3] 2026-04-12
## Fixed
- Use GLibUnix namespace where available when setting up SIGINT callback
## [6.0.2] 2026-01-26
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
- Raise error correctly from module
- Do not spawn dialogs from inside of thread when fetching prereqs
## Changed
- Optimize time complexity of startup checks
- Optimize coordinate calculation by using local records first
- Rename some internal functions
- Wrap entire startup process in dialog
## [6.0.1] 2026-01-16
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
+3 -3
View File
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
# Requesting help
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
(issue tracker)[https://github.com/aclist/dzgui-devel/issues] under the
[issue tracker](https://github.com/aclist/dztui/issues) under the
`troubleshooting` tag.
# How can I help the project?
@@ -24,7 +24,7 @@ accordingly.
## Submitting a ticket
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dzgui-devel/issues].
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
From there, follow the onscreen prompts. You will be asked questions such as:
- What version are you using?
@@ -67,5 +67,5 @@ will be merged to `prerelease/<latest-version>`, then `testing`, and after a liv
testing phase, to `stable` as well.
It is recommended to follow
(Conventional Commits)[https://www.conventionalcommits.org/en/v1.0.0/], as this
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), as this
integrates well with tooling and helps the project review your code.
+9 -1
View File
@@ -11,6 +11,15 @@ This overcomes certain limitations in the Linux client and helps prepare the gam
- Concatenate launch options to pass to Steam
- Connect to mod-enabled LAN servers
## Design philosophy
DZGUI follows a specific set of guidelines, enumerated below:
- Provide a free and open-source tool under GPL
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
## Setup and usage
Refer to the documentation for installation and setup instructions:
@@ -18,7 +27,6 @@ Refer to the documentation for installation and setup instructions:
- [GitHub](https://aclist.github.io/dzgui/index.html)
- [Mirror (Codeberg)](https://aclist.codeberg.page)
![A screenshot of DZGUI](/images/example.png)
## Attribution
+120 -71
View File
@@ -2,7 +2,7 @@
set -o pipefail
src_path="$(readlink -e "$0")"
version=6.0.1
version=6.0.5
#CONSTANTS
aid=221100
@@ -142,7 +142,7 @@ test_gobject(){
python3.13 -c "import gi"
if [[ ! $? -eq 0 ]]; then
logger CRITICAL "Missing PyGObject"
quit_with_pdialog "Requires PyGObject (python-gobject)"
quit_with_help_dialog "Requires PyGObject (python-gobject)"
exit 1
fi
logger INFO "Found PyGObject in Python env"
@@ -247,7 +247,7 @@ open_url(){
xdg-open "$url"
fi
}
quit_with_pdialog(){
quit_with_help_dialog(){
local sel
msg="$1"
help_button="Open help page"
@@ -260,7 +260,7 @@ quit_with_pdialog(){
check_pyver(){
if [[ ! $(python3.13 --version) ]]; then
local msg="Requires Python 3.13"
quit_with_pdialog "$msg"
quit_with_help_dialog "$msg"
fi
}
watcher_deps(){
@@ -335,6 +335,7 @@ check_unmerged(){
fi
}
check_version(){
echo "# Checking version"
[[ -n $reference_branch ]] && return
local version_url=$(format_version_url)
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
@@ -414,6 +415,7 @@ test_display_mode(){
fi
}
check_architecture(){
echo "# Checking system architecture"
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
read -a APU_MODEL <<< "$cpu"
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
@@ -430,6 +432,7 @@ check_architecture(){
logger INFO "Setting architecture to 'Steam Deck'"
}
check_map_count(){
echo "# Checking map count"
[[ $is_steam_deck -gt 0 ]] && return 0
local map_count_file="/proc/sys/vm/max_map_count"
local min_count=1048576
@@ -479,6 +482,7 @@ tdialog(){
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
}
steam_deps(){
echo "# Checking Steam"
local flatpak
local steam
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
@@ -489,16 +493,8 @@ steam_deps(){
exit 1
fi
}
migrate_files(){
if [[ ! -f $config_path/dztuirc.oldapi ]]; then
cp $config_file $config_path/dztuirc.oldapi
logger INFO "Migrated old API file"
fi
[[ ! -f $hist_file ]] && return
rm $hist_file
logger INFO "Wiped old history file"
}
stale_symlinks(){
echo "# Cleaning stale symlinks"
local game_dir="$steam_path/steamapps/common/DayZ"
readarray -t links < <(find "$game_dir" -xtype l)
for link in "${links[@]}"; do
@@ -512,7 +508,7 @@ check_availability() {
return 1
fi
local url=$1
local timeout_sec="3"
local timeout_sec="2"
if [[ $2 ]]; then
timeout_sec=$2
fi
@@ -523,6 +519,7 @@ check_availability() {
}
local_latlon(){
echo "# Checking coordinates"
if [[ -z $(command -v dig) ]]; then
local url_ipecho="https://ipecho.net/plain"
if ! check_availability "ipecho.net"; then
@@ -534,6 +531,9 @@ local_latlon(){
# TODO : implement checking remote
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
fi
if calc_local_coords "$local_ip"; then
return 0
fi
local url_ip_api="http://ip-api.com/json/$local_ip"
if ! check_availability "ip-api.com"; then
logger WARN "Failed to get local coordinates, ip-api.com service may be down."
@@ -548,6 +548,7 @@ local_latlon(){
}
lock(){
echo "# Setting up lock file"
[[ ! -f $lock_file ]] && touch $lock_file
local pid=$(cat $lock_file)
ps -p $pid -o pid= >/dev/null 2>&1
@@ -566,9 +567,12 @@ get_hash(){
md5sum "$1" | awk '{print $1}'
}
fetch_a2s(){
# this file is currently monolithic
[[ -d $helpers_path/a2s ]] && { logger INFO "A2S helper is current"; return 0; }
local sha=c7590ffa9a6d0c6912e17ceeab15b832a1090640
sum="d26da2fe03df2b95436f823f0496887b"
if [[ -d $helpers_path/a2s ]] && [[ $(get_hash "$helpers_path/a2s/info.py") == $sum ]]; then
logger INFO "A2S helper is current"
return 0
fi
local sha=b40eb24cdbb06ebd08272f224257fe5a81610e86
local author="yepoleb"
local repo="python-a2s"
local url="https://github.com/$author/$repo/tarball/$sha"
@@ -582,13 +586,13 @@ fetch_a2s(){
logger INFO "Updated A2S helper to sha '$sha'"
}
fetch_dzq(){
local sum="0a334e1e144e76e560419d155435c91e"
local sum="0d316badbb8d943c5f078e73862cac3e"
local file="$helpers_path/a2s/dayzquery.py"
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
logger INFO "DZQ is current"
return 0
fi
local sha=a22a9f428cbe075d7dda62f78000296955eea92a
local sha="07483b88ed096327ebca752f5e177011b604d7fe"
local author="yepoleb"
local repo="dayzquery"
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
@@ -625,12 +629,12 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file"
declare -A sums
sums=(
["funcs"]="5e35a9812c03fc64d75843a23d024f0d"
["funcs"]="cff21e5ecfbbf72951f9f772b46024c9"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["servers.py"]="ed442c3aecf33f777d59dcf53650d263"
["ui.py"]="3d67e5e8e85a23dde1fd0e85a9be62a9"
["ui.py"]="cf52a3d5883afe02a013d8673df494fa"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["pefile.py"]="21531f2c0d9dfa5f110cf6779f9d22c0"
["pefile.py"]="b452974a84bff1d821872fcebf59e380"
)
local author="aclist"
local repo="dztui"
@@ -690,12 +694,12 @@ fetch_km_helper(){
}
get_response_code(){
local url="$1"
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
curl --connect-timeout 3 -Ls -I -o /dev/null -w "%{http_code}" "$url"
}
fetch_ip_db(){
parse_dl_url(){
curl -Ls "$url" \
curl --connect-timeout 3 -Ls "$url" \
| grep "csv.gz" \
| awk -F"['']" '{print $2}'
}
@@ -711,6 +715,9 @@ fetch_ip_db(){
fetch(){
logger INFO "Triggering fetch routine"
local url="$1"
local ip_file="${state_path}/ips.csv"
local base_file="${state_path}/${this_month}.csv.gz"
local extracted_file="${state_path}/${this_month}.csv"
curl -Ls "$url" > "$base_file"
if [[ $? -ne 0 ]]; then
@@ -730,7 +737,7 @@ fetch_ip_db(){
logger WARN "Abnormal exit while parsing IPs"
return
fi
rm "${state_path}/${month}.csv"
rm "${state_path}/${this_month}.csv"
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}')
if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
@@ -745,55 +752,57 @@ fetch_ip_db(){
rm "$ip_file"
fi
echo "$month" > "$month_file"
logger INFO "Wrote '$month' to stub '$month_file'"
echo "$this_month" > "$month_file"
logger INFO "Wrote '$this_month' to stub '$month_file'"
logger INFO "Updated '$ip_file'"
}
check_remote(){
local url="$1"
local res=$(get_response_code "$url")
if [[ $res -ne 200 ]]; then
logger WARN "Failed to retrieve remote resource: '$url' ($res)"
return
fi
logger INFO "Resolved remote URL: '$url'"
# test dl url
local dl_url="$(parse_dl_url)"
local res=$(get_response_code "$dl_url")
if [[ $res -ne 200 ]]; then
logger WARN "Remote resource unavailable: '$dl_url' ($res)"
return
fi
logger INFO "Resolved download URL: '$dl_url'"
fetch "$dl_url"
}
local url="https://db-ip.com/db/download/ip-to-city-lite"
local month_file="${state_path}/.month"
local ip_file="${state_path}/ips.csv"
# test main url
local res=$(get_response_code "$url")
if [[ $res -ne 200 ]]; then
logger WARN "Failed to retrieve remote resource: '$url' ($res)"
return
fi
logger INFO "Resolved remote URL: '$url'"
# test dl url
local dl_url="$(parse_dl_url)"
local res=$(get_response_code "$dl_url")
if [[ $res -ne 200 ]]; then
logger WARN "Remote resource unavailable: '$dl_url' ($res)"
return
fi
logger INFO "Resolved download URL: '$dl_url'"
local month=$(parse_dl_url_date "$dl_url")
local base_file="${state_path}/${month}.csv.gz"
local extracted_file="${state_path}/${month}.csv"
month_file="${state_path}/.month"
this_month=$(date +%Y-%m)
# no stub file
if [[ ! -f $month_file ]]; then
logger WARN "No stub file '$month_file' present"
fetch "$dl_url"
return
fi
# local needs update
local last_month=$(< "$month_file")
if [[ $last_month != "$month" ]]; then
logger WARN "Local stub '$last_month' does not match remote stub '$month'"
fetch "$dl_url"
check_remote "$url"
return
fi
# if stub is same date, abort
logger INFO "Local stub '$last_month' is identical to remote, skipping"
local last_month=$(< "$month_file")
if [[ $last_month == "$this_month" ]]; then
logger INFO "Local stub '$last_month' is identical to remote, skipping"
return
fi
# local needs update
logger WARN "Local stub '$last_month' does not match '$this_month'"
check_remote "$url"
return
}
fetch_helpers(){
echo "# Checking helper files"
fetch_a2s
fetch_dzq
fetch_km_helper
@@ -981,6 +990,7 @@ create_config(){
done
}
varcheck(){
echo "# Checking config file"
local msg="Config file '$config_file' missing. Start first-time setup now?"
local msg2="The Steam paths set in your config file appear to be invalid (DayZ was moved or uninstalled). Restart first-time setup now?"
if [[ ! -f $config_file ]]; then
@@ -1007,12 +1017,14 @@ varcheck(){
fi
}
is_dzg_downloading(){
echo "# Checking DayZ"
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
logger WARN "DayZ may be scheduling updates"
return 0
fi
}
is_steam_running(){
echo "# Checking Steam"
local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
if [[ -z $res ]]; then
logger WARN "Steam may not be running"
@@ -1025,6 +1037,7 @@ get_response_code(){
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
}
test_connection(){
echo "# Testing connection"
declare -A hr
local res1
local res2
@@ -1058,6 +1071,7 @@ legacy_cols(){
mv $cols_file.new $cols_file
}
stale_mod_signatures(){
echo "# Checking stale mods"
[[ ! -f "$versions_file" ]] && return
local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
if [[ -d $workshop_dir ]]; then
@@ -1071,18 +1085,48 @@ stale_mod_signatures(){
}
create_new_links(){
echo "# Setting up symlinks"
"$func_helper" "update_symlinks"
}
calc_local_coords(){
local ip="$1"
[[ ! -f "$geo_helper" ]] && return 1
IFS="." read -ra split <<< "$ip"
[[ ${#split[@]} -ne 4 ]] && return 1
prefix="^${split[0]}.${split[1]}."
readarray -t res < <(grep "$prefix" "$geo_helper")
for address in "${res[@]}"; do
IFS="," read -ra fields <<< "$address"
upper="${fields[1]}"
IFS="." read -ra upper_digits <<< "$upper"
if [[ ${split[2]} -gt ${upper_digits[2]} ]]; then
continue
fi
if [[ ${split[2]} -eq ${upper_digits[2]} ]]; then
if [[ ${split[3]} -gt ${upper_digits[3]} ]]; then
continue
fi
if [[ ${split[3]} -eq ${upper_digits[3]} ]]; then
echo "${fields[-2]}" >> "$coords_file"
echo "${fields[-1]}" >> "$coords_file"
return 0
fi
fi
if [[ ${split[2]} -lt ${upper_digits[2]} ]]; then
echo "${fields[-2]}" >> "$coords_file"
echo "${fields[-1]}" >> "$coords_file"
return 0
fi
done
return 1
}
initial_setup(){
setup_dirs
setup_state_files
depcheck
check_pyver
test_gobject
watcher_deps
check_architecture
test_connection
fetch_helpers > >(pdialog "Checking helper files")
fetch_helpers
varcheck
source "$config_file"
lock
@@ -1091,14 +1135,12 @@ initial_setup(){
check_version
check_map_count
steam_deps
migrate_files
stale_symlinks
stale_mod_signatures
create_new_links
local_latlon
is_steam_running
is_dzg_downloading
print_config_vals
}
uninstall(){
_full(){
@@ -1200,8 +1242,15 @@ main(){
set_im_module
printf "Initializing setup...\n"
initial_setup
printf "Checking dependencies...\n"
setup_dirs
setup_state_files
depcheck
check_pyver
test_gobject
watcher_deps
initial_setup > >(pdialog "Initializing setup")
printf "All OK. Kicking off UI...\n"
python3.13 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
+12 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -o pipefail
version="6.0.1"
version="6.0.5"
#CONSTANTS
aid=221100
@@ -929,13 +929,19 @@ query_defunct(){
<<< "$result" awk '{print $2}'
}
encode(){
echo "$1" | md5sum | cut -c -8
echo -n "$1" | md5sum | cut -c -8
}
compare(){
local modlist="$@"
diff=$(comm -23 <(<<< "$modlist" sort -u) <(installed_mods | sort))
echo "$diff"
}
unlink_all(){
readarray -t links < <(find "$game_dir" -maxdepth 1 -mindepth 1 -type l)
for link in "${links[@]}"; do
unlink "$link"
done
}
legacy_symlinks(){
logger INFO "Removing legacy symlinks"
@@ -1026,7 +1032,10 @@ update_symlinks(){
if [[ ! -d "$workshop_dir" ]]; then
return
fi
legacy_symlinks
if [[ ! -d "$game_dir" ]]; then
return
fi
unlink_all
symlinks
}
try_fallback(){
+1 -1
View File
@@ -295,7 +295,7 @@ def get_dayz_version(file: Path) -> DayZVersion | Exception:
try:
version = get_version(file)
except Exception as e:
return e
raise e
patch = str(version.build) + str(version.revision)
dz_vers = DayZVersion(version.major, version.minor, int(patch))
return dz_vers
+151 -264
View File
@@ -19,7 +19,7 @@ from collections.abc import Callable
from concurrent.futures import wait
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Literal, Self, Any
from typing import Literal, Self, Any, TYPE_CHECKING
import servers as Servers # noqa E402
import pefile as PeFile # noqa E402
@@ -32,12 +32,16 @@ from pefile import (
)
from pefile import VersionMatch
if TYPE_CHECKING:
from servers import Prereqs
locale.setlocale(locale.LC_ALL, "")
import gi # noqa E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
gi.require_version("GLibUnix", "2.0")
from gi.repository import Gtk, GLib, GLibUnix, Gdk, GObject, Pango # noqa E402
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
@@ -850,9 +854,7 @@ def call_out(command: str, *args: str) -> subprocess.CompletedProcess:
arg_ar = []
for i in args:
arg_ar.append(i)
logger.info(
f"Context '{name}' calling subprocess '{command}' with args '{arg_ar}'"
)
logger.info(f"Context '{name}' calling subprocess '{command}' with args '{arg_ar}'")
proc = subprocess.run(
["/usr/bin/env", "bash", funcs, command] + arg_ar,
capture_output=True,
@@ -878,9 +880,7 @@ def spawn_dialog(msg: str, mode: Popup) -> bool:
return False
def process_shell_return_code(
msg: str, code: int, original_input: RowType
) -> None:
def process_shell_return_code(msg: str, code: int, original_input: RowType) -> None:
logger.info(
f"Processing return code '{code}' for the input "
f"'{original_input}', returned message '{msg}'"
@@ -907,9 +907,7 @@ def process_shell_return_code(
spawn_dialog("Invalid Steam API key, reverting", Popup.NOTIFY)
App.notebook.settings.revert(Preferences.STEAM)
case 79: # failed settings update (bm)
spawn_dialog(
"Invalid Battlemetrics API key, reverting", Popup.NOTIFY
)
spawn_dialog("Invalid Battlemetrics API key, reverting", Popup.NOTIFY)
App.notebook.settings.revert(Preferences.BM)
case 80: # pop toast after successful settings change
config_vals.clear()
@@ -975,9 +973,7 @@ def call_on_thread(
if state:
wait_dialog = GenericDialog(msg, Popup.WAIT)
wait_dialog.show_all()
thread = threading.Thread(
target=_background, args=(subproc, args, wait_dialog)
)
thread = threading.Thread(target=_background, args=(subproc, args, wait_dialog))
thread.start()
else:
"""
@@ -1002,18 +998,14 @@ def process_tree_option(choice: RowType) -> None:
record = App.treeview.get_record_string()
wait_msg = command.dict["wait_msg"]
show_wait_dialog = True
call_on_thread(
show_wait_dialog, cmd_string, wait_msg, record, choice=choice
)
call_on_thread(show_wait_dialog, cmd_string, wait_msg, record, choice=choice)
return
# modlist highlight stale action
if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT:
wait_msg = command.dict["wait_msg"]
show_wait_dialog = True
call_on_thread(
show_wait_dialog, cmd_string, wait_msg, "", choice=choice
)
call_on_thread(show_wait_dialog, cmd_string, wait_msg, "", choice=choice)
return
if command == RowType.CHANGELOG:
@@ -1027,13 +1019,7 @@ def process_tree_option(choice: RowType) -> None:
return
record = str_to_record(record)
thread_new_with_dialog(
App.treeview.prepare_connection,
parse_shell_output,
"Querying server",
command,
[record],
)
prepare_connection(command, record)
return
match command.dict["type"]:
@@ -1061,48 +1047,50 @@ def parse_shell_output(proc: subprocess.CompletedProcess, row: RowType):
process_shell_return_code(msg, proc.returncode, row)
def thread_new_with_dialog(
func: Callable,
callback: Callable | None,
msg: str,
row: RowType | None,
args: list,
) -> None:
"""
Pop a GenericDialog transient to App.treeview and
call a function on a thread, with optional callback.
Chiefly used for connection-related subprocesses.
def prepare_connection(rowtype: RowType, record: Record) -> None:
def background(rowtype: RowType, record: Record) -> None:
def cleanup() -> None:
App.treeview.wait_dialog.destroy()
# NOTE: When using RowType.CONN_BY_IP, the gameport needs to be interpolated
record.gameport = prereqs.gameport
addr = record_to_str(record)
After completion, the dialog is destroyed in the main event loop
and additional exception handling occurs.
if proceed is False:
spawn_dialog(msg, Popup.NOTIFY)
return
if msg != "":
res = spawn_dialog(msg, Popup.CONFIRM)
if res is False:
try_connect(addr, str(prereqs.appid), str(pefile_path), rowtype)
else:
try_connect(addr, str(prereqs.appid), str(pefile_path), rowtype)
This is intended as a bridge between legacy shell methods and the UI.
A more abstracted version of call_on_thread() for when extra threaded
processing occurs before calls to shell subprocesses.
"""
proceed, msg, pefile_path, prereqs = App.treeview.get_prereqs(record)
GLib.idle_add(cleanup)
def background(*args):
msg = "Checking prerequisites"
App.treeview.dialog_show(msg)
thread = threading.Thread(target=background, args=(rowtype, record))
thread.start()
def try_connect(addr: str, appid: str, path: str, row: RowType) -> None:
def background(addr, appid, path):
def cleanup():
App.treeview.dialog_hide()
if exception is not None:
spawn_dialog(str(exception), Popup.NOTIFY)
process_user_input(row)
return
if callback is not None and proc is not None:
callback(proc, row)
parse_shell_output(proc, row)
exception = None
proc = None
try:
proc = func(*args)
proc = call_out("try_connect", addr, appid, path)
except Exception as e:
exception = e
logger.critical(e)
GLib.idle_add(cleanup)
return
GLib.idle_add(cleanup)
msg = "Querying server"
App.treeview.dialog_show(msg)
thread = threading.Thread(target=background, args=(args))
thread = threading.Thread(target=background, args=(addr, appid, path))
thread.start()
@@ -1142,27 +1130,32 @@ def record_to_str(record: Record) -> str:
def connect_by_ip(enum: RowType, response: str) -> None:
def _prep(response: str) -> None:
try:
record = Servers.validate_ip(response)
proc = App.treeview.prepare_connection(record)
return proc
thread_new_with_dialog(
_prep, parse_shell_output, "Querying IP", enum, [response]
)
return
except Exception as e:
spawn_dialog(str(e), Popup.NOTIFY)
return
prepare_connection(enum, record)
def connect_by_id(enum: RowType, response: str, key: str) -> None:
def _prep(key: str, response: str) -> None:
record = Servers.query_bm_api(key, response)
proc = App.treeview.prepare_connection(record)
return proc
def cleanup() -> None:
App.treeview.dialog_hide()
prepare_connection(enum, record)
thread_new_with_dialog(
_prep, parse_shell_output, "Querying API", enum, [key, response]
)
return
try:
record = Servers.query_bm_api(key, response)
except Exception as e:
spawn_dialog(str(e), Popup.NOTIFY)
App.treeview.dialog_hide()
return
GLib.idle_add(cleanup)
msg = "Validating ID"
App.treeview.dialog_show(msg)
thread = threading.Thread(target=_prep, args=(key, response))
thread.start()
def process_user_input(enum: RowType) -> None:
@@ -1173,9 +1166,7 @@ def process_user_input(enum: RowType) -> None:
if enum == RowType.CONN_BY_ID:
key = query_config("api_key")[0]
if len(key) == 0:
spawn_dialog(
"No Battlemetrics API key is set; see Options", Popup.NOTIFY
)
spawn_dialog("No Battlemetrics API key is set; see Options", Popup.NOTIFY)
return
user_entry = EntryDialog(prompt, Popup.ENTRY, link_label)
@@ -1196,9 +1187,7 @@ def process_user_input(enum: RowType) -> None:
show_wait_dialog = True
wait_msg = "Working"
call_on_thread(
show_wait_dialog, cmd_string, wait_msg, response, choice=enum
)
call_on_thread(show_wait_dialog, cmd_string, wait_msg, response, choice=enum)
return
@@ -1286,9 +1275,7 @@ class OuterWindow(Gtk.Window):
logger.info(f"Using default window size {w},{h}")
self.set_default_size(w, h)
def _on_delete_event(
self, window: "OuterWindow", event: Gdk.EventKey
) -> None:
def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
self.halt_proc_and_quit()
def halt_proc_and_quit(self) -> None:
@@ -1355,8 +1342,7 @@ class RightPanel(Gtk.Box):
self.pack_start(self.filters_vbox, False, False, 0)
debug_tooltip = (
"Used to perform a dry run without\n"
"actually connecting to a server"
"Used to perform a dry run without\n" "actually connecting to a server"
)
ping_tooltip = (
"Refresh the ping for visible servers.\n"
@@ -1425,8 +1411,7 @@ class RightPanel(Gtk.Box):
rows = ModelManager.get_filtered()
with ThreadPoolExecutor(100) as executor:
futures = [
executor.submit(Servers.ping, i, row)
for i, row in enumerate(rows)
executor.submit(Servers.ping, i, row) for i, row in enumerate(rows)
]
wait(futures)
for future in futures:
@@ -1802,9 +1787,7 @@ class ModelManagerSingleton:
class TreeView(Gtk.TreeView):
__gsignals__ = {
"on_distcalc_started": (GObject.SignalFlags.RUN_FIRST, None, ())
}
__gsignals__ = {"on_distcalc_started": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self):
super().__init__()
@@ -1923,9 +1906,7 @@ class TreeView(Gtk.TreeView):
if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate()
def _delete_note(
self, button: Gtk.Button, user_entry: Gtk.Box, addr: str
) -> None:
def _delete_note(self, button: Gtk.Button, user_entry: Gtk.Box, addr: str) -> None:
box = button.get_parent()
dialog = box.get_parent()
try:
@@ -2017,9 +1998,7 @@ class TreeView(Gtk.TreeView):
model.remove(it)
block_signals()
thread = threading.Thread(
target=ModelManager.resync_model, args=(addr, qport)
)
thread = threading.Thread(target=ModelManager.resync_model, args=(addr, qport))
thread.start()
def remove_from_history(self) -> None:
@@ -2043,16 +2022,12 @@ class TreeView(Gtk.TreeView):
if hasattr(TreeView, menu_item.action):
func = getattr(TreeView, menu_item.action)
msg = (
f"User clicked context menu '{menu_item.get_label()}', "
f"calls {func}"
f"User clicked context menu '{menu_item.get_label()}', " f"calls {func}"
)
logger.info(msg)
func(self)
else:
msg = (
f"Context menu function for '{menu_item.action}' "
f"does not exist"
)
msg = f"Context menu function for '{menu_item.action}' " f"does not exist"
u_msg = (
f"Something went wrong when accessing the method "
f"'{menu_item.action}'"
@@ -2093,9 +2068,7 @@ class TreeView(Gtk.TreeView):
return True
return False
def _on_button_release(
self, widget: Gtk.Widget, event: Gdk.EventButton
) -> None:
def _on_button_release(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None:
if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3:
return
@@ -2179,9 +2152,7 @@ class TreeView(Gtk.TreeView):
if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l:
if self.is_selection_empty():
return
self.menu.popup_at_widget(
widget, Gdk.Gravity.CENTER, Gdk.Gravity.WEST
)
self.menu.popup_at_widget(widget, Gdk.Gravity.CENTER, Gdk.Gravity.WEST)
else:
self.menu.popup_at_pointer(event)
self.menu.select_first(False)
@@ -2195,9 +2166,7 @@ class TreeView(Gtk.TreeView):
return None
call_out("start_cooldown", "", "")
thread = threading.Thread(
target=self._background_player_count, args=()
)
thread = threading.Thread(target=self._background_player_count, args=())
thread.start()
def get_current_iter(self) -> Gtk.TreeIter | None:
@@ -2212,9 +2181,7 @@ class TreeView(Gtk.TreeView):
context = App.treeview.get_subpage_label()
row_sel = self.get_value_at_index(0)
logger.info(
f"Tree selection for context '{context}' changed to '{row_sel}'"
)
logger.info(f"Tree selection for context '{context}' changed to '{row_sel}'")
if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate()
@@ -2272,9 +2239,7 @@ class TreeView(Gtk.TreeView):
self.set_cursor(path)
return None
def _on_keypress(
self, treeview: Gtk.TreeView, event: Gdk.EventKey
) -> bool | None:
def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool | None:
keyname = Gdk.keyval_name(event.keyval)
grid = App.grid
if event.state is Gdk.ModifierType.CONTROL_MASK:
@@ -2329,9 +2294,7 @@ class TreeView(Gtk.TreeView):
return False
return None
def _on_key_release(
self, treeview: Gtk.TreeView, event: Gdk.EventKey
) -> None:
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
"""
Suppresses spamming on keydown
"""
@@ -2446,10 +2409,7 @@ class TreeView(Gtk.TreeView):
params = Servers.params
serv = []
with ThreadPoolExecutor() as executor:
futures = [
executor.submit(job, key, APPID_DAYZ, param)
for param in params
]
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
wait(futures)
for future in futures:
res = future.result()
@@ -2471,10 +2431,7 @@ class TreeView(Gtk.TreeView):
def _dump_lan(self, port: int) -> list | None:
with ThreadPoolExecutor() as executor:
futures = [
executor.submit(Servers.test_ip, i, port)
for i in range(1, 256)
]
futures = [executor.submit(Servers.test_ip, i, port) for i in range(1, 256)]
wait(futures)
servers = []
for future in futures:
@@ -2493,6 +2450,8 @@ class TreeView(Gtk.TreeView):
def _dump_servers(self, ips: list) -> list | None:
if len(ips) == 0:
return []
# NOTE: block malformed records
ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""]
with ThreadPoolExecutor() as executor:
futures = [
executor.submit(
@@ -2597,9 +2556,7 @@ class TreeView(Gtk.TreeView):
ModelManager.set_store(App.treeview.get_model())
App.treeview.set_model(None)
thread = threading.Thread(
target=ModelManager.filter, args=(mode, *args)
)
thread = threading.Thread(target=ModelManager.filter, args=(mode, *args))
thread.start()
def _background_quad(self, dialog: "GenericDialog", mode: RowType) -> None:
@@ -2758,13 +2715,9 @@ class TreeView(Gtk.TreeView):
self.initialize_columns()
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.wait_dialog = GenericDialog(
"Fetching server metadata", Popup.WAIT
)
self.wait_dialog = GenericDialog("Fetching server metadata", Popup.WAIT)
self.wait_dialog.show_all()
thread = threading.Thread(
target=self._query_servers, args=(mode, port)
)
thread = threading.Thread(target=self._query_servers, args=(mode, port))
thread.start()
def _format_float(
@@ -2943,9 +2896,7 @@ class TreeView(Gtk.TreeView):
def get_view(self):
return self.view
def prepare_connection(
self, record: Record
) -> subprocess.CompletedProcess | None:
def get_prereqs(self, record: Record) -> tuple[bool, str, str | None, "Prereqs"]:
"""
Always called on a thread with a dialog on the transient parent window
"""
@@ -2953,50 +2904,39 @@ class TreeView(Gtk.TreeView):
if prereqs.appid is None:
logger.warning(f"Query to '{record.ip}:{record.qport}' timed out")
msg = "Timed out when querying server, check IP or try again later"
spawn_dialog(msg, Popup.NOTIFY)
return None
return (False, msg, None, prereqs)
build = "DayZ" if prereqs.appid == APPID_DAYZ else "DayZ Experimental"
steam_path = query_config("default_steam_path")[0]
if len(steam_path) < 1:
logger.critical(
"Config file has no value set for 'default_steam_path'"
)
logger.critical("Config file has no value set for 'default_steam_path'")
msg = "Local Steam installation is not set, possibly malformed config file."
spawn_dialog(msg, Popup.NOTIFY)
return None
return (False, msg, None, prereqs)
try:
pefile_path = PeFile.get_pefile_path(steam_path, prereqs.appid)
except AppNotInstalledError:
logger.critical(
f"'{prereqs.appid}' not found in user's libraryfolders"
)
logger.critical(f"'{prereqs.appid}' not found in user's libraryfolders")
msg = (
f"This server is running {build}. You can install "
f"{build} by searching for it in your Steam library. "
f"If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again."
)
spawn_dialog(msg, Popup.NOTIFY)
return None
return (False, msg, None, prereqs)
except AppMovedError:
logger.critical(
f"Library folder synch error for '{prereqs.appid}'"
)
logger.critical(f"Library folder synch error for '{prereqs.appid}'")
msg = (
f"Steam is reporting that {build} is installed at a non-existent location. "
f"If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again."
)
spawn_dialog(msg, Popup.NOTIFY)
return None
return (False, msg, None, prereqs)
except (VDFLoadError, PeFileError, Exception) as e:
logger.critical(e)
msg = "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
spawn_dialog(msg, Popup.NOTIFY)
return None
return (False, msg, None, prereqs)
try:
local_vers = PeFile.get_dayz_version(pefile_path)
@@ -3008,7 +2948,7 @@ class TreeView(Gtk.TreeView):
local_vers = None
try:
remote_vers = PeFile.dayz_version_from_str(prereqs.version)
remote_vers = PeFile.dayz_version_from_str(prereqs)
except Exception:
remote_vers = None
@@ -3021,18 +2961,15 @@ class TreeView(Gtk.TreeView):
f"This server is running a newer build ({prereqs.version}) of {build} than "
f"your local version. You may be unable to connect. Proceed anyway?"
)
res = spawn_dialog(msg, Popup.CONFIRM)
if res is True:
return None
return (True, msg, pefile_path, prereqs)
case VersionMatch.LOCAL_NEWER:
msg = (
f"This server is running an out-of-date build ({prereqs.version}) of {build}. "
"You may be unable to connect. Proceed anyway?"
)
res = spawn_dialog(msg, Popup.CONFIRM)
if res is True:
return None
return (True, msg, pefile_path, prereqs)
case VersionMatch.SAME_VERSION:
return (True, "", pefile_path, prereqs)
pass
if prereqs.password is True:
@@ -3040,19 +2977,9 @@ class TreeView(Gtk.TreeView):
"This server is password-protected and you will be "
"prompted when connecting. Do you want to proceed?"
)
res = spawn_dialog(msg, Popup.CONFIRM)
if res is True:
return None
return (True, msg, pefile_path, prereqs)
"""
When using RowType.CONN_BY_IP, the gameport needs to be interpolated
"""
record.gameport = prereqs.gameport
addr = record_to_str(record)
proc = call_out(
"try_connect", addr, str(prereqs.appid), str(pefile_path)
)
return proc
return (True, "", pefile_path, prereqs)
@signal_emission
@update_window_labels
@@ -3122,13 +3049,7 @@ class TreeView(Gtk.TreeView):
record = self.get_record()
if record is None:
return
thread_new_with_dialog(
self.prepare_connection,
parse_shell_output,
"Querying server",
None,
[record],
)
prepare_connection(None, record)
case _: # any other non-server option from the main menu
process_tree_option(output)
@@ -3213,7 +3134,7 @@ class GenericDialog(Gtk.MessageDialog):
self.outer.set_margin_end(30)
def _on_dialog_delete(
self, response_id: Gtk.ResponseType, event: Gdk.Event
self, response_id: Gtk.ResponseType, event: Gdk.Event
) -> Literal[True]:
"""
Prevent manual dialog destruction
@@ -3292,9 +3213,7 @@ class LanDialog(Gtk.MessageDialog):
self.connect("delete-event", self.restore_context)
self.entry.connect("insert-text", self._on_text_typed)
self.entry.get_property("buffer").connect(
"deleted-text", self._on_text_deleted
)
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
def _validate(self, text: str) -> None:
if self._is_invalid(text):
@@ -3410,9 +3329,7 @@ class DetailsDialog(GenericDialog):
for i, column_title in enumerate(["Item", "Details"]):
renderer = Gtk.CellRendererText(xalign=0)
if i == 0:
column = Gtk.TreeViewColumn(
column_title, renderer, text=i, weight=2
)
column = Gtk.TreeViewColumn(column_title, renderer, text=i, weight=2)
else:
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
@@ -3428,12 +3345,8 @@ class DetailsDialog(GenericDialog):
scrollable_message = Gtk.ScrolledWindow()
desc = Gtk.Label(label="Server message", valign=Gtk.Align.START)
add_class(desc, "details-heading")
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER
)
self.description = Gtk.Label(
justify=Gtk.Justification.CENTER, wrap=True
)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_bottom(10)
for el in desc, sep, self.description:
@@ -3471,9 +3384,7 @@ class DetailsDialog(GenericDialog):
self.run()
self.destroy()
def _background(
self, dialog: "GenericDialog", ip: str, qport: int
) -> None:
def _background(self, dialog: "GenericDialog", ip: str, qport: int) -> None:
response = Servers.details(self.ip, self.qport)
if response.success:
for row in response.data:
@@ -3530,9 +3441,7 @@ class ModDialog(GenericDialog):
wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT)
wait_dialog.show_all()
thread = threading.Thread(
target=self._background, args=(wait_dialog, record)
)
thread = threading.Thread(target=self._background, args=(wait_dialog, record))
thread.start()
def _background(self, dialog: "GenericDialog", record: str) -> None:
@@ -3557,9 +3466,7 @@ class ModDialog(GenericDialog):
self.view.set_model(modlist_store)
GLib.idle_add(_load)
def _parse_modlist_rows(
self, data: subprocess.CompletedProcess
) -> bool | int:
def _parse_modlist_rows(self, data: subprocess.CompletedProcess) -> bool | int:
lines = data.stdout.splitlines()
hits = len(lines)
reader = csv.reader(lines, delimiter=delimiter)
@@ -3589,9 +3496,7 @@ class ModDialog(GenericDialog):
class LinkDialog(GenericDialog):
def __init__(
self, text: str, link: str | None, command: RowType, uid: str = ""
):
def __init__(self, text: str, link: str | None, command: RowType, uid: str = ""):
super().__init__(text, Popup.NOTIFY)
text = textwrap.dedent(text)
@@ -3809,7 +3714,12 @@ class Options(Gtk.Box):
client_store = Gtk.ListStore(str, str)
client_store.append(["steam", "steam"])
client_store.append(["flatpak", "flatpak run com.valvesoftware.Steam"])
client_store.append(["flatpak (container)", "flatpak-spawn --host flatpak run com.valvesoftware.Steam"])
client_store.append(
[
"flatpak (container)",
"flatpak-spawn --host flatpak run com.valvesoftware.Steam",
]
)
renderer_text = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
self.client_combo = Gtk.ComboBox.new_with_model(client_store)
@@ -3833,8 +3743,7 @@ class Options(Gtk.Box):
self.force_button.set_sensitive(False)
msg = (
"Manual: prompt to subscribe to mods in Steam. "
"Auto: unmanned downloads."
"Manual: prompt to subscribe to mods in Steam. " "Auto: unmanned downloads."
)
eb = InfoEventBox(msg)
@@ -3893,9 +3802,7 @@ class Options(Gtk.Box):
self.make_frame(mods_grid, "Mods"),
self.make_frame(version_grid, "Version"),
]:
grid.attach(
frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT
)
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
row += 1
self.scrollable = Gtk.ScrolledWindow(vexpand=True)
@@ -3941,9 +3848,7 @@ class Options(Gtk.Box):
return box
def _on_field_activated(
self, entry: Gtk.Entry, context: Preferences
) -> None:
def _on_field_activated(self, entry: Gtk.Entry, context: Preferences) -> None:
text = entry.get_text()
button = entry.sibling
if not self._is_valid_text(text, context):
@@ -3964,9 +3869,7 @@ class Options(Gtk.Box):
for record in rows:
col = 1
for el in record:
grid.attach(
el, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT
)
grid.attach(el, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
col += 1
row += 1
return grid
@@ -4019,9 +3922,7 @@ class Options(Gtk.Box):
return
process_toggle(RowType.TGL_BRANCH)
def _on_radio_toggled(
self, button: Gtk.RadioButton, context: Preferences
) -> None:
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
if App.treeview.subpage is not RowType.OPTIONS:
return
match context:
@@ -4165,18 +4066,14 @@ class Options(Gtk.Box):
field[1].get_children()[1].set_sensitive(False)
try:
pe_file_path = PeFile.get_pefile_path(
default_steam_path, APPID_DAYZ
)
pe_file_path = PeFile.get_pefile_path(default_steam_path, APPID_DAYZ)
vers = PeFile.get_dayz_version(pe_file_path)
dayz_version = PeFile.dayz_version_to_str(vers)
except Exception:
dayz_version = "-"
try:
exp_file_path = PeFile.get_pefile_path(
default_steam_path, APPID_DAYZ_EXP
)
exp_file_path = PeFile.get_pefile_path(default_steam_path, APPID_DAYZ_EXP)
vers = PeFile.get_dayz_version(exp_file_path)
dayz_exp_version = PeFile.dayz_version_to_str(vers)
except Exception:
@@ -4285,9 +4182,7 @@ class KeybindingsDialog(Gtk.Box):
if not sep:
grid.attach(desc, col, row, w, h)
else:
grid.attach_next_to(
desc, sep, Gtk.PositionType.BOTTOM, w, h
)
grid.attach_next_to(desc, sep, Gtk.PositionType.BOTTOM, w, h)
row += 1
sep = None
grid.attach_next_to(frame, desc, Gtk.PositionType.RIGHT, w, h)
@@ -4295,9 +4190,7 @@ class KeybindingsDialog(Gtk.Box):
l_spacer = Gtk.Label(label="")
r_spacer = Gtk.Label(label="")
grid.attach(l_spacer, col, row + 1, w, h)
grid.attach_next_to(
r_spacer, l_spacer, Gtk.PositionType.RIGHT, w, h
)
grid.attach_next_to(r_spacer, l_spacer, Gtk.PositionType.RIGHT, w, h)
row += 1
return grid
@@ -4306,9 +4199,7 @@ class KeybindingsDialog(Gtk.Box):
col = 0
w = 1
h = 1
sidebar = Gtk.Grid(
row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL
)
sidebar = Gtk.Grid(row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL)
for cat in categories:
label = Gtk.Label(label=cat)
add_class(label, "left-label")
@@ -4328,9 +4219,7 @@ class KeybindingsDialog(Gtk.Box):
column = 1
w = 1
h = 1
sidebar = self.build_sidebar(
["Servers", "Navigation", "Vim-style keys"]
)
sidebar = self.build_sidebar(["Servers", "Navigation", "Vim-style keys"])
separator = Gtk.Separator()
keys_box = self.build_keys(items)
@@ -4548,9 +4437,7 @@ class Statusbar(Gtk.Statusbar):
players_pretty = pluralize("players", players)
hits_pretty = pluralize("matches", hits)
formatted = (
f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}"
)
formatted = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}"
suffix = "| Distance: calculating..."
if players == 0:
@@ -4602,9 +4489,7 @@ class Grid(Gtk.Grid):
self.breadcrumbs = Gtk.Label(label="Main menu", halign=Gtk.Align.START)
self.attach(self.notebook, 0, 0, 3, 1)
self.attach_next_to(
self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1
)
self.attach_next_to(self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1)
self.attach_next_to(
self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1
)
@@ -4666,11 +4551,23 @@ class App(Gtk.Application):
)
self.win.add_accel_group(accel)
GLib.unix_signal_add(
GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint
)
self.setup_signals()
Gtk.main()
def setup_signals(self) -> None:
SIGNAL_ADD = "signal_add"
SIGNAL_ADD_FULL = "signal_add_full"
try:
if SIGNAL_ADD in dir(GLibUnix):
func = GLibUnix.signal_add
elif SIGNAL_ADD_FULL in dir(GLibUnix):
func = GLibUnix.signal_add_full
else:
func = GLib.unix_signal_add
func(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint)
except Exception as e:
logger.critical(e)
def _catch_sigint(self) -> None:
self.win.halt_proc_and_quit()
@@ -4755,9 +4652,7 @@ class ModSelectionPanel(Gtk.Box):
def toggle_select_stale_button(self, state: bool) -> None:
if state:
button = Gtk.Button(
label="Select stale", margin_start=10, margin_end=10
)
button = Gtk.Button(label="Select stale", margin_start=10, margin_end=10)
text = "Bulk selects all currently highlighted mods"
button.set_tooltip_text(text)
button.connect("clicked", self._on_button_clicked)
@@ -4877,9 +4772,7 @@ class FilterPanel(Gtk.Box):
self.keyword_entry = Gtk.Entry()
self.keyword_entry.set_placeholder_text("Filter by keyword")
self.keyword_entry.connect("activate", self._on_keyword_enter)
self.keyword_entry.connect(
"key-press-event", self._on_keyword_keypress
)
self.keyword_entry.connect("key-press-event", self._on_keyword_keypress)
completion = Gtk.EntryCompletion(inline_completion=True)
completion.set_text_column(0)
@@ -4947,9 +4840,7 @@ class FilterPanel(Gtk.Box):
state = self.default_filters[label]
check.set_active(state)
def _on_map_entry_keypress(
self, entry: Gtk.Entry, event: Gdk.EventKey
) -> None:
def _on_map_entry_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> None:
match event.keyval:
case Gdk.KEY_Return:
text = entry.get_text()
@@ -4992,9 +4883,7 @@ class FilterPanel(Gtk.Box):
App.treeview.grab_focus()
return False
def _on_keyword_keypress(
self, entry: Gtk.Entry, event: Gdk.EventKey
) -> bool:
def _on_keyword_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> bool:
match event.keyval:
case Gdk.KEY_Up:
return True
@@ -5005,9 +4894,7 @@ class FilterPanel(Gtk.Box):
return True
return False
def _on_combo_keypress(
self, combo: Gtk.ComboBox, event: Gdk.EventKey
) -> bool:
def _on_combo_keypress(self, combo: Gtk.ComboBox, event: Gdk.EventKey) -> bool:
match event.keyval:
case Gdk.KEY_Down:
self.maps_combo.popup()