Compare commits

..
1 Commits
Author SHA1 Message Date
aclist fb728e0418 Merge 5bce8709af into a08c588df3 2025-11-10 03:07:51 +00:00
12 changed files with 559 additions and 638 deletions

No files matched your search

-4
View File
@@ -5,10 +5,6 @@ labels: ["bug"]
assignees: assignees:
- aclist - aclist
body: body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: checkboxes - type: checkboxes
id: kb id: kb
attributes: attributes:
-4
View File
@@ -5,10 +5,6 @@ labels: ["troubleshooting"]
assignees: assignees:
- aclist - aclist
body: body:
- type: markdown
attributes:
value: |
### WARNING: When posting log/config data, redact any sensitive information like API keys.
- type: dropdown - type: dropdown
id: device id: device
attributes: attributes:
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
mirror-to-codeberg: mirror-to-codeberg:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: pixta-dev/repository-mirroring-action@v1
-3
View File
@@ -1,4 +1 @@
tags tags
__pycache__
pre-commit
tests/
+2 -96
View File
@@ -1,105 +1,11 @@
# Changelog # Changelog
## [6.0.6-beta.1] 2026-05-17 ## [6.0.1-beta.2] 2025-11-10
## Changed
- Update symlink hash logic to align with DZGUI 7 beta
- Update A2S helper to latest commit
## [6.0.5-beta.2] 2026-04-17
## Changed
- During server connection attempts, only download mods local to specific server
## [6.0.5-beta.1] 2026-04-13
## Fixed
- Update A2S module checksums to support servers with missing description field
## Changed
- Bump version to reflect hotfixes on stable branch
## [6.0.2-beta.5] 2026-04-12
## Fixed
- Use conditional logic when testing GLibUnix namespaces
## [6.0.2-beta.4] 2026-04-10
## Fixed
- Use GLibUnix namespace when setting up SIGINT handling
## [6.0.2-beta.3] 2026-01-26
## Fixed
- Raise error correctly from module
## [6.0.2-beta.2] 2026-01-18
## Fixed
- Helpers not being downloaded (uncommented boilerplate)
## [6.0.2-beta.1] 2026-01-18
## Fixed
- 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-beta.1] 2026-01-16
## Fixed
- Explicitly use Python 3.13 when calling subprocesses
## [6.0.0-beta.20] 2026-01-14
## Fixed
- Write cache file after wiping
## [6.0.0-beta.19] 2026-01-14
## Fixed
- Steam Deck compatible script path expansion
## [6.0.0-beta.18] 2026-01-14
## Fixed
- Update UI checksums
## [6.0.0-beta.17] 2026-01-14
## Added
- More descriptive help dialogs when initial dependencies are missing
## [6.0.0-beta.16] 2026-01-04
## Fixed
- Regression introduced in previous hotfix
## [6.0.0-beta.15] 2025-12-27
## Fixed
- Path discovery during first-time setup when parsing filepaths with whitespaces
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
## [6.0.0-beta.14] 2025-12-09
## Fixed
- Dialogs with newlines breaking output in log table
## [6.0.0-beta.13] 2025-11-29
## Fixed
- Server filter panel not being hidden when entering other page contexts
- Path to remote changelog being constructed incorrectly
## [6.0.0-beta.12] 2025-11-14
## Changed
- Require Python 3.13
## Fixed
- Normalize beta version
## [6.0.0-beta.11] 2025-11-13
## Added
- Support sandboxed flatpak
## Changed
- Preferred client from radio toggle to combobox
## Fixed
- Config file erroneously getting updated when populating settings menu
## [6.0.0-beta.10] 2025-11-10
## Fixed ## Fixed
- Python 3.14 error in finally block - Python 3.14 error in finally block
- Require Python version between 3.11 and 3.12 - Require Python version between 3.11 and 3.12
## [6.0.0-beta.9] 2025-11-05 ## [6.0.1-beta.1] 2025-11-05
## Fixed ## Fixed
- UI not being constructed correctly if CHANGELOG.md was missing - UI not being constructed correctly if CHANGELOG.md was missing
+3 -3
View File
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
# Requesting help # Requesting help
If you encounter a problem with DZGUI, you can submit tickets on the GitHub If you encounter a problem with DZGUI, you can submit tickets on the GitHub
[issue tracker](https://github.com/aclist/dztui/issues) under the (issue tracker)[https://github.com/aclist/dzgui-devel/issues] under the
`troubleshooting` tag. `troubleshooting` tag.
# How can I help the project? # How can I help the project?
@@ -24,7 +24,7 @@ accordingly.
## Submitting a ticket ## Submitting a ticket
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues). Navigate to the GitHub (issue tracker)[https://github.com/aclist/dzgui-devel/issues].
From there, follow the onscreen prompts. You will be asked questions such as: From there, follow the onscreen prompts. You will be asked questions such as:
- What version are you using? - What version are you using?
@@ -67,5 +67,5 @@ will be merged to `prerelease/<latest-version>`, then `testing`, and after a liv
testing phase, to `stable` as well. testing phase, to `stable` as well.
It is recommended to follow 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. integrates well with tooling and helps the project review your code.
+1 -9
View File
@@ -11,15 +11,6 @@ This overcomes certain limitations in the Linux client and helps prepare the gam
- Concatenate launch options to pass to Steam - Concatenate launch options to pass to Steam
- Connect to mod-enabled LAN servers - 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 ## Setup and usage
Refer to the documentation for installation and setup instructions: Refer to the documentation for installation and setup instructions:
@@ -27,6 +18,7 @@ Refer to the documentation for installation and setup instructions:
- [GitHub](https://aclist.github.io/dzgui/index.html) - [GitHub](https://aclist.github.io/dzgui/index.html)
- [Mirror (Codeberg)](https://aclist.codeberg.page) - [Mirror (Codeberg)](https://aclist.codeberg.page)
![A screenshot of DZGUI](/images/example.png) ![A screenshot of DZGUI](/images/example.png)
## Attribution ## Attribution
+104 -174
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
src_path="$(readlink -e "$0")" src_path=$(realpath "$0")
version=6.0.6-beta.1
version=6.0.1.beta-2
reference_branch="prerelease/6.0.1-beta.1"
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@@ -40,7 +42,6 @@ cols_file="$state_path/$prefix.cols.json"
#CACHE FILES #CACHE FILES
coords_file="$cache_path/$prefix.coords" coords_file="$cache_path/$prefix.coords"
src_path_file="$cache_path/$prefix.src"
#legacy paths #legacy paths
hist_file="$config_path/history" hist_file="$config_path/history"
@@ -117,7 +118,6 @@ setup_state_files(){
done done
logger INFO "Wiped cache files" logger INFO "Wiped cache files"
fi fi
echo "$src_path" > "$src_path_file"
} }
print_config_vals(){ print_config_vals(){
local keys=( local keys=(
@@ -139,10 +139,10 @@ print_config_vals(){
} }
test_gobject(){ test_gobject(){
python3.13 -c "import gi" python3 -c "import gi"
if [[ ! $? -eq 0 ]]; then if [[ ! $? -eq 0 ]]; then
logger CRITICAL "Missing PyGObject" logger CRITICAL "Missing PyGObject"
quit_with_help_dialog "Requires PyGObject (python-gobject)" fdialog "Requires PyGObject (python-gobject)"
exit 1 exit 1
fi fi
logger INFO "Found PyGObject in Python env" logger INFO "Found PyGObject in Python env"
@@ -227,8 +227,7 @@ depcheck(){
for dep in "${!deps[@]}"; do for dep in "${!deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then if ! command -v "$dep" &> /dev/null; then
local msg="Requires $dep >= ${deps[$dep]}" local msg="Requires $dep >= ${deps[$dep]}"
echo "$msg" raise_error_and_quit "$msg"
exit 1
fi fi
done done
local jqmsg="jq must be compiled with support for oniguruma" local jqmsg="jq must be compiled with support for oniguruma"
@@ -237,31 +236,14 @@ depcheck(){
[[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg" [[ $? -ne 0 ]] && raise_error_and_quit "$jqmsg"
logger INFO "Initial dependencies satisfied" logger INFO "Initial dependencies satisfied"
} }
open_url(){
url="$1"
if [[ -n "$BROWSER" ]]; then
logger INFO "Opening '$url' in '$BROWSER'"
"$BROWSER" "$url"
else
logger INFO "Opening '$url' with xdg-open"
xdg-open "$url"
fi
}
quit_with_help_dialog(){
local sel
msg="$1"
help_button="Open help page"
url="https://aclist.github.io/dzgui/installation.html"
logger CRITICAL "$msg"
sel=$(zenity --info --extra-button="$help_button" --text="$msg")
[[ $sel == "$help_button" ]] && open_url "$url" &
exit 1
}
check_pyver(){ check_pyver(){
if [[ ! $(python3.13 --version) ]]; then local pyver=$(python3 --version | awk '{print $2}')
local msg="Requires Python 3.13" local minor=$(<<< $pyver awk -F. '{print $2}')
quit_with_help_dialog "$msg" if [[ -z $pyver ]] || [[ ${pyver:0:1} -lt 3 ]] || [[ $minor -lt 11 ]] || [[ $minor -gt 12 ]]; then
local msg="Requires Python 3.11 or 3.12"
raise_error_and_quit "$msg"
fi fi
logger INFO "Found Python version: $pyver"
} }
watcher_deps(){ watcher_deps(){
if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then if [[ ! $(command -v wmctrl) ]] && [[ ! $(command -v xdotool) ]]; then
@@ -335,7 +317,6 @@ check_unmerged(){
fi fi
} }
check_version(){ check_version(){
echo "# Checking version"
[[ -n $reference_branch ]] && return [[ -n $reference_branch ]] && return
local version_url=$(format_version_url) local version_url=$(format_version_url)
local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}') local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}')
@@ -400,11 +381,10 @@ prompt_dl(){
dl_changelog(){ dl_changelog(){
local mdbranch local mdbranch
local md local md
source "$config_file"
[[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "stable" ]] && mdbranch="dzgui"
[[ $branch == "testing" ]] && mdbranch="testing" [[ $branch == "testing" ]] && mdbranch="testing"
local changelog="$url_prefix/${mdbranch}/CHANGELOG.md" local md="$url_prefix/${mdbranch}/$file"
curl -Ls "$changelog" > "$state_path/CHANGELOG.md" curl -Ls "$md" > "$state_path/CHANGELOG.md"
} }
test_display_mode(){ test_display_mode(){
pgrep -a gamescope | grep -q "generate-drm-mode" pgrep -a gamescope | grep -q "generate-drm-mode"
@@ -415,7 +395,6 @@ test_display_mode(){
fi fi
} }
check_architecture(){ check_architecture(){
echo "# Checking system architecture"
local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}') local cpu=$(< /proc/cpuinfo awk -F": " '/AMD Custom APU [0-9]{4}$/ {print $2; exit}')
read -a APU_MODEL <<< "$cpu" read -a APU_MODEL <<< "$cpu"
if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then if [[ ${APU_MODEL[3]} != "0932" ]] && [[ ${APU_MODEL[3]} != "0405" ]]; then
@@ -432,7 +411,6 @@ check_architecture(){
logger INFO "Setting architecture to 'Steam Deck'" logger INFO "Setting architecture to 'Steam Deck'"
} }
check_map_count(){ check_map_count(){
echo "# Checking map count"
[[ $is_steam_deck -gt 0 ]] && return 0 [[ $is_steam_deck -gt 0 ]] && return 0
local map_count_file="/proc/sys/vm/max_map_count" local map_count_file="/proc/sys/vm/max_map_count"
local min_count=1048576 local min_count=1048576
@@ -482,7 +460,6 @@ tdialog(){
$steamsafe_zenity --info --text="$1" "${zenity_flags[@]}" $steamsafe_zenity --info --text="$1" "${zenity_flags[@]}"
} }
steam_deps(){ steam_deps(){
echo "# Checking Steam"
local flatpak local flatpak
local steam local steam
[[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam) [[ $(command -v flatpak) ]] && flatpak=$(flatpak list | grep valvesoftware.Steam)
@@ -491,15 +468,33 @@ steam_deps(){
local msg="Found neither Steam nor Flatpak Steam" local msg="Found neither Steam nor Flatpak Steam"
raise_error_and_quit "$msg" raise_error_and_quit "$msg"
exit 1 exit 1
elif [[ -n "$steam" ]] && [[ -n "$flatpak" ]]; then
[[ -n $preferred_client ]] && return 0
if [[ -z $preferred_client ]]; then
preferred_client="steam"
fi
elif [[ -n "$steam" ]]; then
preferred_client="steam"
else
preferred_client="flatpak"
fi fi
update_config
logger INFO "Preferred client set to '$preferred_client'"
}
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(){ stale_symlinks(){
echo "# Cleaning stale symlinks"
local game_dir="$steam_path/steamapps/common/DayZ" local game_dir="$steam_path/steamapps/common/DayZ"
readarray -t links < <(find "$game_dir" -xtype l) for l in $(find "$game_dir" -xtype l); do
for link in "${links[@]}"; do logger DEBUG "Updating stale symlink '$l'"
logger DEBUG "Updating stale symlink '$link'" unlink "$l"
unlink "$link"
done done
} }
@@ -508,7 +503,7 @@ check_availability() {
return 1 return 1
fi fi
local url=$1 local url=$1
local timeout_sec="1" local timeout_sec="3"
if [[ $2 ]]; then if [[ $2 ]]; then
timeout_sec=$2 timeout_sec=$2
fi fi
@@ -519,7 +514,6 @@ check_availability() {
} }
local_latlon(){ local_latlon(){
echo "# Checking coordinates"
if [[ -z $(command -v dig) ]]; then if [[ -z $(command -v dig) ]]; then
local url_ipecho="https://ipecho.net/plain" local url_ipecho="https://ipecho.net/plain"
if ! check_availability "ipecho.net"; then if ! check_availability "ipecho.net"; then
@@ -531,9 +525,6 @@ local_latlon(){
# TODO : implement checking remote # TODO : implement checking remote
local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com) local local_ip=$(dig -4 +short myip.opendns.com @resolver1.opendns.com)
fi fi
if calc_local_coords "$local_ip"; then
return 0
fi
local url_ip_api="http://ip-api.com/json/$local_ip" local url_ip_api="http://ip-api.com/json/$local_ip"
if ! check_availability "ip-api.com"; then if ! check_availability "ip-api.com"; then
logger WARN "Failed to get local coordinates, ip-api.com service may be down." logger WARN "Failed to get local coordinates, ip-api.com service may be down."
@@ -548,7 +539,6 @@ local_latlon(){
} }
lock(){ lock(){
echo "# Setting up lock file"
[[ ! -f $lock_file ]] && touch $lock_file [[ ! -f $lock_file ]] && touch $lock_file
local pid=$(cat $lock_file) local pid=$(cat $lock_file)
ps -p $pid -o pid= >/dev/null 2>&1 ps -p $pid -o pid= >/dev/null 2>&1
@@ -567,12 +557,9 @@ get_hash(){
md5sum "$1" | awk '{print $1}' md5sum "$1" | awk '{print $1}'
} }
fetch_a2s(){ fetch_a2s(){
sum="d26da2fe03df2b95436f823f0496887b" # this file is currently monolithic
if [[ -d $helpers_path/a2s ]] && [[ $(get_hash "$helpers_path/a2s/info.py") == $sum ]]; then [[ -d $helpers_path/a2s ]] && { logger INFO "A2S helper is current"; return 0; }
logger INFO "A2S helper is current" local sha=c7590ffa9a6d0c6912e17ceeab15b832a1090640
return 0
fi
local sha=b40eb24cdbb06ebd08272f224257fe5a81610e86
local author="yepoleb" local author="yepoleb"
local repo="python-a2s" local repo="python-a2s"
local url="https://github.com/$author/$repo/tarball/$sha" local url="https://github.com/$author/$repo/tarball/$sha"
@@ -586,13 +573,13 @@ fetch_a2s(){
logger INFO "Updated A2S helper to sha '$sha'" logger INFO "Updated A2S helper to sha '$sha'"
} }
fetch_dzq(){ fetch_dzq(){
local sum="0d316badbb8d943c5f078e73862cac3e" local sum="0a334e1e144e76e560419d155435c91e"
local file="$helpers_path/a2s/dayzquery.py" local file="$helpers_path/a2s/dayzquery.py"
if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then if [[ -f $file ]] && [[ $(get_hash "$file") == $sum ]]; then
logger INFO "DZQ is current" logger INFO "DZQ is current"
return 0 return 0
fi fi
local sha="07483b88ed096327ebca752f5e177011b604d7fe" local sha=a22a9f428cbe075d7dda62f78000296955eea92a
local author="yepoleb" local author="yepoleb"
local repo="dayzquery" local repo="dayzquery"
local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py" local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py"
@@ -629,12 +616,12 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file" [[ -f "$config_file" ]] && source "$config_file"
declare -A sums declare -A sums
sums=( sums=(
["funcs"]="1cd26343cd68a5382e3377a9757497fa" ["funcs"]="5d7d92b7b8a9c788b133ce5271a76553"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["servers.py"]="ed442c3aecf33f777d59dcf53650d263" ["servers.py"]="ed442c3aecf33f777d59dcf53650d263"
["ui.py"]="cf52a3d5883afe02a013d8673df494fa" ["ui.py"]="2476397883a4272eaf940403ba5a6575"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["pefile.py"]="b452974a84bff1d821872fcebf59e380" ["pefile.py"]="21531f2c0d9dfa5f110cf6779f9d22c0"
) )
local author="aclist" local author="aclist"
local repo="dztui" local repo="dztui"
@@ -694,12 +681,15 @@ fetch_km_helper(){
} }
get_response_code(){ get_response_code(){
local url="$1" local url="$1"
curl --connect-timeout 3 -Ls -I -o /dev/null -w "%{http_code}" "$url" curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
}
raise_error_and_quit(){
echo "$1"
exit 1
} }
fetch_ip_db(){ fetch_ip_db(){
parse_dl_url(){ parse_dl_url(){
curl --connect-timeout 3 -Ls "$url" \ curl -Ls "$url" \
| grep "csv.gz" \ | grep "csv.gz" \
| awk -F"['']" '{print $2}' | awk -F"['']" '{print $2}'
} }
@@ -715,9 +705,6 @@ fetch_ip_db(){
fetch(){ fetch(){
logger INFO "Triggering fetch routine" logger INFO "Triggering fetch routine"
local url="$1" 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" curl -Ls "$url" > "$base_file"
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@@ -737,7 +724,7 @@ fetch_ip_db(){
logger WARN "Abnormal exit while parsing IPs" logger WARN "Abnormal exit while parsing IPs"
return return
fi fi
rm "${state_path}/${this_month}.csv" rm "${state_path}/${month}.csv"
readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}') 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 if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then
@@ -752,57 +739,55 @@ fetch_ip_db(){
rm "$ip_file" rm "$ip_file"
fi fi
echo "$this_month" > "$month_file" echo "$month" > "$month_file"
logger INFO "Wrote '$this_month' to stub '$month_file'" logger INFO "Wrote '$month' to stub '$month_file'"
logger INFO "Updated '$ip_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 url="https://db-ip.com/db/download/ip-to-city-lite"
month_file="${state_path}/.month" local month_file="${state_path}/.month"
this_month=$(date +%Y-%m) 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"
# no stub file # no stub file
if [[ ! -f $month_file ]]; then if [[ ! -f $month_file ]]; then
logger WARN "No stub file '$month_file' present" logger WARN "No stub file '$month_file' present"
check_remote "$url" fetch "$dl_url"
return
fi
# if stub is same date, abort
local last_month=$(< "$month_file")
if [[ $last_month == "$this_month" ]]; then
logger INFO "Local stub '$last_month' is identical to remote, skipping"
return return
fi fi
# local needs update # local needs update
logger WARN "Local stub '$last_month' does not match '$this_month'" local last_month=$(< "$month_file")
check_remote "$url" if [[ $last_month != "$month" ]]; then
return logger WARN "Local stub '$last_month' does not match remote stub '$month'"
fetch "$dl_url"
return
fi
# if stub is same date, abort
logger INFO "Local stub '$last_month' is identical to remote, skipping"
} }
fetch_helpers(){ fetch_helpers(){
echo "# Checking helper files"
fetch_a2s fetch_a2s
fetch_dzq fetch_dzq
fetch_km_helper fetch_km_helper
@@ -901,26 +886,12 @@ file_picker(){
} }
find_library_folder(){ find_library_folder(){
local search_path="$1" local search_path="$1"
readarray -t paths < <(python3.13 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \ steam_path="$(python3 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \
| jq -r '.libraryfolders[]|select(.apps|has("221100")).path') | jq -r '.libraryfolders[]|select(.apps|has("221100")).path')"
if [[ ! $? -eq 0 ]]; then if [[ ! $? -eq 0 ]] || [[ -z $steam_path ]]; then
logger WARN "Failed to parse Steam path using '$search_path'" logger WARN "Failed to parse Steam path using '$search_path'"
return 1 return 1
fi fi
if [[ ${#paths[@]} -eq 0 ]]; then
logger WARN "No valid VDF paths in '$search_path'"
return 1
fi
if [[ ${#paths[@]} -gt 1 ]]; then
for path in "${paths[@]}"; do
if [[ -d "$path" ]]; then
steam_path="$path"
break
fi
done
else
steam_path="${paths[0]}"
fi
logger INFO "Steam path resolved to: $steam_path" logger INFO "Steam path resolved to: $steam_path"
} }
create_config(){ create_config(){
@@ -929,7 +900,6 @@ create_config(){
unset default_steam_path unset default_steam_path
unset steam_path unset steam_path
preferred_client="steam"
while true; do while true; do
local player_input="$($steamsafe_zenity \ local player_input="$($steamsafe_zenity \
--forms \ --forms \
@@ -990,7 +960,6 @@ create_config(){
done done
} }
varcheck(){ varcheck(){
echo "# Checking config file"
local msg="Config file '$config_file' missing. Start first-time setup now?" 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?" 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 if [[ ! -f $config_file ]]; then
@@ -1004,7 +973,7 @@ varcheck(){
source "$config_file" source "$config_file"
local workshop_dir="$steam_path/steamapps/workshop/content/$aid" local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
local game_dir="$steam_path/steamapps/common/DayZ" local game_dir="$steam_path/steamapps/common/DayZ"
if [[ ! -d $steam_path ]] || [[ ! -d $game_dir ]] || [[ ! $(find "$game_dir" -type f) ]]; then if [[ ! -d $steam_path ]] || [[ ! -d $game_dir ]] || [[ ! $(find $game_dir -type f) ]]; then
logger WARN "DayZ path resolved to '$game_dir'" logger WARN "DayZ path resolved to '$game_dir'"
logger WARN "Workshop path resolved to '$workshop_dir'" logger WARN "Workshop path resolved to '$workshop_dir'"
qdialog "$msg2" "Yes" "Exit" qdialog "$msg2" "Yes" "Exit"
@@ -1017,14 +986,12 @@ varcheck(){
fi fi
} }
is_dzg_downloading(){ is_dzg_downloading(){
echo "# Checking DayZ"
if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then if [[ -d $steam_path ]] && [[ -d $steam_path/downloading/$aid ]]; then
logger WARN "DayZ may be scheduling updates" logger WARN "DayZ may be scheduling updates"
return 0 return 0
fi fi
} }
is_steam_running(){ is_steam_running(){
echo "# Checking Steam"
local res=$(ps aux | grep "steamwebhelper" | grep -v grep) local res=$(ps aux | grep "steamwebhelper" | grep -v grep)
if [[ -z $res ]]; then if [[ -z $res ]]; then
logger WARN "Steam may not be running" logger WARN "Steam may not be running"
@@ -1037,7 +1004,6 @@ get_response_code(){
curl -Ls -I -o /dev/null -w "%{http_code}" "$url" curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
} }
test_connection(){ test_connection(){
echo "# Testing connection"
declare -A hr declare -A hr
local res1 local res1
local res2 local res2
@@ -1071,7 +1037,6 @@ legacy_cols(){
mv $cols_file.new $cols_file mv $cols_file.new $cols_file
} }
stale_mod_signatures(){ stale_mod_signatures(){
echo "# Checking stale mods"
[[ ! -f "$versions_file" ]] && return [[ ! -f "$versions_file" ]] && return
local workshop_dir="$steam_path/steamapps/workshop/content/$aid" local workshop_dir="$steam_path/steamapps/workshop/content/$aid"
if [[ -d $workshop_dir ]]; then if [[ -d $workshop_dir ]]; then
@@ -1085,48 +1050,18 @@ stale_mod_signatures(){
} }
create_new_links(){ create_new_links(){
echo "# Setting up symlinks"
"$func_helper" "update_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(){ initial_setup(){
setup_dirs
setup_state_files
depcheck
check_pyver
test_gobject
watcher_deps
check_architecture check_architecture
test_connection test_connection
fetch_helpers fetch_helpers > >(pdialog "Checking helper files")
varcheck varcheck
source "$config_file" source "$config_file"
lock lock
@@ -1135,12 +1070,14 @@ initial_setup(){
check_version check_version
check_map_count check_map_count
steam_deps steam_deps
migrate_files
stale_symlinks stale_symlinks
stale_mod_signatures stale_mod_signatures
create_new_links create_new_links
local_latlon local_latlon
is_steam_running is_steam_running
is_dzg_downloading is_dzg_downloading
print_config_vals
} }
uninstall(){ uninstall(){
_full(){ _full(){
@@ -1242,18 +1179,11 @@ main(){
set_im_module set_im_module
printf "Checking dependencies...\n" printf "Initializing setup...\n"
setup_dirs initial_setup
setup_state_files
depcheck
check_pyver
test_gobject
watcher_deps
initial_setup > >(pdialog "Initializing setup")
printf "All OK. Kicking off UI...\n" printf "All OK. Kicking off UI...\n"
python3.13 "$ui_helper" "--init-ui" "$version" "$is_steam_deck" python3 "$ui_helper" "--init-ui" "$version" "$is_steam_deck"
} }
+171 -162
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version="6.0.5-beta.2" version="6.0.1-beta.2"
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@@ -51,7 +51,6 @@ _cache_binary="$cache_dir/$prefix.binary"
_cache_coords="$cache_path/$prefix.coords" _cache_coords="$cache_path/$prefix.coords"
_cache_cooldown="$cache_path/$prefix.cooldown" _cache_cooldown="$cache_path/$prefix.cooldown"
_cache_lan="$cache_path/$prefix.lan" _cache_lan="$cache_path/$prefix.lan"
_cache_src_path="$cache_path/$prefix.src"
#XDG #XDG
freedesktop_path="$HOME/.local/share/applications" freedesktop_path="$HOME/.local/share/applications"
@@ -88,14 +87,17 @@ steam_api_url="https://steamcommunity.com/dev/apikey"
battlemetrics_api_url="https://www.battlemetrics.com/developers" battlemetrics_api_url="https://www.battlemetrics.com/developers"
bm_api="https://api.battlemetrics.com/servers" bm_api="https://api.battlemetrics.com/servers"
steam_cmd="$preferred_client" if [[ $preferred_client == "steam" ]]; then
steam_cmd="steam"
else
steam_cmd="flatpak run com.valvesoftware.Steam"
fi
declare -A funcs=( declare -A funcs=(
["Highlight stale"]="find_stale_mods" ["Highlight stale"]="find_stale_mods"
["Change player name"]="update_config_val" ["Change player name"]="update_config_val"
["Change Steam API key"]="update_config_val" ["Change Steam API key"]="update_config_val"
["Change Battlemetrics API key"]="update_config_val" ["Change Battlemetrics API key"]="update_config_val"
["Change client"]="update_config_val"
["Change favorite server"]="add_record" ["Change favorite server"]="add_record"
["Add server by IP"]="add_record" ["Add server by IP"]="add_record"
["Add server by ID"]="add_record" ["Add server by ID"]="add_record"
@@ -286,7 +288,7 @@ a2s(){
local mode="$3" local mode="$3"
logger INFO "Querying '$ip:$qport' with mode '$mode'" logger INFO "Querying '$ip:$qport' with mode '$mode'"
local res local res
res=$(python3.13 "$query_helper" "$ip" "$qport" "$mode") res=$(python3 "$query_helper" "$ip" "$qport" "$mode")
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
res=$(try_fallback "$ip" "$qport" "$mode") res=$(try_fallback "$ip" "$qport" "$mode")
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
@@ -309,7 +311,7 @@ is_in_favs(){
find_id(){ find_id(){
local file="$default_steam_path/config/loginusers.vdf" local file="$default_steam_path/config/loginusers.vdf"
[[ ! -f $file ]] && return 1 [[ ! -f $file ]] && return 1
local res=$(python3.13 "$helpers_path/vdf2json.py" \ local res=$(python3 "$helpers_path/vdf2json.py" \
-i "$file" \ -i "$file" \
| jq -r '.users | jq -r '.users
|to_entries[] |to_entries[]
@@ -338,7 +340,7 @@ list_mods(){
name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp") name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp")
base_dir=$(basename $(readlink -f $game_dir/$symlink)) base_dir=$(basename $(readlink -f $game_dir/$symlink))
size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}') size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}')
size=$(python3.13 -c "n=($size/1024) +.005; print(round(n,4))") size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))")
LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size"
done | sort -k1) done | sort -k1)
# user may have manually pruned mods out-of-band # user may have manually pruned mods out-of-band
@@ -397,7 +399,6 @@ get_dist(){
logger INFO "Distance: $dist km" logger INFO "Distance: $dist km"
fi fi
} }
query_config(){ query_config(){
[[ -n $2 ]] && local key=$2 [[ -n $2 ]] && local key=$2
keys=( keys=(
@@ -423,7 +424,6 @@ query_config(){
echo "${!i}" echo "${!i}"
done done
} }
align_versions_file(){ align_versions_file(){
shift shift
local mod="$1" local mod="$1"
@@ -589,6 +589,9 @@ default_steam_path="$default_steam_path"
#Preferred Steam launch command (for Flatpak support) #Preferred Steam launch command (for Flatpak support)
preferred_client="$preferred_client" preferred_client="$preferred_client"
#DZGUI source path
src_path="$src_path"
END END
} }
format_version_url(){ format_version_url(){
@@ -642,7 +645,6 @@ download_new_version(){
return 1 return 1
fi fi
local version_url="$(format_version_url)" local version_url="$(format_version_url)"
local src_path="$(< "$_cache_src_path")"
mv "$src_path" "$src_path.old" mv "$src_path" "$src_path.old"
curl -Ls "$version_url" > "$src_path" curl -Ls "$version_url" > "$src_path"
rc=$? rc=$?
@@ -690,7 +692,6 @@ toggle(){
auto_install="1" auto_install="1"
else else
auto_install="" auto_install=""
rm "$versions_file"
fi fi
;; ;;
Toggle[[:space:]]debug[[:space:]]mode) Toggle[[:space:]]debug[[:space:]]mode)
@@ -700,6 +701,13 @@ toggle(){
debug="" debug=""
fi fi
;; ;;
Toggle[[:space:]]Steam/Flatpak)
if [[ $preferred_client == "steam" ]]; then
preferred_client="flatpak"
else
preferred_client="steam"
fi
;;
Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot) Toggle[[:space:]]DZGUI[[:space:]]fullscreen[[:space:]]boot)
if [[ $fullscreen == "true" ]]; then if [[ $fullscreen == "true" ]]; then
fullscreen="false" fullscreen="false"
@@ -798,9 +806,6 @@ update_config_val(){
local context="$1" local context="$1"
local value="$2" local value="$2"
case $1 in case $1 in
"Change client")
key="preferred_client"
;;
"Change player name") "Change player name")
key="name" key="name"
if [[ -z "${value// }" ]]; then if [[ -z "${value// }" ]]; then
@@ -826,6 +831,7 @@ update_config_val(){
declare -n nr=$key declare -n nr=$key
nr="$value" nr="$value"
update_config update_config
echo "Updated the key '$key' to '$value'"
return 80 return 80
} }
show_log(){ show_log(){
@@ -930,19 +936,13 @@ query_defunct(){
<<< "$result" awk '{print $2}' <<< "$result" awk '{print $2}'
} }
encode(){ encode(){
echo -n "$1" | md5sum | cut -c -8 echo "$1" | md5sum | cut -c -8
} }
compare(){ compare(){
local modlist="$@" local modlist="$@"
diff=$(comm -23 <(<<< "$modlist" sort -u) <(installed_mods | sort)) diff=$(comm -23 <(<<< "$modlist" sort -u) <(installed_mods | sort))
echo "$diff" 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(){ legacy_symlinks(){
logger INFO "Removing legacy symlinks" logger INFO "Removing legacy symlinks"
@@ -1033,10 +1033,7 @@ update_symlinks(){
if [[ ! -d "$workshop_dir" ]]; then if [[ ! -d "$workshop_dir" ]]; then
return return
fi fi
if [[ ! -d "$game_dir" ]]; then legacy_symlinks
return
fi
unlink_all
symlinks symlinks
} }
try_fallback(){ try_fallback(){
@@ -1087,42 +1084,41 @@ try_connect(){
local appid="$2" local appid="$2"
local binary="$3" local binary="$3"
IFS=":" read -ra address <<< "$record" local ip=$(<<< $record awk -F: '{print $1}')
local ip local gameport=$(<<< $record awk -F: '{print $2}')
local gameport local qport=$(<<< $record awk -F: '{print $3}')
local qport
ip=${address[0]}
gameport=${address[1]}
qport=${address[2]}
[[ $appid -eq $exp ]] && echo "$binary" > "$_cache_binary" [[ $appid -eq $exp ]] && echo "$binary" > $_cache_binary
local remote_mods local remote_mods
remote_mods=$(a2s "$ip" "$qport" rules) remote_mods=$(a2s $ip $qport rules)
if [[ $? -eq 1 ]]; then if [[ $? -eq 1 ]]; then
printf "Failed to fetch server modlist, possibly timed out" printf "Failed to fetch server modlist, possibly timed out"
return 1 return 1
fi fi
logger INFO "Server returned modlist: $(<<< "$remote_mods" tr '\n' ' ')" logger INFO "Server returned modlist: $(<<< $remote_mods tr '\n' ' ')"
local sanitized_mods=$(query_defunct "$remote_mods")
local sanitized_mods local diff=$(compare "$sanitized_mods")
sanitized_mods=$(query_defunct "$remote_mods")
logger INFO "Connection attempt for $ip:$qport" logger INFO "Connection attempt for $ip:$qport"
update_history "$record" update_history "$record"
case $auto_install in if [[ -n $auto_install ]]; then
"") install_mods "$sanitized_mods" "manual" ;; logger INFO "Merging modlists"
1|2) install_mods "$sanitized_mods" "auto" ;; diff=$(merge_modlists "$diff")
esac diff=$(query_defunct "$diff")
fi
# NOTE: clean diff indicates that all requisite mods were downloaded if [[ -n $diff ]]; then
diff=$(compare "$sanitized_mods") if [[ $(check_architecture) -eq 1 ]] && [[ $(test_display_mode) == "gm" ]]; then
if [[ -z $diff ]]; then printf "Use Desktop Mode to download mods on Steam Deck"
launch "$ip" "$gameport" "$sanitized_mods" "$appid" return 1
fi
case $auto_install in
"") manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "" "$appid";;
1|2) manual_mod_install "$ip" "$gameport" "$diff" "$sanitized_mods" "auto" "$appid" ;;
esac
else else
printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync." launch "$ip" "$gameport" "$sanitized_mods" "$appid"
exit 1
fi fi
} }
check_architecture(){ check_architecture(){
@@ -1138,15 +1134,13 @@ force_update(){
printf "Only available when mod auto-install is ON" printf "Only available when mod auto-install is ON"
return 1 return 1
fi fi
# NOTE: forcibly regenerate signatures by removing file
rm "$versions_file" rm "$versions_file"
local mods local update=$(check_timestamps)
mods=$(ls -1 "$workshop_dir") manual_mod_install "null" "null" "$update" "null" "force" "null"
install_mods "$mods" "force"
echo "Finished requesting mod updates." echo "Finished requesting mod updates."
return 0 return 0
} }
get_timestamps(){ get_local_stamps(){
readarray -t modlist < <(printf "%s\n" "$@") readarray -t modlist < <(printf "%s\n" "$@")
local max="${#modlist[@]}" local max="${#modlist[@]}"
_concat(){ _concat(){
@@ -1171,6 +1165,67 @@ update_stamps(){
printf "%s\n" "${stamps[$i]}" >> $versions_file printf "%s\n" "${stamps[$i]}" >> $versions_file
done done
} }
check_timestamps(){
readarray -t local_modlist < <(ls -1 $workshop_dir)
local max=${#local_modlist[@]}
logger INFO "Local mod count: $max"
[[ $max -eq 0 ]] && return 1
local local_stamps=$(get_local_stamps "${local_modlist[@]}")
if [[ -z $local_stamps ]]; then
logger WARN "Timestamp query returned empty response"
return 1
fi
local aligned=$(<<< "$local_stamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"')
readarray -t remote_ids < <(<<< "$aligned" awk -F, '{print $1}')
readarray -t remote_times < <(<<< "$aligned" awk -F, '{print $2}')
if [[ ! -f $versions_file ]]; then
logger INFO "No prior versions file found, creating"
update_stamps "$aligned"
#force refresh all mods if versions file was missing
printf "%s\n" "${remote_ids[@]}"
return 0
fi
readarray -t old_ids < <(< $versions_file awk -F, '{print $1}')
readarray -t old_times < <(< $versions_file awk -F, '{print $2}')
declare -A remote_version
declare -A local_version
for((i = 0; i < ${#remote_ids[@]}; ++i)); do
remote_version[${remote_ids[$i]}]=${remote_times[$i]}
done
needs_update=()
for((i=0;i<${#old_ids[@]};i++)); do
local id=${old_ids[$i]}
local time=${old_times[$i]}
if [[ $time != ${remote_version[$id]} ]]; then
logger WARN "Mod '$id' timestamp '$time' != '${remote_version[$id]}'"
needs_update+=($id)
fi
done
printf "%s\n" "${needs_update[@]}"
}
merge_modlists(){
local diff="$1"
_sort(){
printf "%s\n" "$diff"
printf "%s\n" "${needs_update[@]}"
}
readarray -t needs_update < <(check_timestamps)
if [[ -z ${needs_update[@]} ]]; then
echo "$diff"
return 0
fi
if [[ -z "$diff" ]] && [[ ${#needs_update[@]} -gt 0 ]]; then
printf "%s\n" "${needs_update[@]}"
else
# remove duplicates
_sort | sort -u
fi
}
concat_mods(){ concat_mods(){
readarray -t concat_arr <<< "$@" readarray -t concat_arr <<< "$@"
local id local id
@@ -1212,7 +1267,7 @@ launch(){
if [[ $debug -eq 1 ]]; then if [[ $debug -eq 1 ]]; then
local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat" local launch_options="$steam_cmd -applaunch $appid -connect=$ip:$gameport -nolauncher -nosplash -name=$name -skipintro -mod=$concat"
printf "Debug mode: these options would have been used to launch the game: %s" "$launch_options" printf "Debug mode: these options would have been used to launch the game: $launch_options"
return 0 return 0
fi fi
echo "$concat" > "$_cache_launch" echo "$concat" > "$_cache_launch"
@@ -1248,121 +1303,75 @@ final_handshake(){
printf "\n" printf "\n"
return 6 return 6
} }
watcher(){ manual_mod_install(){
local -n to_download="$1" local ip="$1"
local -n signatures="$2" local gameport="$2"
local mode="$3" local diff="$3"
local sanitized_mods="$4"
declare -i i=0 local mode="$5"
local remote_time local appid="$6"
local length
length=${#to_download[@]}
for mod in "${!to_download[@]}"; do
remote_time=${to_download[$mod]}
[[ -f $ex ]] && return 1
case $mode in
"auto" | "force")
$steam_cmd "steam://url/CommunityFilePage/${mod}+workshop_download_item ${aid} ${mod}"
echo "# Opening workshop page for ${mod}. If you see no progress, you may be out of disk space."
;;
*)
$steam_cmd "steam://url/CommunityFilePage/${mod}"
echo "# Opening workshop page for ${mod}. If you see no progress after subscribing, try unsubscribing and resubscribing again until the download commences."
;;
esac
sleep 1s
foreground
until [[ -d $downloads_dir/${mod} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
# NOTE: fallback for instantaneous downloads
if [[ $mode == "auto" || $mode == "force" ]]; then
if [[ -d $workshop_dir/${mod} ]]; then
break
fi
fi
done
foreground
case $mode in
"auto" | "force")
echo "# Steam is downloading ${mod} (mod $((i+1)) of ${length}). You do not need to manually Subscribe."
;;
*)
echo "# Steam is downloading ${mod} (mod $((i+1)) of ${length})"
;;
esac
until [[ -d $workshop_dir/${mod} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
done
foreground
if [[ ! -v signatures["${mod}"] ]]; then
echo "$mod,$remote_time" >> "$versions_file"
else
sed -i "s/$mod,${signatures[$mod]}/$mod,$remote_time/" "$versions_file"
fi
echo "# ${mod} moved to mods dir"
i+=1
done
echo "100"
}
install_mods(){
local sanitized_mods="$1"
local mode="$2"
local ex="$state_path/dzg.watcher" local ex="$state_path/dzg.watcher"
[[ -f $ex ]] && rm "$ex" readarray -t stage_mods <<< "$diff"
[[ -f $ex ]] && rm $ex
if [[ -f "$versions_file" ]]; then _watcher(){
readarray -t local_mods < "$versions_file" for((i=0;i<${#stage_mods[@]};i++)); do
else [[ -f $ex ]] && return 1
local_mods=()
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}+workshop_download_item $aid ${stage_mods[$i]}"
echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress, you may be out of disk space."
else
$steam_cmd "steam://url/CommunityFilePage/${stage_mods[$i]}"
echo "# Opening workshop page for ${stage_mods[$i]}. If you see no progress after subscribing, try unsubscribing and resubscribing again until the download commences."
fi
sleep 1s
foreground
until [[ -d $downloads_dir/${stage_mods[$i]} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
if [[ -d $workshop_dir/${stage_mods[$i]} ]]; then
break
fi
done
foreground
if [[ $mode == "auto" ]] || [[ $mode == "force" ]]; then
echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]}). You do not need to manually Subscribe."
else
echo "# Steam is downloading ${stage_mods[$i]} (mod $((i+1)) of ${#stage_mods[@]})"
fi
until [[ -d $workshop_dir/${stage_mods[$i]} ]]; do
[[ -f $ex ]] && return 1
sleep 0.1s
done
foreground
echo "# ${stage_mods[$i]} moved to mods dir"
done
echo "100"
}
_watcher > >($steamsafe_zenity --pulsate --progress --auto-close --title="DZG Watcher" --width=500 2>/dev/null; rc=$?; [[ $rc -eq 1 ]] && touch $ex)
if [[ $mode == "force" ]]; then
rm "$versions_file"
check_timestamps
return 0
fi fi
declare -A mod_signatures local diff=$(compare "$sanitized_mods")
local local_id if [[ -z $diff ]]; then
local local_time if [[ $mode == "auto" ]]; then
for mod in "${local_mods[@]}"; do rm "$versions_file"
IFS="," read -ra fields <<< "$mod" check_timestamps
local_id="${fields[0]}"
local_time="${fields[1]}"
mod_signatures[$local_id]=$local_time
done
readarray -t remote_mods <<< "$sanitized_mods"
declare -A download
local remote_timestamps
local remote_id
local remote_time
remote_timestamps=$(get_timestamps "${remote_mods[@]}")
for mod in $(<<< "$remote_timestamps" jq -r '.response.publishedfiledetails[]|"\(.publishedfileid),\(.time_updated)"'); do
IFS="," read -ra fields <<< "$mod"
remote_id="${fields[0]}"
remote_time="${fields[1]}"
# NOTE: mod is not present in versions file
if [[ ! -v mod_signatures["$remote_id"] ]]; then
download["$remote_id"]=$remote_time
continue
fi fi
# NOTE: mod timestamp is out of date launch "$ip" "$gameport" "$sanitized_mods" "$appid"
local_time="${mod_signatures["$remote_id"]}" else
if [[ $remote_time != "$local_time" ]]; then printf "User aborted download process, or some mods may have failed to download. Try connecting again to resync."
download["$remote_id"]=$remote_time
continue
fi
done
[[ ${#download[@]} -lt 1 ]] && return
if [[ $(check_architecture) -eq 1 ]] && [[ $(test_display_mode) == "gm" ]]; then
printf "Use Desktop Mode to download mods on Steam Deck"
exit 1 exit 1
fi fi
watcher download mod_signatures "$mode" > >($steamsafe_zenity --pulsate --progress --auto-close --title="DZG Watcher" --width=500 2>/dev/null; rc=$?; [[ $rc -eq 1 ]] && touch "$ex")
} }
test_display_mode(){ test_display_mode(){
pgrep -a gamescope | grep -q "generate-drm-mode" pgrep -a gamescope | grep -q "generate-drm-mode"
+1 -1
View File
@@ -295,7 +295,7 @@ def get_dayz_version(file: Path) -> DayZVersion | Exception:
try: try:
version = get_version(file) version = get_version(file)
except Exception as e: except Exception as e:
raise e return e
patch = str(version.build) + str(version.revision) patch = str(version.build) + str(version.revision)
dz_vers = DayZVersion(version.major, version.minor, int(patch)) dz_vers = DayZVersion(version.major, version.minor, int(patch))
return dz_vers return dz_vers
+276 -181
View File
@@ -19,7 +19,7 @@ from collections.abc import Callable
from concurrent.futures import wait from concurrent.futures import wait
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from typing import Literal, Self, Any, TYPE_CHECKING from typing import Literal, Self, Any
import servers as Servers # noqa E402 import servers as Servers # noqa E402
import pefile as PeFile # noqa E402 import pefile as PeFile # noqa E402
@@ -32,16 +32,12 @@ from pefile import (
) )
from pefile import VersionMatch from pefile import VersionMatch
if TYPE_CHECKING:
from servers import Prereqs
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
import gi # noqa E402 import gi # noqa E402
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
gi.require_version("GLibUnix", "2.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
from gi.repository import Gtk, GLib, GLibUnix, Gdk, GObject, Pango # noqa E402
# https://bugzilla.gnome.org/show_bug.cgi?id=708676 # https://bugzilla.gnome.org/show_bug.cgi?id=708676
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
@@ -854,7 +850,9 @@ def call_out(command: str, *args: str) -> subprocess.CompletedProcess:
arg_ar = [] arg_ar = []
for i in args: for i in args:
arg_ar.append(i) 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( proc = subprocess.run(
["/usr/bin/env", "bash", funcs, command] + arg_ar, ["/usr/bin/env", "bash", funcs, command] + arg_ar,
capture_output=True, capture_output=True,
@@ -868,19 +866,20 @@ def spawn_dialog(msg: str, mode: Popup) -> bool:
dialog = GenericDialog(msg, mode) dialog = GenericDialog(msg, mode)
response = dialog.run() response = dialog.run()
dialog.destroy() dialog.destroy()
clean_msg = msg.replace("\n", " ")
match response: match response:
case Gtk.ResponseType.OK: case Gtk.ResponseType.OK:
logger.info(f"User confirmed dialog with message '{clean_msg}'") logger.info(f"User confirmed dialog with message '{msg}'")
return False return False
case Gtk.ResponseType.CANCEL | Gtk.ResponseType.DELETE_EVENT: case Gtk.ResponseType.CANCEL | Gtk.ResponseType.DELETE_EVENT:
logger.info(f"User aborted dialog with message '{clean_msg}'") logger.info(f"User aborted dialog with message '{msg}'")
return True return True
return False 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( logger.info(
f"Processing return code '{code}' for the input " f"Processing return code '{code}' for the input "
f"'{original_input}', returned message '{msg}'" f"'{original_input}', returned message '{msg}'"
@@ -907,7 +906,9 @@ def process_shell_return_code(msg: str, code: int, original_input: RowType) -> N
spawn_dialog("Invalid Steam API key, reverting", Popup.NOTIFY) spawn_dialog("Invalid Steam API key, reverting", Popup.NOTIFY)
App.notebook.settings.revert(Preferences.STEAM) App.notebook.settings.revert(Preferences.STEAM)
case 79: # failed settings update (bm) 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) App.notebook.settings.revert(Preferences.BM)
case 80: # pop toast after successful settings change case 80: # pop toast after successful settings change
config_vals.clear() config_vals.clear()
@@ -973,7 +974,9 @@ def call_on_thread(
if state: if state:
wait_dialog = GenericDialog(msg, Popup.WAIT) wait_dialog = GenericDialog(msg, Popup.WAIT)
wait_dialog.show_all() 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() thread.start()
else: else:
""" """
@@ -998,14 +1001,18 @@ def process_tree_option(choice: RowType) -> None:
record = App.treeview.get_record_string() record = App.treeview.get_record_string()
wait_msg = command.dict["wait_msg"] wait_msg = command.dict["wait_msg"]
show_wait_dialog = True 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 return
# modlist highlight stale action # modlist highlight stale action
if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT: if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT:
wait_msg = command.dict["wait_msg"] wait_msg = command.dict["wait_msg"]
show_wait_dialog = True 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 return
if command == RowType.CHANGELOG: if command == RowType.CHANGELOG:
@@ -1019,7 +1026,13 @@ def process_tree_option(choice: RowType) -> None:
return return
record = str_to_record(record) record = str_to_record(record)
prepare_connection(command, record) thread_new_with_dialog(
App.treeview.prepare_connection,
parse_shell_output,
"Querying server",
command,
[record],
)
return return
match command.dict["type"]: match command.dict["type"]:
@@ -1047,50 +1060,48 @@ def parse_shell_output(proc: subprocess.CompletedProcess, row: RowType):
process_shell_return_code(msg, proc.returncode, row) process_shell_return_code(msg, proc.returncode, row)
def prepare_connection(rowtype: RowType, record: Record) -> None: def thread_new_with_dialog(
def background(rowtype: RowType, record: Record) -> None: func: Callable,
def cleanup() -> None: callback: Callable | None,
App.treeview.wait_dialog.destroy() msg: str,
# NOTE: When using RowType.CONN_BY_IP, the gameport needs to be interpolated row: RowType | None,
record.gameport = prereqs.gameport args: list,
addr = record_to_str(record) ) -> None:
"""
Pop a GenericDialog transient to App.treeview and
call a function on a thread, with optional callback.
Chiefly used for connection-related subprocesses.
if proceed is False: After completion, the dialog is destroyed in the main event loop
spawn_dialog(msg, Popup.NOTIFY) and additional exception handling occurs.
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)
proceed, msg, pefile_path, prereqs = App.treeview.get_prereqs(record) This is intended as a bridge between legacy shell methods and the UI.
GLib.idle_add(cleanup) A more abstracted version of call_on_thread() for when extra threaded
processing occurs before calls to shell subprocesses.
"""
msg = "Checking prerequisites" def background(*args):
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(): def cleanup():
App.treeview.dialog_hide() App.treeview.dialog_hide()
parse_shell_output(proc, row) 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)
exception = None
proc = None
try: try:
proc = call_out("try_connect", addr, appid, path) proc = func(*args)
except Exception as e: except Exception as e:
logger.critical(e) exception = e
GLib.idle_add(cleanup) GLib.idle_add(cleanup)
return return
GLib.idle_add(cleanup) GLib.idle_add(cleanup)
msg = "Querying server"
App.treeview.dialog_show(msg) App.treeview.dialog_show(msg)
thread = threading.Thread(target=background, args=(addr, appid, path)) thread = threading.Thread(target=background, args=(args))
thread.start() thread.start()
@@ -1130,32 +1141,27 @@ def record_to_str(record: Record) -> str:
def connect_by_ip(enum: RowType, response: str) -> None: def connect_by_ip(enum: RowType, response: str) -> None:
try: def _prep(response: str) -> None:
record = Servers.validate_ip(response) record = Servers.validate_ip(response)
except Exception as e: proc = App.treeview.prepare_connection(record)
spawn_dialog(str(e), Popup.NOTIFY) return proc
return
prepare_connection(enum, record) thread_new_with_dialog(
_prep, parse_shell_output, "Querying IP", enum, [response]
)
return
def connect_by_id(enum: RowType, response: str, key: str) -> None: def connect_by_id(enum: RowType, response: str, key: str) -> None:
def _prep(key: str, response: str) -> None: def _prep(key: str, response: str) -> None:
def cleanup() -> None: record = Servers.query_bm_api(key, response)
App.treeview.dialog_hide() proc = App.treeview.prepare_connection(record)
prepare_connection(enum, record) return proc
try: thread_new_with_dialog(
record = Servers.query_bm_api(key, response) _prep, parse_shell_output, "Querying API", enum, [key, response]
except Exception as e: )
spawn_dialog(str(e), Popup.NOTIFY) return
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: def process_user_input(enum: RowType) -> None:
@@ -1166,7 +1172,9 @@ def process_user_input(enum: RowType) -> None:
if enum == RowType.CONN_BY_ID: if enum == RowType.CONN_BY_ID:
key = query_config("api_key")[0] key = query_config("api_key")[0]
if len(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 return
user_entry = EntryDialog(prompt, Popup.ENTRY, link_label) user_entry = EntryDialog(prompt, Popup.ENTRY, link_label)
@@ -1187,7 +1195,9 @@ def process_user_input(enum: RowType) -> None:
show_wait_dialog = True show_wait_dialog = True
wait_msg = "Working" 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 return
@@ -1275,7 +1285,9 @@ class OuterWindow(Gtk.Window):
logger.info(f"Using default window size {w},{h}") logger.info(f"Using default window size {w},{h}")
self.set_default_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() self.halt_proc_and_quit()
def halt_proc_and_quit(self) -> None: def halt_proc_and_quit(self) -> None:
@@ -1342,7 +1354,8 @@ class RightPanel(Gtk.Box):
self.pack_start(self.filters_vbox, False, False, 0) self.pack_start(self.filters_vbox, False, False, 0)
debug_tooltip = ( 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 = ( ping_tooltip = (
"Refresh the ping for visible servers.\n" "Refresh the ping for visible servers.\n"
@@ -1411,7 +1424,8 @@ class RightPanel(Gtk.Box):
rows = ModelManager.get_filtered() rows = ModelManager.get_filtered()
with ThreadPoolExecutor(100) as executor: with ThreadPoolExecutor(100) as executor:
futures = [ 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) wait(futures)
for future in futures: for future in futures:
@@ -1472,8 +1486,6 @@ class ButtonBox(Gtk.Box):
context = button.type context = button.type
logger.info(f"User clicked '{context}'") logger.info(f"User clicked '{context}'")
App.grid.right_panel.filters_vbox.set_visible(False)
if context == ButtonType.EXIT: if context == ButtonType.EXIT:
logger.info("Normal user exit") logger.info("Normal user exit")
save_res_and_quit() save_res_and_quit()
@@ -1787,7 +1799,9 @@ class ModelManagerSingleton:
class TreeView(Gtk.TreeView): 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): def __init__(self):
super().__init__() super().__init__()
@@ -1906,7 +1920,9 @@ class TreeView(Gtk.TreeView):
if self.current_proc and self.current_proc.is_alive(): if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate() 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() box = button.get_parent()
dialog = box.get_parent() dialog = box.get_parent()
try: try:
@@ -1998,7 +2014,9 @@ class TreeView(Gtk.TreeView):
model.remove(it) model.remove(it)
block_signals() block_signals()
thread = threading.Thread(target=ModelManager.resync_model, args=(addr, qport)) thread = threading.Thread(
target=ModelManager.resync_model, args=(addr, qport)
)
thread.start() thread.start()
def remove_from_history(self) -> None: def remove_from_history(self) -> None:
@@ -2022,12 +2040,16 @@ class TreeView(Gtk.TreeView):
if hasattr(TreeView, menu_item.action): if hasattr(TreeView, menu_item.action):
func = getattr(TreeView, menu_item.action) func = getattr(TreeView, menu_item.action)
msg = ( 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) logger.info(msg)
func(self) func(self)
else: 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 = ( u_msg = (
f"Something went wrong when accessing the method " f"Something went wrong when accessing the method "
f"'{menu_item.action}'" f"'{menu_item.action}'"
@@ -2068,7 +2090,9 @@ class TreeView(Gtk.TreeView):
return True return True
return False 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: if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3:
return return
@@ -2152,7 +2176,9 @@ class TreeView(Gtk.TreeView):
if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l: if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l:
if self.is_selection_empty(): if self.is_selection_empty():
return 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: else:
self.menu.popup_at_pointer(event) self.menu.popup_at_pointer(event)
self.menu.select_first(False) self.menu.select_first(False)
@@ -2166,7 +2192,9 @@ class TreeView(Gtk.TreeView):
return None return None
call_out("start_cooldown", "", "") call_out("start_cooldown", "", "")
thread = threading.Thread(target=self._background_player_count, args=()) thread = threading.Thread(
target=self._background_player_count, args=()
)
thread.start() thread.start()
def get_current_iter(self) -> Gtk.TreeIter | None: def get_current_iter(self) -> Gtk.TreeIter | None:
@@ -2181,7 +2209,9 @@ class TreeView(Gtk.TreeView):
context = App.treeview.get_subpage_label() context = App.treeview.get_subpage_label()
row_sel = self.get_value_at_index(0) 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(): if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate() self.current_proc.terminate()
@@ -2239,7 +2269,9 @@ class TreeView(Gtk.TreeView):
self.set_cursor(path) self.set_cursor(path)
return None 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) keyname = Gdk.keyval_name(event.keyval)
grid = App.grid grid = App.grid
if event.state is Gdk.ModifierType.CONTROL_MASK: if event.state is Gdk.ModifierType.CONTROL_MASK:
@@ -2294,7 +2326,9 @@ class TreeView(Gtk.TreeView):
return False return False
return None 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 Suppresses spamming on keydown
""" """
@@ -2409,7 +2443,10 @@ class TreeView(Gtk.TreeView):
params = Servers.params params = Servers.params
serv = [] serv = []
with ThreadPoolExecutor() as executor: 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) wait(futures)
for future in futures: for future in futures:
res = future.result() res = future.result()
@@ -2431,7 +2468,10 @@ class TreeView(Gtk.TreeView):
def _dump_lan(self, port: int) -> list | None: def _dump_lan(self, port: int) -> list | None:
with ThreadPoolExecutor() as executor: 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) wait(futures)
servers = [] servers = []
for future in futures: for future in futures:
@@ -2450,8 +2490,6 @@ class TreeView(Gtk.TreeView):
def _dump_servers(self, ips: list) -> list | None: def _dump_servers(self, ips: list) -> list | None:
if len(ips) == 0: if len(ips) == 0:
return [] return []
# NOTE: block malformed records
ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""]
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [ futures = [
executor.submit( executor.submit(
@@ -2556,7 +2594,9 @@ class TreeView(Gtk.TreeView):
ModelManager.set_store(App.treeview.get_model()) ModelManager.set_store(App.treeview.get_model())
App.treeview.set_model(None) 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() thread.start()
def _background_quad(self, dialog: "GenericDialog", mode: RowType) -> None: def _background_quad(self, dialog: "GenericDialog", mode: RowType) -> None:
@@ -2715,9 +2755,13 @@ class TreeView(Gtk.TreeView):
self.initialize_columns() self.initialize_columns()
self.set_selection_mode(Gtk.SelectionMode.SINGLE) 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() 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() thread.start()
def _format_float( def _format_float(
@@ -2896,7 +2940,9 @@ class TreeView(Gtk.TreeView):
def get_view(self): def get_view(self):
return self.view return self.view
def get_prereqs(self, record: Record) -> tuple[bool, str, str | None, "Prereqs"]: def prepare_connection(
self, record: Record
) -> subprocess.CompletedProcess | None:
""" """
Always called on a thread with a dialog on the transient parent window Always called on a thread with a dialog on the transient parent window
""" """
@@ -2904,39 +2950,50 @@ class TreeView(Gtk.TreeView):
if prereqs.appid is None: if prereqs.appid is None:
logger.warning(f"Query to '{record.ip}:{record.qport}' timed out") logger.warning(f"Query to '{record.ip}:{record.qport}' timed out")
msg = "Timed out when querying server, check IP or try again later" msg = "Timed out when querying server, check IP or try again later"
return (False, msg, None, prereqs) spawn_dialog(msg, Popup.NOTIFY)
return None
build = "DayZ" if prereqs.appid == APPID_DAYZ else "DayZ Experimental" build = "DayZ" if prereqs.appid == APPID_DAYZ else "DayZ Experimental"
steam_path = query_config("default_steam_path")[0] steam_path = query_config("default_steam_path")[0]
if len(steam_path) < 1: 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." msg = "Local Steam installation is not set, possibly malformed config file."
return (False, msg, None, prereqs) spawn_dialog(msg, Popup.NOTIFY)
return None
try: try:
pefile_path = PeFile.get_pefile_path(steam_path, prereqs.appid) pefile_path = PeFile.get_pefile_path(steam_path, prereqs.appid)
except AppNotInstalledError: 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 = ( msg = (
f"This server is running {build}. You can install " f"This server is running {build}. You can install "
f"{build} by searching for it in your Steam library. " f"{build} by searching for it in your Steam library. "
f"If you recently installed {build} or moved it to a different drive, " f"If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again." "restart Steam to allow these changes to synchronize, then try again."
) )
return (False, msg, None, prereqs) spawn_dialog(msg, Popup.NOTIFY)
return None
except AppMovedError: except AppMovedError:
logger.critical(f"Library folder synch error for '{prereqs.appid}'") logger.critical(
f"Library folder synch error for '{prereqs.appid}'"
)
msg = ( msg = (
f"Steam is reporting that {build} is installed at a non-existent location. " 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, " f"If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again." "restart Steam to allow these changes to synchronize, then try again."
) )
return (False, msg, None, prereqs) spawn_dialog(msg, Popup.NOTIFY)
return None
except (VDFLoadError, PeFileError, Exception) as e: except (VDFLoadError, PeFileError, Exception) as e:
logger.critical(e) logger.critical(e)
msg = "Steam settings or DayZ installation may be corrupted. Try restarting Steam." msg = "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
return (False, msg, None, prereqs) spawn_dialog(msg, Popup.NOTIFY)
return None
try: try:
local_vers = PeFile.get_dayz_version(pefile_path) local_vers = PeFile.get_dayz_version(pefile_path)
@@ -2948,7 +3005,7 @@ class TreeView(Gtk.TreeView):
local_vers = None local_vers = None
try: try:
remote_vers = PeFile.dayz_version_from_str(prereqs) remote_vers = PeFile.dayz_version_from_str(prereqs.version)
except Exception: except Exception:
remote_vers = None remote_vers = None
@@ -2961,15 +3018,18 @@ class TreeView(Gtk.TreeView):
f"This server is running a newer build ({prereqs.version}) of {build} than " 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?" f"your local version. You may be unable to connect. Proceed anyway?"
) )
return (True, msg, pefile_path, prereqs) res = spawn_dialog(msg, Popup.CONFIRM)
if res is True:
return None
case VersionMatch.LOCAL_NEWER: case VersionMatch.LOCAL_NEWER:
msg = ( msg = (
f"This server is running an out-of-date build ({prereqs.version}) of {build}. " f"This server is running an out-of-date build ({prereqs.version}) of {build}. "
"You may be unable to connect. Proceed anyway?" "You may be unable to connect. Proceed anyway?"
) )
return (True, msg, pefile_path, prereqs) res = spawn_dialog(msg, Popup.CONFIRM)
if res is True:
return None
case VersionMatch.SAME_VERSION: case VersionMatch.SAME_VERSION:
return (True, "", pefile_path, prereqs)
pass pass
if prereqs.password is True: if prereqs.password is True:
@@ -2977,9 +3037,19 @@ class TreeView(Gtk.TreeView):
"This server is password-protected and you will be " "This server is password-protected and you will be "
"prompted when connecting. Do you want to proceed?" "prompted when connecting. Do you want to proceed?"
) )
return (True, msg, pefile_path, prereqs) res = spawn_dialog(msg, Popup.CONFIRM)
if res is True:
return None
return (True, "", 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
@signal_emission @signal_emission
@update_window_labels @update_window_labels
@@ -3049,7 +3119,13 @@ class TreeView(Gtk.TreeView):
record = self.get_record() record = self.get_record()
if record is None: if record is None:
return return
prepare_connection(None, record) thread_new_with_dialog(
self.prepare_connection,
parse_shell_output,
"Querying server",
None,
[record],
)
case _: # any other non-server option from the main menu case _: # any other non-server option from the main menu
process_tree_option(output) process_tree_option(output)
@@ -3134,7 +3210,7 @@ class GenericDialog(Gtk.MessageDialog):
self.outer.set_margin_end(30) self.outer.set_margin_end(30)
def _on_dialog_delete( def _on_dialog_delete(
self, response_id: Gtk.ResponseType, event: Gdk.Event self, response_id: Gtk.ResponseType, event: Gdk.Event
) -> Literal[True]: ) -> Literal[True]:
""" """
Prevent manual dialog destruction Prevent manual dialog destruction
@@ -3213,7 +3289,9 @@ class LanDialog(Gtk.MessageDialog):
self.connect("delete-event", self.restore_context) self.connect("delete-event", self.restore_context)
self.entry.connect("insert-text", self._on_text_typed) 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: def _validate(self, text: str) -> None:
if self._is_invalid(text): if self._is_invalid(text):
@@ -3329,7 +3407,9 @@ class DetailsDialog(GenericDialog):
for i, column_title in enumerate(["Item", "Details"]): for i, column_title in enumerate(["Item", "Details"]):
renderer = Gtk.CellRendererText(xalign=0) renderer = Gtk.CellRendererText(xalign=0)
if i == 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: else:
column = Gtk.TreeViewColumn(column_title, renderer, text=i) column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
@@ -3345,8 +3425,12 @@ class DetailsDialog(GenericDialog):
scrollable_message = Gtk.ScrolledWindow() scrollable_message = Gtk.ScrolledWindow()
desc = Gtk.Label(label="Server message", valign=Gtk.Align.START) desc = Gtk.Label(label="Server message", valign=Gtk.Align.START)
add_class(desc, "details-heading") add_class(desc, "details-heading")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER) box = Gtk.Box(
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True) 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 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_bottom(10) sep.set_margin_bottom(10)
for el in desc, sep, self.description: for el in desc, sep, self.description:
@@ -3384,7 +3468,9 @@ class DetailsDialog(GenericDialog):
self.run() self.run()
self.destroy() 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) response = Servers.details(self.ip, self.qport)
if response.success: if response.success:
for row in response.data: for row in response.data:
@@ -3441,7 +3527,9 @@ class ModDialog(GenericDialog):
wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT) wait_dialog = GenericDialog("Fetching modlist", Popup.WAIT)
wait_dialog.show_all() 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() thread.start()
def _background(self, dialog: "GenericDialog", record: str) -> None: def _background(self, dialog: "GenericDialog", record: str) -> None:
@@ -3466,7 +3554,9 @@ class ModDialog(GenericDialog):
self.view.set_model(modlist_store) self.view.set_model(modlist_store)
GLib.idle_add(_load) 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() lines = data.stdout.splitlines()
hits = len(lines) hits = len(lines)
reader = csv.reader(lines, delimiter=delimiter) reader = csv.reader(lines, delimiter=delimiter)
@@ -3496,7 +3586,9 @@ class ModDialog(GenericDialog):
class LinkDialog(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) super().__init__(text, Popup.NOTIFY)
text = textwrap.dedent(text) text = textwrap.dedent(text)
@@ -3709,27 +3801,12 @@ class Options(Gtk.Box):
"Always fullscreen", "Always fullscreen",
Preferences.WINDOW, Preferences.WINDOW,
) )
self.steam_toggle = self.make_binary_radio(
# TODO: gray out options if not available on system "Steam", "Flatpak (experimental)", Preferences.CLIENT
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",
]
) )
renderer_text = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
self.client_combo = Gtk.ComboBox.new_with_model(client_store)
self.client_combo.set_halign(Gtk.Align.START)
self.client_combo.pack_start(renderer_text, True)
self.client_combo.connect("changed", self._on_client_changed)
self.client_combo.add_attribute(renderer_text, "text", 0)
pref_rows = [ pref_rows = [
[LeftLabel("Steam client"), self.client_combo], [LeftLabel("Steam client"), self.steam_toggle],
[LeftLabel("Window size at boot"), self.fullscreen_toggle], [LeftLabel("Window size at boot"), self.fullscreen_toggle],
[LeftLabel("Player name"), self.player_box], [LeftLabel("Player name"), self.player_box],
] ]
@@ -3743,7 +3820,8 @@ class Options(Gtk.Box):
self.force_button.set_sensitive(False) self.force_button.set_sensitive(False)
msg = ( 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) eb = InfoEventBox(msg)
@@ -3802,7 +3880,9 @@ class Options(Gtk.Box):
self.make_frame(mods_grid, "Mods"), self.make_frame(mods_grid, "Mods"),
self.make_frame(version_grid, "Version"), 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 row += 1
self.scrollable = Gtk.ScrolledWindow(vexpand=True) self.scrollable = Gtk.ScrolledWindow(vexpand=True)
@@ -3848,7 +3928,9 @@ class Options(Gtk.Box):
return 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() text = entry.get_text()
button = entry.sibling button = entry.sibling
if not self._is_valid_text(text, context): if not self._is_valid_text(text, context):
@@ -3869,7 +3951,9 @@ class Options(Gtk.Box):
for record in rows: for record in rows:
col = 1 col = 1
for el in record: 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 col += 1
row += 1 row += 1
return grid return grid
@@ -3907,25 +3991,20 @@ class Options(Gtk.Box):
show_wait_dialog = True show_wait_dialog = True
call_on_thread(show_wait_dialog, cmd, wait_msg, "") call_on_thread(show_wait_dialog, cmd, wait_msg, "")
def _on_client_changed(self, combo: Gtk.ComboBox) -> None:
# prevent triggering on initial init
if App.treeview.subpage is not RowType.OPTIONS:
return
ind = combo.get_active()
mod = combo.get_model()
client = mod[ind][1]
call_bash_func("Change client", client)
def _on_branch_changed(self, combo: Gtk.ComboBoxText) -> None: def _on_branch_changed(self, combo: Gtk.ComboBoxText) -> None:
# prevent triggering on initial init # prevent triggering on initial init
if App.treeview.subpage is not RowType.OPTIONS: if App.treeview.subpage is not RowType.OPTIONS:
return return
process_toggle(RowType.TGL_BRANCH) process_toggle(RowType.TGL_BRANCH)
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None: def _on_radio_toggled(
if App.treeview.subpage is not RowType.OPTIONS: self, button: Gtk.RadioButton, context: Preferences
) -> None:
if App.treeview.subpage is None:
return return
match context: match context:
case Preferences.CLIENT:
toggle = RowType.TGL_STEAM
case Preferences.INSTALL: case Preferences.INSTALL:
toggle = RowType.TGL_INSTALL toggle = RowType.TGL_INSTALL
state = button.get_group()[0].get_active() state = button.get_group()[0].get_active()
@@ -4047,9 +4126,11 @@ class Options(Gtk.Box):
radio = 0 radio = 0
self.mod_install_toggle.get_children()[radio].set_active(True) self.mod_install_toggle.get_children()[radio].set_active(True)
for ind, row in enumerate(self.client_combo.get_model()): if client == "flatpak":
if row[1] == client: radio = 1
self.client_combo.set_active(ind) else:
radio = 0
self.steam_toggle.get_children()[radio].set_active(True)
if fullscreen == "true": if fullscreen == "true":
radio = 1 radio = 1
@@ -4066,14 +4147,18 @@ class Options(Gtk.Box):
field[1].get_children()[1].set_sensitive(False) field[1].get_children()[1].set_sensitive(False)
try: 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) vers = PeFile.get_dayz_version(pe_file_path)
dayz_version = PeFile.dayz_version_to_str(vers) dayz_version = PeFile.dayz_version_to_str(vers)
except Exception: except Exception:
dayz_version = "-" dayz_version = "-"
try: 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) vers = PeFile.get_dayz_version(exp_file_path)
dayz_exp_version = PeFile.dayz_version_to_str(vers) dayz_exp_version = PeFile.dayz_version_to_str(vers)
except Exception: except Exception:
@@ -4182,7 +4267,9 @@ class KeybindingsDialog(Gtk.Box):
if not sep: if not sep:
grid.attach(desc, col, row, w, h) grid.attach(desc, col, row, w, h)
else: 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 row += 1
sep = None sep = None
grid.attach_next_to(frame, desc, Gtk.PositionType.RIGHT, w, h) grid.attach_next_to(frame, desc, Gtk.PositionType.RIGHT, w, h)
@@ -4190,7 +4277,9 @@ class KeybindingsDialog(Gtk.Box):
l_spacer = Gtk.Label(label="") l_spacer = Gtk.Label(label="")
r_spacer = Gtk.Label(label="") r_spacer = Gtk.Label(label="")
grid.attach(l_spacer, col, row + 1, w, h) 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 row += 1
return grid return grid
@@ -4199,7 +4288,9 @@ class KeybindingsDialog(Gtk.Box):
col = 0 col = 0
w = 1 w = 1
h = 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: for cat in categories:
label = Gtk.Label(label=cat) label = Gtk.Label(label=cat)
add_class(label, "left-label") add_class(label, "left-label")
@@ -4219,7 +4310,9 @@ class KeybindingsDialog(Gtk.Box):
column = 1 column = 1
w = 1 w = 1
h = 1 h = 1
sidebar = self.build_sidebar(["Servers", "Navigation", "Vim-style keys"]) sidebar = self.build_sidebar(
["Servers", "Navigation", "Vim-style keys"]
)
separator = Gtk.Separator() separator = Gtk.Separator()
keys_box = self.build_keys(items) keys_box = self.build_keys(items)
@@ -4437,7 +4530,9 @@ class Statusbar(Gtk.Statusbar):
players_pretty = pluralize("players", players) players_pretty = pluralize("players", players)
hits_pretty = pluralize("matches", hits) 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..." suffix = "| Distance: calculating..."
if players == 0: if players == 0:
@@ -4489,7 +4584,9 @@ class Grid(Gtk.Grid):
self.breadcrumbs = Gtk.Label(label="Main menu", halign=Gtk.Align.START) self.breadcrumbs = Gtk.Label(label="Main menu", halign=Gtk.Align.START)
self.attach(self.notebook, 0, 0, 3, 1) 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.attach_next_to(
self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1
) )
@@ -4551,23 +4648,11 @@ class App(Gtk.Application):
) )
self.win.add_accel_group(accel) self.win.add_accel_group(accel)
self.setup_signals() GLib.unix_signal_add(
GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint
)
Gtk.main() 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: def _catch_sigint(self) -> None:
self.win.halt_proc_and_quit() self.win.halt_proc_and_quit()
@@ -4652,7 +4737,9 @@ class ModSelectionPanel(Gtk.Box):
def toggle_select_stale_button(self, state: bool) -> None: def toggle_select_stale_button(self, state: bool) -> None:
if state: 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" text = "Bulk selects all currently highlighted mods"
button.set_tooltip_text(text) button.set_tooltip_text(text)
button.connect("clicked", self._on_button_clicked) button.connect("clicked", self._on_button_clicked)
@@ -4772,7 +4859,9 @@ class FilterPanel(Gtk.Box):
self.keyword_entry = Gtk.Entry() self.keyword_entry = Gtk.Entry()
self.keyword_entry.set_placeholder_text("Filter by keyword") self.keyword_entry.set_placeholder_text("Filter by keyword")
self.keyword_entry.connect("activate", self._on_keyword_enter) 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 = Gtk.EntryCompletion(inline_completion=True)
completion.set_text_column(0) completion.set_text_column(0)
@@ -4840,7 +4929,9 @@ class FilterPanel(Gtk.Box):
state = self.default_filters[label] state = self.default_filters[label]
check.set_active(state) 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: match event.keyval:
case Gdk.KEY_Return: case Gdk.KEY_Return:
text = entry.get_text() text = entry.get_text()
@@ -4883,7 +4974,9 @@ class FilterPanel(Gtk.Box):
App.treeview.grab_focus() App.treeview.grab_focus()
return False 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: match event.keyval:
case Gdk.KEY_Up: case Gdk.KEY_Up:
return True return True
@@ -4894,7 +4987,9 @@ class FilterPanel(Gtk.Box):
return True return True
return False 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: match event.keyval:
case Gdk.KEY_Down: case Gdk.KEY_Down:
self.maps_combo.popup() self.maps_combo.popup()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB