Last active
July 7, 2026 14:09
-
-
Save ergolyam/9657ccde929cd282df684b7596b3465a to your computer and use it in GitHub Desktop.
Search for Yggdrasil peers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| set -f | |
| URL_DEFAULT='https://peers.yggdrasil.link/publicnodes.json' | |
| url=$URL_DEFAULT | |
| source_file= | |
| sort_mode=ping | |
| records_mode=0 | |
| ping_timeout=1 | |
| ping_concurrency=8 | |
| tmpdir= | |
| ui_started=0 | |
| raw_started=0 | |
| old_stty= | |
| ping_worker_pid= | |
| ping_status_file= | |
| ping_total=0 | |
| die() { | |
| printf 'ygg-peer-picker: %s\n' "$1" >&2 | |
| exit 1 | |
| } | |
| usage() { | |
| cat <<'EOF' | |
| Usage: ygg-peer-picker.sh [options] | |
| Fetch Yggdrasil public peers, sort them, select peers in a TUI, and print | |
| selected peer addresses one per line after Enter. | |
| Options: | |
| -s, --sort ping|country|proto Initial sort mode (default: ping) | |
| --url URL JSON URL to fetch | |
| --file FILE Read JSON from a local file | |
| --records Print parsed TSV records and exit | |
| --ping-timeout SECONDS Per-peer local ping timeout (default: 1) | |
| --ping-concurrency COUNT Parallel local ping batch size (default: 8) | |
| -h, --help Show this help | |
| TUI keys: | |
| Up/Down or k/j Move cursor | |
| Space Select or unselect peer | |
| p, c, r Sort by ping, country, protocol | |
| Enter Accept selection and print addresses | |
| q or Esc Quit without output | |
| EOF | |
| } | |
| set_sort_mode() { | |
| case $1 in | |
| ping|p|1) | |
| sort_mode=ping | |
| ;; | |
| country|c|2) | |
| sort_mode=country | |
| ;; | |
| proto|protocol|r|3) | |
| sort_mode=proto | |
| ;; | |
| *) | |
| die "unknown sort mode: $1" | |
| ;; | |
| esac | |
| } | |
| set_positive_number() { | |
| name=$1 | |
| value=$2 | |
| case $value in | |
| ''|*[!0-9]*|0) | |
| die "$name must be a positive integer" | |
| ;; | |
| esac | |
| } | |
| while [ "$#" -gt 0 ] | |
| do | |
| case $1 in | |
| -s|--sort) | |
| shift | |
| [ "$#" -gt 0 ] || die "missing value for --sort" | |
| set_sort_mode "$1" | |
| ;; | |
| --url) | |
| shift | |
| [ "$#" -gt 0 ] || die "missing value for --url" | |
| url=$1 | |
| ;; | |
| --file) | |
| shift | |
| [ "$#" -gt 0 ] || die "missing value for --file" | |
| source_file=$1 | |
| ;; | |
| --records) | |
| records_mode=1 | |
| ;; | |
| --ping-timeout) | |
| shift | |
| [ "$#" -gt 0 ] || die "missing value for --ping-timeout" | |
| set_positive_number "--ping-timeout" "$1" | |
| ping_timeout=$1 | |
| ;; | |
| --ping-concurrency) | |
| shift | |
| [ "$#" -gt 0 ] || die "missing value for --ping-concurrency" | |
| set_positive_number "--ping-concurrency" "$1" | |
| ping_concurrency=$1 | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| die "unknown option: $1" | |
| ;; | |
| esac | |
| shift | |
| done | |
| cleanup() { | |
| if [ -n "$ping_worker_pid" ]; then | |
| kill "$ping_worker_pid" 2>/dev/null | |
| wait "$ping_worker_pid" 2>/dev/null | |
| ping_worker_pid= | |
| fi | |
| if [ "$raw_started" -eq 1 ] && [ -n "$old_stty" ]; then | |
| stty "$old_stty" <&4 2>/dev/null | |
| fi | |
| if [ "$ui_started" -eq 1 ]; then | |
| printf '\033[0m\033[?25h\033[?1049l' >&3 | |
| fi | |
| if [ -n "$tmpdir" ] && [ -d "$tmpdir" ]; then | |
| rm -rf "$tmpdir" | |
| fi | |
| } | |
| on_signal() { | |
| exit 130 | |
| } | |
| trap cleanup EXIT | |
| trap on_signal HUP INT TERM | |
| make_tmpdir() { | |
| base=${TMPDIR:-/tmp} | |
| base=${base%/} | |
| if command -v mktemp >/dev/null 2>&1; then | |
| tmpdir=$(mktemp -d "$base/ygg-peer-picker.XXXXXX") || exit 1 | |
| else | |
| tmpdir=$base/ygg-peer-picker.$$ | |
| old_umask=$(umask) | |
| umask 077 | |
| mkdir "$tmpdir" || exit 1 | |
| umask "$old_umask" | |
| fi | |
| } | |
| fetch_nodes() { | |
| if [ -n "$source_file" ]; then | |
| cat "$source_file" | |
| return $? | |
| fi | |
| if command -v curl >/dev/null 2>&1; then | |
| curl -fsSL "$url" | |
| return $? | |
| fi | |
| if command -v wget >/dev/null 2>&1; then | |
| wget -qO- "$url" | |
| return $? | |
| fi | |
| printf 'ygg-peer-picker: need curl or wget to fetch %s\n' "$url" >&2 | |
| return 127 | |
| } | |
| parse_nodes() { | |
| awk ' | |
| BEGIN { | |
| country = "" | |
| addr = "" | |
| proto = "" | |
| host = "" | |
| in_peer = 0 | |
| up = 0 | |
| } | |
| /^ "[^"]+\.md"[[:space:]]*:[[:space:]]*\{[[:space:]]*,?$/ { | |
| country = $0 | |
| sub(/^ "/, "", country) | |
| sub(/\.md"[[:space:]]*:[[:space:]]*\{[[:space:]]*,?$/, "", country) | |
| gsub(/[-_]/, " ", country) | |
| next | |
| } | |
| country != "" && /^ "[^"]+"[[:space:]]*:[[:space:]]*\{[[:space:]]*,?$/ { | |
| addr = $0 | |
| sub(/^ "/, "", addr) | |
| sub(/"[[:space:]]*:[[:space:]]*\{[[:space:]]*,?$/, "", addr) | |
| if (addr ~ /^[A-Za-z][A-Za-z0-9+.-]*:\/\//) { | |
| proto = addr | |
| sub(/:\/\/.*/, "", proto) | |
| host = addr | |
| sub(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "", host) | |
| sub(/[\/?].*$/, "", host) | |
| if (host ~ /^\[[^]]+\]/) { | |
| sub(/^\[/, "", host) | |
| sub(/\].*$/, "", host) | |
| } else { | |
| sub(/:.*/, "", host) | |
| } | |
| in_peer = 1 | |
| up = 0 | |
| } | |
| next | |
| } | |
| in_peer && /^[[:space:]]*"up"[[:space:]]*:[[:space:]]*true/ { | |
| up = 1 | |
| next | |
| } | |
| in_peer && /^ \}[,[:space:]]*$/ { | |
| if (up) { | |
| printf "999999\t%s\t%s\t%s\t%s\n", country, proto, addr, host | |
| } | |
| in_peer = 0 | |
| addr = "" | |
| proto = "" | |
| host = "" | |
| up = 0 | |
| next | |
| } | |
| ' | |
| } | |
| require_ping() { | |
| if ! command -v ping >/dev/null 2>&1; then | |
| printf 'ygg-peer-picker: need ping to test latency from this device\n' >&2 | |
| return 127 | |
| fi | |
| return 0 | |
| } | |
| ping_wait_arg() { | |
| case $(uname -s 2>/dev/null) in | |
| Darwin|FreeBSD|OpenBSD|NetBSD) | |
| printf '%s' $((ping_timeout * 1000)) | |
| ;; | |
| *) | |
| printf '%s' "$ping_timeout" | |
| ;; | |
| esac | |
| } | |
| measure_ping_ms() { | |
| host=$1 | |
| wait_arg=$(ping_wait_arg) | |
| hard_timeout=$((ping_timeout + 2)) | |
| if command -v timeout >/dev/null 2>&1; then | |
| output=$(timeout "$hard_timeout" ping -n -c 1 -W "$wait_arg" "$host" 2>/dev/null) | |
| else | |
| output=$(ping -n -c 1 -W "$wait_arg" "$host" 2>/dev/null) | |
| fi | |
| code=$? | |
| if [ "$code" -ne 0 ]; then | |
| printf '999999' | |
| return | |
| fi | |
| printf '%s\n' "$output" | awk ' | |
| BEGIN { | |
| found = 0 | |
| } | |
| { | |
| for (i = 1; i <= NF; i++) { | |
| if ($i ~ /^time[=<]/) { | |
| value = $i | |
| sub(/^time[=<]/, "", value) | |
| sub(/[^0-9.].*$/, "", value) | |
| if (value != "") { | |
| print value | |
| found = 1 | |
| exit | |
| } | |
| } | |
| } | |
| } | |
| END { | |
| if (!found) { | |
| print "999999" | |
| } | |
| } | |
| ' | |
| } | |
| measure_record_line() { | |
| line=$1 | |
| tab=$(printf '\t') | |
| old_ifs=$IFS | |
| IFS=$tab | |
| set -- $line | |
| IFS=$old_ifs | |
| country=$2 | |
| proto=$3 | |
| addr=$4 | |
| host=$5 | |
| if [ -z "$host" ]; then | |
| ping_ms=999999 | |
| else | |
| ping_ms=$(measure_ping_ms "$host") | |
| fi | |
| printf '%s\t%s\t%s\t%s\n' "$ping_ms" "$country" "$proto" "$addr" | |
| } | |
| measure_records_plain() { | |
| input_file=$1 | |
| output_file=$2 | |
| require_ping || return $? | |
| : > "$output_file" | |
| while IFS= read -r line | |
| do | |
| measure_record_line "$line" >> "$output_file" | |
| done < "$input_file" | |
| } | |
| sort_records() { | |
| file=$1 | |
| tab=$(printf '\t') | |
| case $sort_mode in | |
| ping) | |
| sort -t "$tab" -k1,1n -k2,2 -k3,3 -k4,4 "$file" | |
| ;; | |
| country) | |
| sort -t "$tab" -k2,2 -k3,3 -k1,1n -k4,4 "$file" | |
| ;; | |
| proto) | |
| sort -t "$tab" -k3,3 -k1,1n -k2,2 -k4,4 "$file" | |
| ;; | |
| esac | |
| } | |
| build_records_plain() { | |
| json_file=$tmpdir/publicnodes.json | |
| parsed_file=$tmpdir/parsed.tsv | |
| records_file=$tmpdir/records.tsv | |
| fetch_nodes > "$json_file" | |
| code=$? | |
| [ "$code" -eq 0 ] || exit "$code" | |
| parse_nodes < "$json_file" > "$parsed_file" | |
| code=$? | |
| [ "$code" -eq 0 ] || exit "$code" | |
| measure_records_plain "$parsed_file" "$records_file" | |
| code=$? | |
| [ "$code" -eq 0 ] || exit "$code" | |
| } | |
| line_count() { | |
| file=$1 | |
| if [ ! -f "$file" ]; then | |
| printf '0' | |
| return | |
| fi | |
| set -- $(wc -l < "$file" 2>/dev/null) | |
| printf '%s' "${1:-0}" | |
| } | |
| file_size() { | |
| file=$1 | |
| if [ ! -f "$file" ]; then | |
| printf '0' | |
| return | |
| fi | |
| set -- $(wc -c < "$file" 2>/dev/null) | |
| printf '%s' "${1:-0}" | |
| } | |
| open_tty() { | |
| if ! exec 3>/dev/tty; then | |
| die "cannot open /dev/tty for TUI output" | |
| fi | |
| if ! exec 4</dev/tty; then | |
| die "cannot open /dev/tty for TUI input" | |
| fi | |
| } | |
| enter_ui() { | |
| old_stty=$(stty -g <&4 2>/dev/null) | |
| printf '\033[?1049h\033[?25l\033[H\033[J' >&3 | |
| ui_started=1 | |
| } | |
| leave_ui() { | |
| if [ "$raw_started" -eq 1 ] && [ -n "$old_stty" ]; then | |
| stty "$old_stty" <&4 2>/dev/null | |
| raw_started=0 | |
| fi | |
| if [ "$ui_started" -eq 1 ]; then | |
| printf '\033[0m\033[?25h\033[?1049l' >&3 | |
| ui_started=0 | |
| fi | |
| } | |
| start_raw_input() { | |
| stty -echo -icanon min 1 time 0 <&4 | |
| raw_started=1 | |
| } | |
| set_read_timeout() { | |
| stty min 0 time 1 <&4 | |
| } | |
| set_read_blocking() { | |
| stty min 1 time 0 <&4 | |
| } | |
| read_one() { | |
| dd bs=1 count=1 2>/dev/null <&4 | |
| } | |
| read_one_timeout() { | |
| set_read_timeout | |
| ch=$(dd bs=1 count=1 2>/dev/null <&4) | |
| set_read_blocking | |
| printf '%s' "$ch" | |
| } | |
| read_key() { | |
| esc=$(printf '\033') | |
| cr=$(printf '\r') | |
| key=$(read_one) | |
| case $key in | |
| "$esc") | |
| second=$(read_one_timeout) | |
| if [ -z "$second" ]; then | |
| printf 'QUIT\n' | |
| return | |
| fi | |
| if [ "$second" = "[" ]; then | |
| third=$(read_one_timeout) | |
| case $third in | |
| A) printf 'UP\n' ;; | |
| B) printf 'DOWN\n' ;; | |
| *) printf 'NONE\n' ;; | |
| esac | |
| return | |
| fi | |
| printf 'NONE\n' | |
| ;; | |
| "") | |
| printf 'ENTER\n' | |
| ;; | |
| "$cr") | |
| printf 'ENTER\n' | |
| ;; | |
| " ") | |
| printf 'SPACE\n' | |
| ;; | |
| j) | |
| printf 'DOWN\n' | |
| ;; | |
| k) | |
| printf 'UP\n' | |
| ;; | |
| p) | |
| printf 'SORT_PING\n' | |
| ;; | |
| c) | |
| printf 'SORT_COUNTRY\n' | |
| ;; | |
| r) | |
| printf 'SORT_PROTO\n' | |
| ;; | |
| q) | |
| printf 'QUIT\n' | |
| ;; | |
| *) | |
| printf 'NONE\n' | |
| ;; | |
| esac | |
| } | |
| read_byte_code_timeout() { | |
| timeout_ticks=$1 | |
| stty min 0 time "$timeout_ticks" <&4 | |
| code=$(dd bs=1 count=1 2>/dev/null <&4 | od -An -tu1 | awk '{ print $1 }') | |
| stty min 1 time 0 <&4 | |
| printf '%s' "$code" | |
| } | |
| read_key_poll() { | |
| code=$(read_byte_code_timeout 1) | |
| case $code in | |
| '') | |
| printf 'NONE\n' | |
| ;; | |
| 27) | |
| second=$(read_byte_code_timeout 1) | |
| if [ -z "$second" ]; then | |
| printf 'QUIT\n' | |
| return | |
| fi | |
| if [ "$second" = 91 ]; then | |
| third=$(read_byte_code_timeout 1) | |
| case $third in | |
| 65) printf 'UP\n' ;; | |
| 66) printf 'DOWN\n' ;; | |
| *) printf 'NONE\n' ;; | |
| esac | |
| return | |
| fi | |
| printf 'NONE\n' | |
| ;; | |
| 10|13) | |
| printf 'ENTER\n' | |
| ;; | |
| 32) | |
| printf 'SPACE\n' | |
| ;; | |
| 106) | |
| printf 'DOWN\n' | |
| ;; | |
| 107) | |
| printf 'UP\n' | |
| ;; | |
| 112) | |
| printf 'SORT_PING\n' | |
| ;; | |
| 99) | |
| printf 'SORT_COUNTRY\n' | |
| ;; | |
| 114) | |
| printf 'SORT_PROTO\n' | |
| ;; | |
| 113) | |
| printf 'QUIT\n' | |
| ;; | |
| *) | |
| printf 'NONE\n' | |
| ;; | |
| esac | |
| } | |
| draw_loading() { | |
| title=$1 | |
| detail=$2 | |
| spin=$3 | |
| case $spin in | |
| 0) mark='|' ;; | |
| 1) mark='/' ;; | |
| 2) mark='-' ;; | |
| *) mark='\' ;; | |
| esac | |
| printf '\033[H\033[J' >&3 | |
| printf '\033[1;36mYggdrasil public peer picker\033[0m\n\n' >&3 | |
| printf ' %s %s\n\n' "$mark" "$title" >&3 | |
| printf ' %s\n' "$detail" >&3 | |
| } | |
| draw_progress_table() { | |
| file=$1 | |
| title=$2 | |
| detail=$3 | |
| spin=$4 | |
| progress_view_file=$tmpdir/progress-view.tsv | |
| case $spin in | |
| 0) mark='|' ;; | |
| 1) mark='/' ;; | |
| 2) mark='-' ;; | |
| *) mark='\' ;; | |
| esac | |
| get_size | |
| list_rows=$((rows - 7)) | |
| if [ "$list_rows" -lt 1 ]; then | |
| list_rows=1 | |
| fi | |
| if [ -s "$file" ]; then | |
| sort_records "$file" > "$progress_view_file" | |
| else | |
| : > "$progress_view_file" | |
| fi | |
| count=$(line_count "$progress_view_file") | |
| printf '\033[H\033[J' >&3 | |
| printf '\033[1;36mYggdrasil public peer picker\033[0m\n' >&3 | |
| printf ' %s %s\n' "$mark" "$title" >&3 | |
| printf ' %s\n\n' "$detail" >&3 | |
| printf '\033[2m%-6s %-16s %-7s %s\033[0m\n' 'Ping' 'Country' 'Proto' 'Address' >&3 | |
| if [ "$count" -eq 0 ]; then | |
| printf '\nWaiting for first local ping result...\n' >&3 | |
| return | |
| fi | |
| awk -F '\t' \ | |
| -v rows="$list_rows" \ | |
| -v cols="$cols" ' | |
| function fit(s, w) { | |
| if (w < 1) { | |
| return "" | |
| } | |
| if (length(s) > w) { | |
| return substr(s, 1, w - 1) "~" | |
| } | |
| return s | |
| } | |
| BEGIN { | |
| country_w = 16 | |
| proto_w = 7 | |
| addr_w = cols - 33 | |
| if (addr_w < 10) { | |
| addr_w = 10 | |
| } | |
| } | |
| NR > rows { | |
| exit | |
| } | |
| { | |
| display_ping = $1 | |
| if (display_ping == "999999") { | |
| display_ping = "-" | |
| } | |
| printf "%6s %-16s %-7s %s\n", display_ping, fit($2, country_w), fit($3, proto_w), fit($4, addr_w) | |
| } | |
| ' "$progress_view_file" >&3 | |
| } | |
| measure_records_with_ui() { | |
| input_file=$1 | |
| output_file=$2 | |
| status_file=$tmpdir/ping.status | |
| progress_file=$tmpdir/ping.progress | |
| total=$(line_count "$input_file") | |
| spin=0 | |
| if ! require_ping; then | |
| draw_loading "Cannot test local latency" "ping command was not found" "$spin" | |
| sleep 2 | |
| return 127 | |
| fi | |
| : > "$output_file" | |
| ( | |
| batch=0 | |
| batch_files= | |
| idx=0 | |
| completed=0 | |
| while IFS= read -r line | |
| do | |
| idx=$((idx + 1)) | |
| batch=$((batch + 1)) | |
| result_file=$tmpdir/ping-result.$idx | |
| measure_record_line "$line" > "$result_file" & | |
| batch_files="$batch_files $result_file" | |
| if [ "$batch" -ge "$ping_concurrency" ]; then | |
| wait | |
| for measured_file in $batch_files | |
| do | |
| cat "$measured_file" >> "$output_file" | |
| rm -f "$measured_file" | |
| completed=$((completed + 1)) | |
| done | |
| printf '%s\n' "$completed" > "$progress_file" | |
| batch=0 | |
| batch_files= | |
| fi | |
| done < "$input_file" | |
| if [ "$batch" -gt 0 ]; then | |
| wait | |
| for measured_file in $batch_files | |
| do | |
| cat "$measured_file" >> "$output_file" | |
| rm -f "$measured_file" | |
| completed=$((completed + 1)) | |
| done | |
| printf '%s\n' "$completed" > "$progress_file" | |
| fi | |
| printf '0\n' > "$status_file" | |
| ) & | |
| ping_pid=$! | |
| while [ ! -f "$status_file" ] | |
| do | |
| measured=$(line_count "$output_file") | |
| draw_progress_table "$output_file" "Testing latency from this device" "$measured/$total peers measured by local ping" "$spin" | |
| spin=$(( (spin + 1) % 4 )) | |
| [ -f "$status_file" ] && break | |
| sleep 1 | |
| done | |
| wait "$ping_pid" 2>/dev/null | |
| set -- $(cat "$status_file" 2>/dev/null) | |
| code=${1:-1} | |
| if [ "$code" -ne 0 ]; then | |
| draw_loading "Local ping failed" "ping worker exited with status $code" "$spin" | |
| sleep 2 | |
| return "$code" | |
| fi | |
| return 0 | |
| } | |
| start_measure_records_background() { | |
| input_file=$1 | |
| output_file=$2 | |
| ping_status_file=$tmpdir/ping.status | |
| ping_total=$(line_count "$input_file") | |
| if ! require_ping; then | |
| return 127 | |
| fi | |
| : > "$output_file" | |
| rm -f "$ping_status_file" | |
| ( | |
| batch=0 | |
| batch_files= | |
| idx=0 | |
| completed=0 | |
| while IFS= read -r line | |
| do | |
| idx=$((idx + 1)) | |
| batch=$((batch + 1)) | |
| result_file=$tmpdir/ping-live-result.$idx | |
| measure_record_line "$line" > "$result_file" & | |
| batch_files="$batch_files $result_file" | |
| if [ "$batch" -ge "$ping_concurrency" ]; then | |
| wait | |
| for measured_file in $batch_files | |
| do | |
| cat "$measured_file" >> "$output_file" | |
| rm -f "$measured_file" | |
| completed=$((completed + 1)) | |
| done | |
| batch=0 | |
| batch_files= | |
| fi | |
| done < "$input_file" | |
| if [ "$batch" -gt 0 ]; then | |
| wait | |
| for measured_file in $batch_files | |
| do | |
| cat "$measured_file" >> "$output_file" | |
| rm -f "$measured_file" | |
| completed=$((completed + 1)) | |
| done | |
| fi | |
| printf '0\n' > "$ping_status_file" | |
| ) & | |
| ping_worker_pid=$! | |
| return 0 | |
| } | |
| stop_ping_worker() { | |
| if [ -n "$ping_worker_pid" ]; then | |
| kill "$ping_worker_pid" 2>/dev/null | |
| wait "$ping_worker_pid" 2>/dev/null | |
| ping_worker_pid= | |
| fi | |
| } | |
| load_records_with_ui() { | |
| json_file=$tmpdir/publicnodes.json | |
| parsed_file=$tmpdir/parsed.tsv | |
| records_file=$tmpdir/records.tsv | |
| fetch_status=$tmpdir/fetch.status | |
| parse_status=$tmpdir/parse.status | |
| spin=0 | |
| : > "$json_file" | |
| ( | |
| fetch_nodes > "$json_file" | |
| code=$? | |
| printf '%s\n' "$code" > "$fetch_status" | |
| ) & | |
| fetch_pid=$! | |
| while [ ! -f "$fetch_status" ] | |
| do | |
| bytes=$(file_size "$json_file") | |
| draw_loading "Downloading publicnodes.json" "$bytes bytes received" "$spin" | |
| spin=$(( (spin + 1) % 4 )) | |
| [ -f "$fetch_status" ] && break | |
| sleep 1 | |
| done | |
| wait "$fetch_pid" 2>/dev/null | |
| set -- $(cat "$fetch_status" 2>/dev/null) | |
| code=${1:-1} | |
| if [ "$code" -ne 0 ]; then | |
| draw_loading "Download failed" "curl/wget exited with status $code" "$spin" | |
| sleep 2 | |
| return "$code" | |
| fi | |
| : > "$parsed_file" | |
| ( | |
| parse_nodes < "$json_file" > "$parsed_file" | |
| code=$? | |
| printf '%s\n' "$code" > "$parse_status" | |
| ) & | |
| parse_pid=$! | |
| while [ ! -f "$parse_status" ] | |
| do | |
| peers=$(line_count "$parsed_file") | |
| draw_loading "Parsing reachable peers" "$peers peers parsed" "$spin" | |
| spin=$(( (spin + 1) % 4 )) | |
| [ -f "$parse_status" ] && break | |
| sleep 1 | |
| done | |
| wait "$parse_pid" 2>/dev/null | |
| set -- $(cat "$parse_status" 2>/dev/null) | |
| code=${1:-1} | |
| if [ "$code" -ne 0 ]; then | |
| draw_loading "Parse failed" "awk exited with status $code" "$spin" | |
| sleep 2 | |
| return "$code" | |
| fi | |
| : > "$records_file" | |
| return 0 | |
| } | |
| get_size() { | |
| set -- $(stty size <&4 2>/dev/null) | |
| rows=${1:-24} | |
| cols=${2:-80} | |
| case $rows in | |
| ''|*[!0-9]*) rows=24 ;; | |
| esac | |
| case $cols in | |
| ''|*[!0-9]*) cols=80 ;; | |
| esac | |
| } | |
| selected_count() { | |
| line_count "$selected_file" | |
| } | |
| refresh_view() { | |
| sort_records "$records_file" > "$view_file" | |
| count=$(line_count "$view_file") | |
| if [ "$count" -eq 0 ]; then | |
| cursor=0 | |
| else | |
| cursor=1 | |
| fi | |
| top=1 | |
| } | |
| refresh_view_keep_cursor() { | |
| old_cursor=$cursor | |
| old_top=$top | |
| sort_records "$records_file" > "$view_file" | |
| count=$(line_count "$view_file") | |
| if [ "$count" -eq 0 ]; then | |
| cursor=0 | |
| top=1 | |
| return | |
| fi | |
| if [ "$old_cursor" -lt 1 ]; then | |
| cursor=1 | |
| elif [ "$old_cursor" -gt "$count" ]; then | |
| cursor=$count | |
| else | |
| cursor=$old_cursor | |
| fi | |
| if [ "$old_top" -lt 1 ]; then | |
| top=1 | |
| else | |
| top=$old_top | |
| fi | |
| } | |
| draw_table() { | |
| get_size | |
| count=$(line_count "$view_file") | |
| selected=$(selected_count) | |
| ping_note= | |
| list_rows=$((rows - 6)) | |
| if [ "$ping_total" -gt 0 ]; then | |
| measured=$(line_count "$records_file") | |
| if [ -n "$ping_status_file" ] && [ -f "$ping_status_file" ]; then | |
| ping_note=" Pinging: done" | |
| else | |
| ping_note=" Pinging: $measured/$ping_total" | |
| fi | |
| fi | |
| if [ "$list_rows" -lt 1 ]; then | |
| list_rows=1 | |
| fi | |
| if [ "$count" -eq 0 ]; then | |
| cursor=0 | |
| top=1 | |
| else | |
| if [ "$cursor" -lt 1 ]; then | |
| cursor=1 | |
| fi | |
| if [ "$cursor" -gt "$count" ]; then | |
| cursor=$count | |
| fi | |
| if [ "$top" -lt 1 ]; then | |
| top=1 | |
| fi | |
| if [ "$cursor" -lt "$top" ]; then | |
| top=$cursor | |
| fi | |
| bottom=$((top + list_rows - 1)) | |
| if [ "$cursor" -gt "$bottom" ]; then | |
| top=$((cursor - list_rows + 1)) | |
| fi | |
| fi | |
| printf '\033[H' >&3 | |
| printf '\033[1;36mYggdrasil public peer picker\033[0m\033[K\n' >&3 | |
| printf 'Sort: %-8s Peers: %-5s Selected: %-5s%s\033[K\n' "$sort_mode" "$count" "$selected" "$ping_note" >&3 | |
| printf 'Keys: j/k or arrows move | Space select | p/c/r sort | Enter accept | q/Esc quit\033[K\n' >&3 | |
| printf '\033[2m%-2s %-1s %6s %-16s %-7s %s\033[0m\033[K\n' '' '' 'Ping' 'Country' 'Proto' 'Address' >&3 | |
| if [ "$count" -eq 0 ]; then | |
| if [ "$ping_total" -gt 0 ]; then | |
| printf 'Testing local ping from this device...\033[K\n' >&3 | |
| else | |
| printf 'No reachable peers found.\033[K\n' >&3 | |
| fi | |
| cleared=1 | |
| while [ "$cleared" -lt "$list_rows" ] | |
| do | |
| printf '\033[K\n' >&3 | |
| cleared=$((cleared + 1)) | |
| done | |
| return | |
| fi | |
| awk -F '\t' \ | |
| -v top="$top" \ | |
| -v rows="$list_rows" \ | |
| -v cursor="$cursor" \ | |
| -v cols="$cols" \ | |
| -v selected_file="$selected_file" ' | |
| function fit(s, w) { | |
| if (w < 1) { | |
| return "" | |
| } | |
| if (length(s) > w) { | |
| return substr(s, 1, w - 1) "~" | |
| } | |
| return s | |
| } | |
| BEGIN { | |
| while ((getline s < selected_file) > 0) { | |
| sel[s] = 1 | |
| } | |
| close(selected_file) | |
| country_w = 16 | |
| proto_w = 7 | |
| addr_w = cols - 39 | |
| if (addr_w < 10) { | |
| addr_w = 10 | |
| } | |
| } | |
| NR < top { | |
| next | |
| } | |
| NR >= top + rows { | |
| exit | |
| } | |
| { | |
| ping = $1 | |
| country = fit($2, country_w) | |
| proto = fit($3, proto_w) | |
| addr = fit($4, addr_w) | |
| display_ping = ping | |
| if (display_ping == "999999") { | |
| display_ping = "-" | |
| } | |
| mark = " " | |
| if ($4 in sel) { | |
| mark = "*" | |
| } | |
| pointer = " " | |
| if (NR == cursor) { | |
| pointer = ">" | |
| printf "\033[7m" | |
| } | |
| printf "%-2s %-1s %6s %-16s %-7s %s", pointer, mark, display_ping, country, proto, addr | |
| if (NR == cursor) { | |
| printf "\033[0m" | |
| } | |
| printf "\033[K\n" | |
| printed++ | |
| } | |
| END { | |
| while (printed < rows) { | |
| printf "\033[K\n" | |
| printed++ | |
| } | |
| } | |
| ' "$view_file" >&3 | |
| } | |
| current_address() { | |
| awk -F '\t' -v n="$cursor" 'NR == n { print $4; exit }' "$view_file" | |
| } | |
| toggle_current() { | |
| addr=$(current_address) | |
| [ -n "$addr" ] || return | |
| if grep -F -x -e "$addr" "$selected_file" >/dev/null 2>&1; then | |
| grep -F -x -v -e "$addr" "$selected_file" > "$selected_tmp" | |
| mv "$selected_tmp" "$selected_file" | |
| else | |
| printf '%s\n' "$addr" >> "$selected_file" | |
| fi | |
| } | |
| print_selected_addresses() { | |
| awk -F '\t' -v selected_file="$selected_file" ' | |
| BEGIN { | |
| while ((getline s < selected_file) > 0) { | |
| sel[s] = 1 | |
| } | |
| close(selected_file) | |
| } | |
| $4 in sel { | |
| print $4 | |
| } | |
| ' "$view_file" | |
| } | |
| run_tui() { | |
| selected_file=$tmpdir/selected.txt | |
| selected_tmp=$tmpdir/selected.tmp | |
| view_file=$tmpdir/view.tsv | |
| cursor=1 | |
| top=1 | |
| action=quit | |
| : > "$selected_file" | |
| if ! start_measure_records_background "$parsed_file" "$records_file"; then | |
| draw_loading "Cannot test local latency" "ping command was not found" 0 | |
| sleep 2 | |
| return 1 | |
| fi | |
| refresh_view_keep_cursor | |
| start_raw_input | |
| last_records_count=$(line_count "$records_file") | |
| if [ -n "$ping_status_file" ] && [ -f "$ping_status_file" ]; then | |
| last_ping_done=1 | |
| else | |
| last_ping_done=0 | |
| fi | |
| draw_table | |
| while : | |
| do | |
| key=$(read_key_poll) | |
| redraw=0 | |
| records_count=$(line_count "$records_file") | |
| if [ -n "$ping_status_file" ] && [ -f "$ping_status_file" ]; then | |
| ping_done=1 | |
| else | |
| ping_done=0 | |
| fi | |
| if [ "$records_count" -ne "$last_records_count" ] || [ "$ping_done" -ne "$last_ping_done" ]; then | |
| refresh_view_keep_cursor | |
| last_records_count=$records_count | |
| last_ping_done=$ping_done | |
| redraw=1 | |
| fi | |
| if [ "$key" = NONE ]; then | |
| if [ "$redraw" -eq 1 ]; then | |
| draw_table | |
| fi | |
| continue | |
| fi | |
| case $key in | |
| UP) | |
| if [ "$cursor" -gt 1 ]; then | |
| cursor=$((cursor - 1)) | |
| redraw=1 | |
| fi | |
| ;; | |
| DOWN) | |
| count=$(line_count "$view_file") | |
| if [ "$cursor" -lt "$count" ]; then | |
| cursor=$((cursor + 1)) | |
| redraw=1 | |
| fi | |
| ;; | |
| SPACE) | |
| toggle_current | |
| redraw=1 | |
| ;; | |
| SORT_PING) | |
| sort_mode=ping | |
| refresh_view | |
| redraw=1 | |
| ;; | |
| SORT_COUNTRY) | |
| sort_mode=country | |
| refresh_view | |
| redraw=1 | |
| ;; | |
| SORT_PROTO) | |
| sort_mode=proto | |
| refresh_view | |
| redraw=1 | |
| ;; | |
| ENTER) | |
| action=accept | |
| break | |
| ;; | |
| QUIT) | |
| action=quit | |
| break | |
| ;; | |
| esac | |
| if [ "$redraw" -eq 1 ]; then | |
| draw_table | |
| fi | |
| done | |
| leave_ui | |
| stop_ping_worker | |
| if [ "$action" = accept ]; then | |
| print_selected_addresses | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| make_tmpdir | |
| if [ "$records_mode" -eq 1 ]; then | |
| build_records_plain | |
| sort_records "$records_file" | |
| exit 0 | |
| fi | |
| open_tty | |
| enter_ui | |
| load_records_with_ui || exit 1 | |
| run_tui |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment