Compare commits

...

24 Commits

Author SHA1 Message Date
aclist
adf8a62e8a
Merge pull request #238 from aclist/doc/port-contributing
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
Doc/port contributing
2025-10-30 11:50:23 +09:00
aclist
46a54cef12 docs: add contributing file 2025-10-30 11:47:10 +09:00
aclist
321384dc7b
Merge pull request #222 from aclist/release/5.8.0
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: release 5.8.0
2025-07-07 10:28:57 +09:00
aclist
e02f02b775 chore: update changelog
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2025-07-07 10:18:25 +09:00
aclist
de524bd2a5 chore: backports from testing 2025-07-06 12:37:29 +09:00
aclist
edee9b6234
Merge pull request #219 from aclist/feat/coc2
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: add code of conduct
2025-06-05 17:49:08 +09:00
aclist
cefd041e08 feat: add code of conduct 2025-06-05 17:48:38 +09:00
aclist
5ab18d0796
Merge pull request #214 from aclist/release/5.7.0
Release/5.7.0
2025-04-17 22:13:38 +09:00
aclist
23e3d34947 chore: update IP checksum 2025-04-17 21:52:36 +09:00
aclist
b86dd37b50 chore: bump minor version 2025-04-17 21:50:33 +09:00
aclist
59fdd30236 docs: update changelog 2025-04-17 21:49:35 +09:00
aclist
a35bfda5f1 fix: constrain window to viewport 2025-04-17 21:47:47 +09:00
aclist
5393bf1f5f fix: more robust ping method 2025-04-17 21:43:43 +09:00
aclist
90b3aa8663
Merge pull request #209 from aclist/aclist-patch-1
chore: update CHANGELOG.md
2025-04-05 09:15:56 +09:00
aclist
728e4daf7c
chore: update CHANGELOG.md 2025-04-05 09:15:23 +09:00
aclist
7a210c7f39
Merge pull request #208 from aclist/fix/test-connection
chore: drop pre-boot check
2025-04-05 09:08:35 +09:00
aclist
459dd69359 chore: bump version 2025-04-05 09:07:02 +09:00
aclist
fbd1a355d5 chore: drop pre-boot api check 2025-04-05 09:03:29 +09:00
aclist
82d2afb1f2
Merge pull request #207 from aclist/chore/2025-03-ipdb
chore: update ip records
2025-03-16 12:18:52 +09:00
aclist
8193980878 chore: update ip records 2025-03-16 12:17:41 +09:00
aclist
f75ef947c9
Merge pull request #204 from aclist/fix/batch-queries
fix: batch queries
2025-03-05 11:15:31 +09:00
aclist
6dc9a719ee fix: split up batch queries 2025-03-05 11:13:20 +09:00
aclist
785df28f66
Merge pull request #202 from aclist/chore/backports
chore: backports
2025-02-12 20:15:51 +09:00
aclist
66e114a400 chore: backports 2025-02-10 17:54:45 +09:00
5 changed files with 363 additions and 94 deletions

View File

@ -1,18 +1,51 @@
# Changelog # Changelog
## [5.6.6] 2024-02-09 ## [5.8.0] 2025-07-06
## Added
- Filter servers by official/unofficial status
- Automatically fetch geolocation records
## Changed
- Updated internal versioning of helper files
## Fixed
- Corrected erroneous 2024 date in prior changelog entries
## [5.7.0] 2025-04-17
## Added
- Save application dimensions when quitting and restore on subsequent boot
## Fixed
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
## Changed
- More direct server ping query method that should return marginally more accurate times
## [5.6.7] 2025-04-04
## Dropped
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
## [5.6.6] 2025-03-16
## Changed
- Update IP database records for 2025-03
## [5.6.5] 2025-03-04
## Fixed
- Livonia server results being dropped from batch queries
## [5.6.4] 2025-02-10
### Changed
- Drop launch flag and check for invocation through Steam automatically
## [5.6.3] 2025-02-09
### Changed ### Changed
- Update IP database records for 2025-02 - Update IP database records for 2025-02
## [5.6.2] 2024-01-22 ## [5.6.2] 2025-01-22
### Fixed ### Fixed
- Resolve regression introduced with IP resolution feature in 5.6.0 (restores functionality of right-click action: Add to My Servers) - Resolve regression introduced with IP resolution feature in 5.6.0 (restores functionality of right-click action: Add to My Servers)
## [5.6.1] 2024-01-10 ## [5.6.1] 2025-01-10
### Fixed ### Fixed
- Add fallback support for jq 1.6 - Add fallback support for jq 1.6
## [5.6.0] 2024-01-06 ## [5.6.0] 2025-01-06
### Added ### Added
- Application header bar and controls - Application header bar and controls
- Menu context subtitle in header bar - Menu context subtitle in header bar

