#!/bin/bash # shellcheck shell=bash # RetroBIOS screens, drawn by PortMaster's own GUI (pugwash fifo_control) # through the functions of PortMasterDialog.txt. Sourced by RetroBIOS.sh # after PortMasterDialog.txt and tui.txt. # # They are the port's fallback: RetroBIOS.sh opens them only when no terminal # can show install.py --tui, that is under a compositor that has neither foot # nor xterm, or when the interface refuses the console it was given. # # Only what PortMaster 2024.02.26 and the 8.5.23 copy JELOS and KOLOS ship # is used: PortMasterDialogInit, PortMasterDialog, PortMasterDialogResult, # PortMasterDialogExit, PortMasterIPCheck, and the commands # register_set_info, register_clear, selection_list, messages_begin, # message, messages_end, progress, progress_clear and check_runtime. # Those releases ignore --want-cancel, so every list ends on its own Back # or Quit entry. They also read "\n" literally, so no text has a newline, # and their wait for an answer gives up after 30 seconds: rb_answer keeps # waiting while pugwash is alive. # # pugwash starts in "no-check" mode, never "no-harbour": only with # HarbourMaster loaded does it apply the A/B fix PortMaster keeps for Miyoo, # TrimUI, Knulli and the Xbox-layout ArkOS and AmberELEC pads. # # Inputs: GAMEDIR, controlfolder, ESUDO, and what tui.txt defines # (RB_INSTALL, RB_STATE, RB_LOG, rb_py, rb_online, rb_find_pack). RB_EVENTS="${RB_EVENTS:-$GAMEDIR/rbevents.py}" RB_SOURCE_ARGS=() # Read by tui.txt's rb_python_text. # shellcheck disable=SC2034 RB_PYTHON_PROBLEM="${RB_PYTHON_PROBLEM:-}" # --- Python runtime ------------------------------------------------------- # tui.txt's rb_runtime calls this when the runtime is not on the card yet: # PortMaster downloads it the way its own ports get theirs. rb_fetch_runtime() { local squashfs="$1" answer if [ -z "$(PortMasterIPCheck)" ]; then RB_PYTHON_PROBLEM="runtime_offline" return 1 fi PortMasterDialogInit "no-check" PortMasterDialog "messages_begin" PortMasterDialog "message" "This system's Python is older than 3.8. Downloading PortMaster's $RB_RUNTIME runtime." answer=$(rb_answer "check_runtime" "$RB_RUNTIME.squashfs") PortMasterDialog "messages_end" PortMasterDialogExit if [ "$answer" != "OKAY" ] || [ ! -f "$squashfs" ]; then RB_PYTHON_PROBLEM="runtime_failed" return 1 fi return 0 } # --- Dialog primitives ---------------------------------------------------- # A command that waits for the user, and its answer. PortMasterDialogResult # stops waiting after 30 seconds on the older releases and then returns # WAIT: the answer is read again until pugwash writes one or exits. rb_answer() { local answer tries=0 answer=$(PortMasterDialogResult "$@") while [ "$answer" = "WAIT" ] || [ -z "$answer" ]; do if [ -n "$PM_PID" ] && [ ! -d "/proc/$PM_PID" ]; then break fi if [ "$tries" -ge 6000 ]; then break fi tries=$((tries + 1)) sleep 0.3 answer=$(cat "$PM_DONE" 2>/dev/null) done printf '%s\n' "$answer" } rb_plural() { if [ "$1" = "1" ]; then printf '%s %s\n' "$1" "$2" else printf '%s %ss\n' "$1" "$2" fi } rb_entry() { PortMasterDialog "register_set_info" "$1" "$2" "title:$3" "description:$4" } # Show a list and print the chosen key. B, where the release reads it, # answers like the Back or Quit entry ($2). rb_choose() { local answer answer=$(rb_answer "selection_list" "--register=$1" "--want-description" "--want-cancel") case "$answer" in CANCEL|FAIL|WAIT|"") printf '%s\n' "$2" ;; *) printf '%s\n' "$answer" ;; esac } # A screen of text with one or two ways out: rb_notice TITLE TEXT KEY LABEL [KEY LABEL]. # B answers like the last one, which leaves. rb_notice() { local title="$1" text="$2" shift 2 local last="$1" answer PortMasterDialog "register_clear" "notice" # The list has no heading of its own: the first entry is the heading. rb_entry "notice" "info" "$title" "$text" rb_entry "notice" "$1" "$2" "$text" if [ $# -ge 4 ]; then rb_entry "notice" "$3" "$4" "$text" last="$3" fi while true; do answer=$(rb_choose "notice" "$last") if [ "$answer" != "info" ]; then printf '%s\n' "$answer" return fi done } # --- Reading install.py --------------------------------------------------- # Run a --check and read its records into RB_* variables. Without a network # the files come from a pack on the card, when there is one. rb_check() { mkdir -p "$RB_STATE" PortMasterDialog "messages_begin" PortMasterDialog "message" "Reading the BIOS folder and the file list." RB_SOURCE_ARGS=() if ! rb_online; then local pack pack=$(rb_find_pack) [ -n "$pack" ] && RB_SOURCE_ARGS=(--from-pack "$pack") fi rb_py "$RB_INSTALL" --check --events "${RB_SOURCE_ARGS[@]}" "${RB_PLATFORM_ARGS[@]}" > "$RB_STATE/check.jsonl" 2>> "$RB_LOG" PortMasterDialog "messages_end" rb_py "$RB_EVENTS" status "$RB_STATE/check.jsonl" > "$RB_STATE/status.tsv" 2>> "$RB_LOG" rb_read_status "$RB_STATE/status.tsv" } rb_read_status() { RB_OS_NAME="this system" RB_DETECTED="" RB_PLATFORM="" RB_PLATFORM_NAME="" RB_BIOS_DIR="" RB_ERROR="" RB_ERROR_TEXT="" RB_CHECKED="" RB_END="none" RB_SYSTEMS=() RB_COMPLETE=0 local kind a b c d e f g h while IFS=$'\t' read -r kind a b c d e f g h; do case "$kind" in detect) RB_OS_NAME="$a"; RB_DETECTED="$b" ;; platform) RB_PLATFORM="$a"; RB_PLATFORM_NAME="$b"; RB_BIOS_DIR="$c" ;; error) RB_ERROR="$a"; RB_ERROR_TEXT="$b" ;; check) RB_CHECKED=1 RB_FILES="$a"; RB_PRESENT="$b"; RB_MISSING="$c"; RB_WRONG="$d" RB_NEEDED_SIZE="$e"; RB_DATA_MISSING="$f"; RB_NOT_INSTALLABLE="$g" ;; system) if [ "$h" = "1" ]; then RB_SYSTEMS+=("$a"$'\t'"$b"$'\t'"$c"$'\t'"$d"$'\t'"$e"$'\t'"$f"$'\t'"$g") else RB_COMPLETE=$((RB_COMPLETE + 1)) fi ;; end) RB_END="$a" ;; esac done < "$1" } rb_status_text() { local text="$RB_OS_NAME, $RB_PLATFORM_NAME file list: $RB_PRESENT/$RB_FILES BIOS files, $RB_MISSING missing, $RB_WRONG wrong hash." if [ "${RB_NOT_INSTALLABLE:-0}" != "0" ]; then text="$text $RB_NOT_INSTALLABLE not in the collection." fi printf '%s\n' "$text" } rb_pending() { printf '%s\n' $((RB_MISSING + RB_WRONG + RB_DATA_MISSING)) } # --- Screens -------------------------------------------------------------- rb_screen_no_python() { rb_notice "Python 3.8 or newer is needed" "$(rb_python_text)" "quit" "Quit" > /dev/null } # What the check found when it found nothing to show: no platform, no # connection, an unreadable list. Prints check or quit. rb_screen_problem() { if [ "$RB_END" = "none" ]; then rb_notice "The installer stopped before checking" "install.py ended without a result. Its messages are in $RB_LOG." "check" "Check again" "quit" "Quit" elif [ "$RB_ERROR" = "conflict" ]; then rb_notice "Two systems found" "$RB_ERROR_TEXT, so the BIOS folder is not known. $RB_PC_ROUTE" "check" "Check again" "quit" "Quit" elif [ "$RB_DETECTED" = "0" ]; then rb_notice "No BIOS folder found" "install.py knows no BIOS folder on $RB_OS_NAME. $RB_PC_ROUTE" "check" "Check again" "quit" "Quit" elif [ "$RB_ERROR" = "network" ]; then rb_notice "No connection" "$RB_OS_NAME, BIOS folder $RB_BIOS_DIR. $(rb_pack_text)" "check" "Check again" "quit" "Quit" elif [ "$RB_ERROR" = "invalid" ]; then rb_notice "The file list is newer than this port" "$RB_ERROR_TEXT. Update RetroBIOS from PortMaster, or use the installer on a computer. $RB_PC_ROUTE" "check" "Check again" "quit" "Quit" else rb_notice "The file list could not be read" "${RB_ERROR_TEXT:-install.py ended with code $RB_END}. Its messages are in $RB_LOG." "check" "Check again" "quit" "Quit" fi } # The menu after a check. Prints update, choose, check or quit. rb_screen_menu() { local status pending status=$(rb_status_text) pending=$(rb_pending) PortMasterDialog "register_clear" "menu" # The list has no heading of its own: the first entry is the status. if [ "$pending" -gt 0 ]; then local data="" if [ "${RB_DATA_MISSING:-0}" != "0" ]; then data=" and $RB_DATA_MISSING data files" fi rb_entry "menu" "status" "$RB_OS_NAME: $RB_PRESENT/$RB_FILES BIOS files, $RB_MISSING missing, $RB_WRONG wrong hash" "$status BIOS folder: $RB_BIOS_DIR." rb_entry "menu" "update" "Update everything missing" "$status Downloads $(rb_plural $((RB_MISSING + RB_WRONG)) file)$data, $RB_NEEDED_SIZE, to $RB_BIOS_DIR." rb_entry "menu" "choose" "Choose systems" "$status Pick the systems to update." else rb_entry "menu" "status" "$RB_OS_NAME: all $RB_FILES BIOS files present" "$status Nothing to download. BIOS folder: $RB_BIOS_DIR." fi rb_entry "menu" "check" "Check again" "$status Reads the BIOS folder and the file list again." rb_entry "menu" "quit" "Quit" "$status" local answer while true; do answer=$(rb_choose "menu" "quit") if [ "$answer" != "status" ]; then printf '%s\n' "$answer" return fi done } # Systems with files to fetch, toggled with A. Prints the chosen system # ids, one per line, or nothing for Back. rb_screen_systems() { local -A chosen=() local line id name files present missing wrong size answer count while true; do count=0 for id in "${!chosen[@]}"; do count=$((count + 1)) done PortMasterDialog "register_clear" "systems" if [ "$count" -gt 0 ]; then rb_entry "systems" "update" "Update the $count selected" "Downloads what the selected systems lack to $RB_BIOS_DIR." else rb_entry "systems" "update" "Update the selected" "Nothing selected yet: pick systems below with A." fi rb_entry "systems" "back" "Back" "Back to the menu. $(rb_plural "$RB_COMPLETE" system) complete, not listed." for line in "${RB_SYSTEMS[@]}"; do IFS=$'\t' read -r id name files present missing wrong size <<< "$line" local mark="" if [ -n "${chosen[$id]}" ]; then mark=" (selected)" fi rb_entry "systems" "sys:$id" "$name$mark" "$present/$files present, $missing missing, $wrong wrong hash, $size to download. A selects or clears it." done answer=$(rb_choose "systems" "back") case "$answer" in back) return ;; update) if [ "$count" -gt 0 ]; then printf '%s\n' "${!chosen[@]}" | sort return fi ;; sys:*) id="${answer#sys:}" # Quoted literally so unset expands the subscript once: a double-quoted # "chosen[$id]" is expanded again inside unset. [[ "$id" =~ ^[A-Za-z0-9._-]+$ ]] || continue if [ -n "${chosen[$id]}" ]; then unset 'chosen[$id]' else chosen[$id]=1 fi ;; esac done } # Download with a progress bar fed by the event stream: rb_update [SYSTEM...] rb_update() { local args=() system for system in "$@"; do args+=(--system "$system") done mkdir -p "$RB_STATE" PortMasterDialog "messages_begin" PortMasterDialog "message" "Downloading to $RB_BIOS_DIR." PortMasterDialog "progress" "Starting" 0 1 local kind a b c d e f rb_py "$RB_INSTALL" --events "${RB_SOURCE_ARGS[@]}" "${RB_PLATFORM_ARGS[@]}" "${args[@]}" 2>> "$RB_LOG" \ | rb_py "$RB_EVENTS" follow "$RB_STATE/summary.tsv" 2>> "$RB_LOG" \ | while IFS=$'\t' read -r kind a b c d e f; do case "$kind" in download) PortMasterDialog "message" "$(rb_plural "$a" file), $b." ;; file) PortMasterDialog "progress" "$d, $e of $f" "$a" "$b" ;; progress) PortMasterDialog "progress" "$c of $d" "$a" "$b" ;; error) PortMasterDialog "message" "$b" ;; esac done PortMasterDialog "progress_clear" PortMasterDialog "messages_end" } # What the update did. Prints back or quit. rb_screen_summary() { local kind a b c d shown=0 failed=0 kept=0 local end="none" installed=0 up_to_date=0 pending=0 interrupted=0 error="" while IFS=$'\t' read -r kind a b c d; do case "$kind" in summary) installed="$a"; up_to_date="$b"; pending="$c"; interrupted="$d" ;; failed) failed=$((failed + 1)) ;; error) error="$b" ;; end) end="$a" ;; esac done < "$RB_STATE/summary.tsv" PortMasterDialog "register_clear" "summary" # The first entry is the outcome, the heading the list does not have. if [ "$end" = "none" ]; then rb_entry "summary" "info:result" "The installer stopped" "install.py ended without a result. Its messages are in $RB_LOG." elif [ -n "$error" ]; then rb_entry "summary" "info:result" "Nothing downloaded" "$error." else rb_entry "summary" "info:result" "Installed: $(rb_plural "$installed" file)" "$up_to_date already up to date, $failed failed, $pending still to fetch." fi rb_entry "summary" "back" "Back to the menu" "Checks the BIOS folder again." rb_entry "summary" "quit" "Quit" "Leaves RetroBIOS." if [ "$interrupted" = "1" ]; then rb_entry "summary" "info:stopped" "Stopped before the end" "Update again to carry on. Files already installed are kept." fi while IFS=$'\t' read -r kind a b c d; do case "$kind" in failed) shown=$((shown + 1)) if [ "$shown" -le 50 ]; then rb_entry "summary" "info:failed:$shown" "Failed: $a" "$b" fi ;; kept) kept=$((kept + 1)) rb_entry "summary" "info:kept:$kept" "Kept $(rb_plural "$b" file) you had ($c)" "The collection's copies replaced them. Yours are in $a. Copy what that folder holds into the folder above retrobios-backup to put them back." ;; skipped) rb_entry "summary" "info:skipped:$a" "Not in the collection ($a): $(rb_plural "$b" file)" "Required: $c. The RetroBIOS collection does not hold these files, so they cannot be downloaded." ;; esac done < "$RB_STATE/summary.tsv" if [ "$shown" -gt 50 ]; then rb_entry "summary" "info:more" "And $((shown - 50)) more failed" "The full list is in $RB_LOG." fi local answer while true; do answer=$(rb_choose "summary" "back") case "$answer" in back|quit) printf '%s\n' "$answer"; return ;; esac done } # --- Flow ----------------------------------------------------------------- # The screens, once rb_select_python has run. rb_main() { PortMasterDialogInit "no-check" if [ -z "$RB_PYTHON" ]; then rb_screen_no_python PortMasterDialogExit return 1 fi RB_PLATFORM_ARGS=() local action systems chosen rb_check while true; do if [ -z "$RB_CHECKED" ]; then action=$(rb_screen_problem) else # Later runs name the platform the first one found, so a second # detected platform never changes what the screens describe. RB_PLATFORM_ARGS=(--platform "$RB_PLATFORM") action=$(rb_screen_menu) fi case "$action" in update) rb_update [ "$(rb_screen_summary)" = "quit" ] && break rb_check ;; choose) systems=$(rb_screen_systems) if [ -n "$systems" ]; then mapfile -t chosen <<< "$systems" rb_update "${chosen[@]}" [ "$(rb_screen_summary)" = "quit" ] && break rb_check fi ;; check) rb_check ;; *) break ;; esac done PortMasterDialogExit return 0 }