71
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,71 @@
# Introduction
Thank you for your interest in DZGUI!
This guide goes over development conventions and best practices for contributors.
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
`troubleshooting` tag.
# How can I help the project?
There are several ways to help this project.
1. Report bugs that you find
2. Request features that you would like
3. Contribute features and fixes to the codebase
4. Contribute documentation to the project
Before making any contribution, please read the `CODE_OF_CONDUCT.md` and act
accordingly.
## Submitting a ticket
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:
- What version are you using?
- What distribution are you using?
- What is the issue that you found?
- How can we reproduce the issue?
You can also attach screenshots, logs, or other data that can help us.
## Requesting a feature
You can also request features via the same issue tracker. It is good practice to
first search for your idea to see if a similar one has already been posted.
If not, open a ticket where you describe your feature and its possible benefits.
Please note that this is a community project, so it takes time for us to develop
features. Putting in a feature request does not mean that it will be implemented,
but we will do our best to support as many cool ideas as possible.
## Contributing code or documentation
If you would like to take ownership or assist with an issue on the issue tracker, or
contribute a new change, please follow the guidelines below.
Fork this repository and check out the code up to `prerelease/<latest-version>`.
If there is no pending prerelease PR, you can base your changes off of
the `testing` branch. You may later be asked to retarget your PR to the prerelease
branch once a new one is made. This is because out-of-band PRs are consolidated into
the next release before merging.
The following naming conventions apply for PRs:
- fix/<your-fix> - patch/hotfix branches
- feat/<your-feature> - feature branches
- doc/<your-doc-branch> - documentation branches
- infra/<your-infra-branch> - infrastructure branches
Implement your changes and test them locally. If they work you may
open a merge request, and then we review your changes. If everything is OK, it
will be merged to `prerelease/<latest-version>`, then `testing`, and after a live
testing phase, to `stable` as well.
It is recommended to follow
(Conventional Commits)[https://www.conventionalcommits.org/en/v1.0.0/], as this
integrates well with tooling and helps the project review your code.

148
dzgui.sh
View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version=5.6.3 version=5.8.0
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@ -49,10 +49,9 @@ freedesktop_path="$HOME/.local/share/applications"
#HELPERS #HELPERS
ui_helper="$helpers_path/ui.py" ui_helper="$helpers_path/ui.py"
geo_file="$helpers_path/ips.csv"
km_helper="$helpers_path/latlon" km_helper="$helpers_path/latlon"
sums_path="$helpers_path/sums.md5"
func_helper="$helpers_path/funcs" func_helper="$helpers_path/funcs"
geo_helper="$helpers_path/ips.csv"
#REMOTE #REMOTE
remote_host=gh remote_host=gh
@ -63,7 +62,6 @@ stable_url="$url_prefix/dzgui"
testing_url="$url_prefix/testing" testing_url="$url_prefix/testing"
releases_url="https://github.com/$author/$repo/releases/download/browser" releases_url="https://github.com/$author/$repo/releases/download/browser"
km_helper_url="$releases_url/latlon" km_helper_url="$releases_url/latlon"
geo_file_url="$releases_url/ips.csv.gz"
set_im_module(){ set_im_module(){
@ -587,10 +585,10 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file" [[ -f "$config_file" ]] && source "$config_file"
declare -A sums declare -A sums
sums=( sums=(
["ui.py"]="5a876efacf208d12b5fe761996425412" ["ui.py"]="f128a97e744e9e11036d707198feb8a8"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["funcs"]="417bd5eaffbefc905a843985c691dc64" ["funcs"]="94287c75a5ae0a06a95dd439711e6fba"
["lan"]="c62e84ddd1457b71a85ad21da662b9af" ["lan"]="c62e84ddd1457b71a85ad21da662b9af"
) )
local author="aclist" local author="aclist"
@ -637,18 +635,8 @@ fetch_helpers_by_sum(){
done done
return 0 return 0
} }
fetch_geo_file(){ fetch_km_helper(){
# for binary releases
local geo_sum="9824e9b9a75a4830a2423932cc188b06"
local km_sum="b038fdb8f655798207bd28de3a004706" local km_sum="b038fdb8f655798207bd28de3a004706"
local gzip="$helpers_path/ips.csv.gz"
if [[ ! -f $geo_file ]] || [[ $(get_hash $geo_file) != $geo_sum ]]; then
local res=$(get_response_code "$geo_file_url")
[[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$geo_file_url'"
curl -Ls "$geo_file_url" > "$gzip"
#force overwrite
gunzip -f "$gzip"
fi
if [[ ! -f $km_helper ]] || [[ $(get_hash $km_helper) != $km_sum ]]; then if [[ ! -f $km_helper ]] || [[ $(get_hash $km_helper) != $km_sum ]]; then
local res=$(get_response_code "$km_helper_url") local res=$(get_response_code "$km_helper_url")
[[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$km_helper_url'" [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$km_helper_url'"
@ -656,10 +644,119 @@ fetch_geo_file(){
chmod +x "$km_helper" chmod +x "$km_helper"
fi fi
} }
get_response_code(){
local url="$1"
curl -Ls -I -o /dev/null -w "%{http_code}" "$url"
}
raise_error_and_quit(){
echo "$1"
exit 1
}
fetch_ip_db(){
parse_dl_url(){
curl -Ls "$url" \
| grep "csv.gz" \
| awk -F"['']" '{print $2}'
}
parse_dl_url_date(){
local url="$1"
<<< "$url" \
awk -F/ '{print $5}' \
| awk -F"dbip-city-lite-" '{print $2}' \
| awk -F'.csv.gz' '{print $1}'
}
fetch(){
logger INFO "Triggering fetch routine"
local url="$1"
curl -Ls "$url" > "$base_file"
if [[ $? -ne 0 ]]; then
logger WARN "Abnormal exit while accessing '$url'"
return
fi
gunzip -f "$base_file"
if [[ $? -ne 0 ]]; then
rm "$base_file"
logger WARN "Abnormal exit while unpacking gzip '$base_file'"
return
fi
< "$extracted_file" grep -vE "^[a-z0-9]{4}:" | grep -v "::" > "$ip_file"
if [[ $? -ne 0 ]]; then
logger WARN "Abnormal exit while parsing IPs"
return
fi
rm "${state_path}/${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
logger WARN "Anchor records missing in '$ip_file', possibly malformed"
rm "$ip_file"
return
fi
mv "$ip_file" "$geo_helper"
if [[ $? -ne 0 ]]; then
logger WARN "Abnormal exit while moving '$ip_file' to '$helpers_path'"
rm "$ip_file"
fi
echo "$month" > "$month_file"
logger INFO "Wrote '$month' to stub '$month_file'"
logger INFO "Updated '$ip_file'"
}
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"
# 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"
return
fi
# if stub is same date, abort
logger INFO "Local stub '$last_month' is identical to remote, skipping"
}
fetch_helpers(){ fetch_helpers(){
fetch_a2s fetch_a2s
fetch_dzq fetch_dzq
fetch_geo_file fetch_km_helper
fetch_ip_db
fetch_helpers_by_sum fetch_helpers_by_sum
[[ ! -f $share_path/icon.png ]] && freedesktop_dirs [[ ! -f $share_path/icon.png ]] && freedesktop_dirs
fetch_icons fetch_icons
@ -876,21 +973,14 @@ 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(){
source "$config_file"
declare -A hr declare -A hr
local res1 local res1
local res2 local res2
local str="No connection could be established to the remote server" local str="No connection could be established to the remote server"
hr=( hr=(
["steampowered.com"]="https://api.steampowered.com/IGameServersService/GetServerList/v1/?key=$steam_api"
["github.com"]="https://github.com/$author" ["github.com"]="https://github.com/$author"
["codeberg.org"]="https://codeberg.org/$author" ["codeberg.org"]="https://codeberg.org/$author"
) )
# steam API is mandatory, except on initial setup
if [[ -n $steam_api ]]; then
res=$(get_response_code "${hr["steampowered.com"]}")
[[ $res -ne 200 ]] && raise_error_and_quit "$str ("steampowered.com")"
fi
res=$(get_response_code "${hr["github.com"]}") res=$(get_response_code "${hr["github.com"]}")
if [[ $res -ne 200 ]]; then if [[ $res -ne 200 ]]; then
@ -906,7 +996,6 @@ test_connection(){
stable_url="$url_prefix/dzgui" stable_url="$url_prefix/dzgui"
testing_url="$url_prefix/testing" testing_url="$url_prefix/testing"
km_helper_url="$releases_url/latlon" km_helper_url="$releases_url/latlon"
geo_file_url="$releases_url/ips.csv.gz"
fi fi
} }
legacy_cols(){ legacy_cols(){
@ -922,14 +1011,14 @@ stale_mod_signatures(){
readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file) readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file)
for ((i=0; i<${#old_mod_ids[@]}; ++i)); do for ((i=0; i<${#old_mod_ids[@]}; ++i)); do
if [[ ! -d $workshop_dir/${old_mod_ids[$i]} ]]; then if [[ ! -d $workshop_dir/${old_mod_ids[$i]} ]]; then
"$HOME/.local/share/$app_name/helpers/funcs" "align_local" "${old_mod_ids[$i]}" "$func_helper" "align_local" "${old_mod_ids[$i]}"
fi fi
done done
fi fi
} }
create_new_links(){ create_new_links(){
"$HOME/.local/share/$app_name/helpers/funcs" "update_symlinks" "$func_helper" "update_symlinks"
} }
initial_setup(){ initial_setup(){
setup_dirs setup_dirs
@ -1007,9 +1096,6 @@ main(){
uninstall && uninstall &&
exit 0 exit 0
fi fi
if [[ $1 == "--steam" ]] || [[ $1 == "-s" ]]; then
export STEAM_LAUNCH=1
fi
set_im_module set_im_module

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version=5.6.0 version="5.8.0"
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@ -59,7 +59,6 @@ geo_file="$helpers_path/ips.csv"
km_helper="$helpers_path/latlon" km_helper="$helpers_path/latlon"
sums_path="$helpers_path/sums.md5" sums_path="$helpers_path/sums.md5"
query_helper="$helpers_path/query_v2.py" query_helper="$helpers_path/query_v2.py"
func_helper="$helpers_path/funcs"
lan_helper="$helpers_path/lan" lan_helper="$helpers_path/lan"
#STEAM PATHS #STEAM PATHS
@ -453,10 +452,15 @@ get_dist(){
} }
get_remote_servers(){ get_remote_servers(){
params=( params=(
"\\nor\1\map\chernarusplus\\nor\1\map\sakhal" "\\nor\1\map\chernarusplus\\nor\1\map\sakhal\\nor\1\map\enoch\empty\1\\nor\1\map\namalsk"
"\map\namalsk\empty\1"
"\map\namalsk\noplayers\1"
"\map\chernarusplus\empty\1" "\map\chernarusplus\empty\1"
"\map\chernarusplus\noplayers\1" "\map\chernarusplus\noplayers\1"
"\map\\sakhal" "\map\\sakhal\empty\1"
"\map\\sakhal\noplayers\1"
"\map\\enoch\empty\1"
"\map\\enoch\noplayers\1"
) )
local limit=10000 local limit=10000
local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?" local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
@ -601,6 +605,20 @@ filter_servers(){
<<< "$filtered" awk -F$separator '!seen[$1]++' <<< "$filtered" awk -F$separator '!seen[$1]++'
fi fi
} }
filter_official(){
if [[ ${filters[*]} =~ Official ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '$10 == "Community"'
fi
}
filter_community(){
if [[ ${filters[*]} =~ Unoffic ]]; then
echo -n "$filtered"
else
<<< "$filtered" awk -F$separator '$10 == "Official"'
fi
}
filtered=$(filter_perspective) filtered=$(filter_perspective)
filtered=$(filter_full) filtered=$(filter_full)
@ -611,6 +629,8 @@ filter_servers(){
filtered=$(filter_ascii) filtered=$(filter_ascii)
filtered=$(filter_duplicates) filtered=$(filter_duplicates)
filtered=$(filter_keyword) filtered=$(filter_keyword)
filtered=$(filter_official)
filtered=$(filter_community)
if [[ -z "$filtered" ]]; then if [[ -z "$filtered" ]]; then
logger WARN "Filter result is empty" logger WARN "Filter result is empty"
@ -648,7 +668,8 @@ parse_server_json(){
"\(.max_players)␞" + "\(.max_players)␞" +
"\(if .gametype == null then "0" elif .gametype|split("lqs")[1] == null then "0" else .gametype|split("lqs")[1]|split(",")[0] end)␞" + "\(if .gametype == null then "0" elif .gametype|split("lqs")[1] == null then "0" else .gametype|split("lqs")[1]|split(",")[0] end)␞" +
"\(.addr|split(":")[0]):\(if .gameport == null then "XXXX" else .gameport end)␞" + "\(.addr|split(":")[0]):\(if .gameport == null then "XXXX" else .gameport end)␞" +
"\(.addr|split(":")[1])" "\(.addr|split(":")[1])␞" +
"\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "external") then "Community" else "Official" end) end)"
' | sort -k1 ' | sort -k1
} }
align_versions_file(){ align_versions_file(){
@ -780,12 +801,17 @@ logger(){
} }
test_ping(){ test_ping(){
shift shift
local time
local res
local ip="$1" local ip="$1"
local qport="$2" local qport="$2"
local res res=$(a2s $ip $qport info)
res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}') if [[ -z $res ]]; then
[[ ! $? -eq 0 ]] && res="Timed out" time="Timed out"
printf "%s" "$res" else
time=$(<<< "$res" jq -r '.[].ping')
fi
printf "%s" "$time"
} }
show_server_modlist(){ show_server_modlist(){
shift shift
@ -1271,7 +1297,7 @@ symlinks(){
link="@$encoded_id" link="@$encoded_id"
mod="${arr[$i]}" mod="${arr[$i]}"
logger INFO "Creating link '$game_dir/$link' for '$workshop_dir/$mod'" logger INFO "Creating link '$game_dir/$link' for '$workshop_dir/$mod'"
[[ $STEAM_LAUNCH -eq 1 ]] && echo "# Creating mod link $((i+1))/${#arr[@]}" [[ -n $STEAMSCRIPT ]] && echo "# Creating mod link $((i+1))/${#arr[@]}"
ln -s "$workshop_dir/$mod" "$game_dir/$link" ln -s "$workshop_dir/$mod" "$game_dir/$link"
done done
} }
@ -1286,7 +1312,7 @@ symlinks(){
if [[ ${#links[@]} -eq 0 ]]; then if [[ ${#links[@]} -eq 0 ]]; then
logger INFO "No symlinks present in '$game_dir', creating them" logger INFO "No symlinks present in '$game_dir', creating them"
if [[ $STEAM_LAUNCH -eq 1 ]]; then if [[ -n $STEAMSCRIPT ]]; then
_create_links "${mods[@]}" > >(_pulse) _create_links "${mods[@]}" > >(_pulse)
else else
_create_links "${mods[@]}" _create_links "${mods[@]}"
@ -1304,7 +1330,7 @@ symlinks(){
# update missing targets # update missing targets
logger INFO "Found ${#hits[@]} unlinked mods" logger INFO "Found ${#hits[@]} unlinked mods"
if [[ $STEAM_LAUNCH -eq 1 ]]; then if [[ -n $STEAMSCRIPT ]]; then
_create_links "${hits[@]}" > >(_pulse) _create_links "${hits[@]}" > >(_pulse)
else else
_create_links "${hits[@]}" _create_links "${hits[@]}"

View File

@ -17,7 +17,6 @@ import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango from gi.repository import Gtk, GLib, Gdk, GObject, Pango
# 5.6.0
app_name = "DZGUI" app_name = "DZGUI"
cache = {} cache = {}
@ -38,7 +37,7 @@ mod_store = Gtk.ListStore(str, str, str, float, str)
#cf. log_cols #cf. log_cols
log_store = Gtk.ListStore(str, str, str, str) log_store = Gtk.ListStore(str, str, str, str)
#cf. browser_cols #cf. browser_cols
server_store = Gtk.ListStore(str, str, str, str, int, int, int, str, int) server_store = Gtk.ListStore(str, str, str, str, int, int, int, str, int, str)
default_tooltip = "Select a row to see its detailed description" default_tooltip = "Select a row to see its detailed description"
server_tooltip = [None, None] server_tooltip = [None, None]
@ -50,6 +49,7 @@ helpers_path = '%s/.local/share/dzgui/helpers' %(user_path)
log_path = '%s/logs' %(state_path) log_path = '%s/logs' %(state_path)
changelog_path = '%s/CHANGELOG.md' %(state_path) changelog_path = '%s/CHANGELOG.md' %(state_path)
geometry_path = '%s/dzg.cols.json' %(state_path) geometry_path = '%s/dzg.cols.json' %(state_path)
res_path = '%s/dzg.res.json' %(state_path)
funcs = '%s/funcs' %(helpers_path) funcs = '%s/funcs' %(helpers_path)
mods_temp_file = '%s/dzg.mods_temp' %(cache_path) mods_temp_file = '%s/dzg.mods_temp' %(cache_path)
stale_mods_temp_file = '%s/dzg.stale_mods_temp' %(cache_path) stale_mods_temp_file = '%s/dzg.stale_mods_temp' %(cache_path)
@ -88,14 +88,16 @@ log_cols = [
] ]
filters = { filters = {
"1PP": True, "1PP": True,
"3PP": True,
"Day": True, "Day": True,
"Night": True,
"Empty": False, "Empty": False,
"3PP": True,
"Night": True,
"Full": False, "Full": False,
"Low pop": True, "Low pop": True,
"Non-ASCII": False, "Non-ASCII": False,
"Duplicate": False "Duplicate": False,
"Official": True,
"Unoffic.": True,
} }
@ -552,7 +554,7 @@ def parse_server_rows(data):
lines = data.stdout.splitlines() lines = data.stdout.splitlines()
reader = csv.reader(lines, delimiter=delimiter) reader = csv.reader(lines, delimiter=delimiter)
try: try:
rows = [[row[0], row[1], row[2], row[3], int(row[4]), int(row[5]), int(row[6]), row[7], int(row[8])] for row in reader if row] rows = [[row[0], row[1], row[2], row[3], int(row[4]), int(row[5]), int(row[6]), row[7], int(row[8]), row[9]] for row in reader if row]
except IndexError: except IndexError:
return 1 return 1
for row in rows: for row in rows:
@ -685,7 +687,7 @@ def process_shell_return_code(transient_parent, msg, code, original_input):
process_tree_option([treeview.view, RowType.HANDSHAKE], treeview) process_tree_option([treeview.view, RowType.HANDSHAKE], treeview)
case 255: case 255:
spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", Popup.NOTIFY) spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", Popup.NOTIFY)
Gtk.main_quit() save_res_and_quit(transient_parent)
def process_tree_option(input, treeview): def process_tree_option(input, treeview):
@ -874,19 +876,36 @@ class OuterWindow(Gtk.Window):
self.add(self.grid) self.add(self.grid)
if is_game_mode is True: if is_game_mode is True:
self.fullscreen() self.fullscreen()
else: elif query_config(None, "fullscreen")[0] == "true":
if query_config(None, "fullscreen")[0] == "true": logger.info("User preference for 'fullscreen' is 'true'")
self.maximize() self.maximize()
else:
if os.path.isfile(res_path):
with open(res_path, "r") as infile:
try:
data = json.load(infile)
valid_json = True
except json.decoder.JSONDecodeError:
logger.critical("JSON decode error in '%s'" %(res_path))
valid_json = False
else:
valid_json = False
if valid_json:
res = data["res"]
w = res["width"]
h = res["height"]
logger.info("Restoring window size to %s,%s" %(w,h))
self.set_default_size(w, h)
# Hide FilterPanel on main menu
self.show_all() self.show_all()
# Hide FilterPanel on main menu
self.grid.right_panel.set_filter_visibility(False) self.grid.right_panel.set_filter_visibility(False)
self.grid.sel_panel.set_visible(False) self.grid.sel_panel.set_visible(False)
self.grid.scrollable_treelist.treeview.grab_focus() self.grid.scrollable_treelist.treeview.grab_focus()
def halt_proc_and_quit(self, window, event): def halt_proc_and_quit(self, window, event):
self.grid.terminate_treeview_process() self.grid.terminate_treeview_process()
Gtk.main_quit() save_res_and_quit(window)
class ScrollableTree(Gtk.ScrolledWindow): class ScrollableTree(Gtk.ScrolledWindow):
@ -896,7 +915,6 @@ class ScrollableTree(Gtk.ScrolledWindow):
self.treeview = TreeView(is_steam_deck) self.treeview = TreeView(is_steam_deck)
self.add(self.treeview) self.add(self.treeview)
class RightPanel(Gtk.Box): class RightPanel(Gtk.Box):
def __init__(self, is_steam_deck): def __init__(self, is_steam_deck):
super().__init__(spacing=6) super().__init__(spacing=6)
@ -925,7 +943,6 @@ class RightPanel(Gtk.Box):
self.question_button.connect("clicked", self._on_button_clicked) self.question_button.connect("clicked", self._on_button_clicked)
self.pack_start(self.debug_toggle, False, True, 0) self.pack_start(self.debug_toggle, False, True, 0)
if is_steam_deck is False:
self.pack_start(self.question_button, False, True, 0) self.pack_start(self.question_button, False, True, 0)
def _on_button_toggled(self, button, command): def _on_button_toggled(self, button, command):
@ -971,7 +988,7 @@ class ButtonBox(Gtk.Box):
button.set_opacity(0.6) button.set_opacity(0.6)
self.buttons.append(button) self.buttons.append(button)
button.connect("clicked", self._on_selection_button_clicked) button.connect("clicked", self._on_selection_button_clicked)
self.pack_start(button, False, False, True) self.pack_start(button, False, False, 0)
self.buttons[0].set_opacity(1.0) self.buttons[0].set_opacity(1.0)
@ -1043,7 +1060,9 @@ class ButtonBox(Gtk.Box):
if context == ButtonType.EXIT: if context == ButtonType.EXIT:
logger.info("Normal user exit") logger.info("Normal user exit")
Gtk.main_quit() widgets = relative_widget(self)
window = widgets["outer"]
save_res_and_quit(window)
return return
cols = treeview.get_columns() cols = treeview.get_columns()
@ -1073,12 +1092,13 @@ class ButtonBox(Gtk.Box):
class CalcDist(multiprocessing.Process): class CalcDist(multiprocessing.Process):
def __init__(self, widget, addr, result_queue, cache): def __init__(self, widget, addr, qport, result_queue, cache):
super().__init__() super().__init__()
self.widget = widget self.widget = widget
self.result_queue = result_queue self.result_queue = result_queue
self.addr = addr self.addr = addr
self.qport = str(qport)
self.ip = addr.split(':')[0] self.ip = addr.split(':')[0]
def run(self): def run(self):
@ -1087,7 +1107,7 @@ class CalcDist(multiprocessing.Process):
self.result_queue.put([self.addr, cache[self.addr][0], cache[self.addr][1]]) self.result_queue.put([self.addr, cache[self.addr][0], cache[self.addr][1]])
return return
proc = call_out(self.widget, "get_dist", self.ip) proc = call_out(self.widget, "get_dist", self.ip)
proc2 = call_out(self.widget, "test_ping", self.ip) proc2 = call_out(self.widget, "test_ping", self.ip, self.qport)
km = proc.stdout km = proc.stdout
ping = proc2.stdout ping = proc2.stdout
self.result_queue.put([self.addr, km, ping]) self.result_queue.put([self.addr, km, ping])
@ -1314,6 +1334,7 @@ class TreeView(Gtk.TreeView):
if self.view == WindowContext.TABLE_API or self.view == WindowContext.TABLE_SERVER: if self.view == WindowContext.TABLE_API or self.view == WindowContext.TABLE_SERVER:
addr = self.get_column_at_index(7) addr = self.get_column_at_index(7)
qport = self.get_column_at_index(8)
if addr is None: if addr is None:
server_tooltip[0] = format_tooltip() server_tooltip[0] = format_tooltip()
grid.update_statusbar(server_tooltip[0]) grid.update_statusbar(server_tooltip[0])
@ -1327,7 +1348,7 @@ class TreeView(Gtk.TreeView):
grid.update_statusbar(tooltip) grid.update_statusbar(tooltip)
return return
self.emit("on_distcalc_started") self.emit("on_distcalc_started")
self.current_proc = CalcDist(self, addr, self.queue, cache) self.current_proc = CalcDist(self, addr, qport, self.queue, cache)
self.current_proc.start() self.current_proc.start()
else: else:
tooltip = format_metadata(row_sel) tooltip = format_metadata(row_sel)
@ -1582,12 +1603,14 @@ class TreeView(Gtk.TreeView):
column_title = "Name" column_title = "Name"
saved_size = data["cols"][column_title] saved_size = data["cols"][column_title]
column.set_fixed_width(saved_size) column.set_fixed_width(saved_size)
column.set_expand(True)
else: else:
if ("Name" in column_title): if ("Name" in column_title):
column.set_fixed_width(800) column.set_fixed_width(800)
if (column_title == "Map"): if (column_title == "Map"):
column.set_fixed_width(300) column.set_fixed_width(300)
if i != 10:
self.append_column(column) self.append_column(column)
self.update_first_col(mode.dict["label"]) self.update_first_col(mode.dict["label"])
@ -2330,12 +2353,8 @@ class Grid(Gtk.Grid):
self._version = "%s %s" %(app_name, sys.argv[2]) self._version = "%s %s" %(app_name, sys.argv[2])
self.scrollable_treelist = ScrollableTree(is_steam_deck) self.scrollable_treelist = ScrollableTree(is_steam_deck)
if is_steam_deck is True:
self.scrollable_treelist.set_hexpand(False) self.scrollable_treelist.set_hexpand(False)
self.scrollable_treelist.set_vexpand(True) self.scrollable_treelist.set_vexpand(True)
else:
self.scrollable_treelist.set_hexpand(True)
self.scrollable_treelist.set_vexpand(True)
self.right_panel = RightPanel(is_steam_deck) self.right_panel = RightPanel(is_steam_deck)
self.sel_panel = ModSelectionPanel() self.sel_panel = ModSelectionPanel()
@ -2352,13 +2371,8 @@ class Grid(Gtk.Grid):
self.bar.add(self.status_right_label) self.bar.add(self.status_right_label)
self.update_right_statusbar() self.update_right_statusbar()
if is_steam_deck is True: self.attach(self.scrollable_treelist, 0, 0, 3, 1)
self.attach(self.scrollable_treelist, 0, 0, 4, 1) self.attach_next_to(self.bar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 3, 1)
self.attach_next_to(self.bar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 4, 1)
self.attach_next_to(self.right_panel, self.scrollable_treelist, Gtk.PositionType.RIGHT, 1, 1)
else:
self.attach(self.scrollable_treelist, 0, 0, 7, 5)
self.attach_next_to(self.bar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 7, 1)
self.attach_next_to(self.right_panel, self.scrollable_treelist, Gtk.PositionType.RIGHT, 1, 1) self.attach_next_to(self.right_panel, self.scrollable_treelist, Gtk.PositionType.RIGHT, 1, 1)
def update_right_statusbar(self): def update_right_statusbar(self):
@ -2438,16 +2452,50 @@ class App(Gtk.Application):
self.win = OuterWindow(is_steam_deck, is_game_mode) self.win = OuterWindow(is_steam_deck, is_game_mode)
self.win.set_icon_name("dzgui") self.win.set_icon_name("dzgui")
accel = Gtk.AccelGroup() accel = Gtk.AccelGroup()
accel.connect(Gdk.KEY_q, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE, self._halt_window_subprocess) accel.connect(Gdk.KEY_q, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE, self._halt_window_subprocess)
self.win.add_accel_group(accel) self.win.add_accel_group(accel)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint)
Gtk.main() Gtk.main()
def _halt_window_subprocess(self, accel_group, window, code, flag): def _catch_sigint(self):
self.win.halt_proc_and_quit(self, None) self.win.halt_proc_and_quit(self.win, None)
def _halt_window_subprocess(self, accel_group, window, code, flag):
self.win.halt_proc_and_quit(self.win, None)
def save_res_and_quit(window):
if window.props.is_maximized:
Gtk.main_quit()
return
rect = window.get_size()
def write_json(rect):
data = {"res": { "width": rect.width, "height": rect.height } }
j = json.dumps(data, indent=2)
with open(res_path, "w") as outfile:
outfile.write(j)
logger.info("Wrote initial window size to '%s'" %(res_path))
if os.path.isfile(res_path):
with open(res_path, "r") as infile:
try:
data = json.load(infile)
data["res"]["width"] = rect.width
data["res"]["height"] = rect.height
with open(res_path, "w") as outfile:
outfile.write(json.dumps(data, indent=2))
except json.decoder.JSONDecodeError:
logger.critical("JSON decode error in '%s'" %(res_path))
write_json(rect)
else:
write_json(rect)
Gtk.main_quit()
class ModSelectionPanel(Gtk.Box): class ModSelectionPanel(Gtk.Box):
def __init__(self): def __init__(self):
@ -2471,7 +2519,6 @@ class ModSelectionPanel(Gtk.Box):
button.connect("clicked", self._on_button_clicked) button.connect("clicked", self._on_button_clicked)
self.pack_start(button, False, True, 0) self.pack_start(button, False, True, 0)
def initialize(self): def initialize(self):
l = len(self.get_children()) l = len(self.get_children())
last = self.get_children()[l-1] last = self.get_children()[l-1]
@ -2483,7 +2530,6 @@ class ModSelectionPanel(Gtk.Box):
case "Unhighlight stale": case "Unhighlight stale":
i.set_label("Highlight stale") i.set_label("Highlight stale")
def _on_button_clicked(self, button): def _on_button_clicked(self, button):
self.active_button = button self.active_button = button
label = button.get_label() label = button.get_label()
@ -2512,7 +2558,6 @@ class ModSelectionPanel(Gtk.Box):
path = Gtk.TreePath(i) path = Gtk.TreePath(i)
treeview.get_selection().select_path(path) treeview.get_selection().select_path(path)
def _remove_last_button(self): def _remove_last_button(self):
children = self.get_children() children = self.get_children()
l = len(children) l = len(children)
@ -2637,12 +2682,20 @@ class FilterPanel(Gtk.Box):
self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("changed", self._on_map_changed)
self.maps_combo.connect("key-press-event", self._on_esc_pressed) self.maps_combo.connect("key-press-event", self._on_esc_pressed)
self.pack_start(self.filters_label, False, False, True) self.pack_start(self.filters_label, False, False, 0)
self.pack_start(self.keyword_entry, False, False, True) self.pack_start(self.keyword_entry, False, False, 0)
self.pack_start(self.maps_combo, False, False, True) self.pack_start(self.maps_combo, False, False, 0)
button_grid = Gtk.Grid()
row = 1
col = 0
for i, check in enumerate(checks[0:]): for i, check in enumerate(checks[0:]):
self.pack_start(checks[i], False, False, True) col = col + 1
if (col > 3):
row = row + 1
col = 1
button_grid.attach(checks[i], col, row, 1, 1)
self.pack_start(button_grid, False, False, 0)
def _on_map_entry_keypress(self, entry, event): def _on_map_entry_keypress(self, entry, event):
match event.keyval: match event.keyval: