gum TUI with plain-text fallback (lib/ui.sh)
Interactive install gets styled menus, confirms, multi-select and a spinner around the system update when gum is available (dnf install gum, installed optionally in the base section); every prompt falls back to the classic numbered/yn prompts otherwise, and --yes runs stay 100% non-interactive. Call sites moved to ui_choose/ui_confirm/ui_choose_multi/ui_run: DE + TWM menus (defaults preserved: none/none), SELinux + dotfiles questions, advanced-menu category + multi-select app picker, reboot prompt. Dead ask_choice/ask_yes_no removed. Two bugs caught by pty-based harness testing before shipping: - stdout is a pipe inside $(ui_...), so have_gum must test stdin + /dev/tty, never -t 1 (gum would never have fired on a real term). - fallback display output must go to >&2 — a stray newline on stdout made choice='\nno' and broke every case match.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# shellcheck shell=bash
|
||||
#------------------------------------------
|
||||
# lib/ui.sh — gum-based TUI with plain-text fallback
|
||||
#
|
||||
# Every ui_* helper has three behaviors:
|
||||
# 1. ASSUME_YES (unattended) -> returns the default, no UI at all
|
||||
# 2. gum available + a TTY -> styled TUI (menus, confirms, spinners)
|
||||
# 3. otherwise -> the classic numbered prompts
|
||||
#
|
||||
# The wrappers print the ANSWER on stdout (like the old ask_* did), so call
|
||||
# sites never change based on which mode is active.
|
||||
#------------------------------------------
|
||||
|
||||
if [[ -n "${_SB_UI_LOADED:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
_SB_UI_LOADED=1
|
||||
|
||||
have_gum() {
|
||||
command -v gum >/dev/null 2>&1 || return 1
|
||||
# stdin must be interactive — it is inherited by $(...) subshells, so
|
||||
# menus still get the real terminal. stdout must NOT be checked: it is
|
||||
# a pipe by definition inside $(ui_choose ...). gum draws its UI on
|
||||
# /dev/tty and prints only the answer to stdout (which we capture).
|
||||
[ -t 0 ] || return 1
|
||||
[ -w /dev/tty ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Single choice from a list.
|
||||
# ui_choose "<header>" "<prompt>" "<default>" item1 item2 ...
|
||||
# prints the chosen item verbatim; prints the default when unattended.
|
||||
ui_choose() {
|
||||
local header="$1" prompt="$2" default="$3"
|
||||
shift 3
|
||||
local items=("$@")
|
||||
|
||||
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||
echo "$default"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if have_gum; then
|
||||
# gum choose returns 130 on Esc/Ctrl-C; treat as default rather than
|
||||
# killing the whole install.
|
||||
local sel
|
||||
sel=$(gum choose --header "$header"$'\n'"$prompt" "${items[@]}") || sel="$default"
|
||||
echo "${sel:-$default}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# classic fallback: numbered list. ALL display goes to stderr — stdout is
|
||||
# captured by the caller ($(ui_choose ...)) and must carry ONLY the answer
|
||||
# (a stray printf newline once made choice='\nno' and broke every case match).
|
||||
printf "\n" >&2
|
||||
color_cyan "$header" >&2
|
||||
local i=1 it
|
||||
for it in "${items[@]}"; do
|
||||
echo " $i) $it" >&2
|
||||
i=$((i+1))
|
||||
done
|
||||
local n=${#items[@]} answer
|
||||
while true; do
|
||||
if ! read -t 90 -rp "$prompt [1-$n] (default: shown above): " answer; then
|
||||
printf "\n"; answer=""
|
||||
fi
|
||||
# empty/timeout -> the default item (its position in the list)
|
||||
if [[ -z "$answer" ]]; then
|
||||
local k=0
|
||||
for it in "${items[@]}"; do
|
||||
k=$((k+1))
|
||||
[[ "$it" == "$default" ]] && { echo "$it"; return 0; }
|
||||
done
|
||||
echo "${items[-1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= n )); then
|
||||
echo "${items[$((answer-1))]}"
|
||||
return 0
|
||||
fi
|
||||
err "Invalid choice: $answer"
|
||||
done
|
||||
}
|
||||
|
||||
# Yes/no. ui_confirm "<prompt>" "<yes|no default>" -> prints yes/no
|
||||
ui_confirm() {
|
||||
local prompt="$1" default="${2:-no}"
|
||||
|
||||
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||
echo "$default"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if have_gum; then
|
||||
if gum confirm "$prompt"; then
|
||||
echo "yes"
|
||||
else
|
||||
echo "no"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "\n" >&2
|
||||
read -t 60 -rp "$prompt [y/n] (default: $default): " answer || printf "\n" >&2
|
||||
answer="${answer,,}"
|
||||
[[ -z "$answer" ]] && answer="$default"
|
||||
case "$answer" in
|
||||
y|yes) echo "yes" ;;
|
||||
*) echo "no" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Multi-select from a list (one per line on stdout; empty = none).
|
||||
# ui_choose_multi "<header>" "<prompt>" item1 item2 ...
|
||||
ui_choose_multi() {
|
||||
local header="$1" prompt="$2"
|
||||
shift 2
|
||||
local items=("$@")
|
||||
|
||||
if [[ "$ASSUME_YES" == "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if have_gum; then
|
||||
gum choose --no-limit --header "$header"$'\n'"$prompt" "${items[@]}" || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "\n" >&2
|
||||
color_cyan "$header" >&2
|
||||
local i=1 it
|
||||
for it in "${items[@]}"; do
|
||||
echo " $i) $it" >&2
|
||||
i=$((i+1))
|
||||
done
|
||||
echo " x) None / back" >&2
|
||||
local sel picks=() pick
|
||||
read -t 90 -rp "Pick one or more (comma-separate, default: none): " sel || printf "\n"
|
||||
[[ "${sel,,}" == "x" || -z "$sel" ]] && return 0
|
||||
IFS=',' read -ra picks <<< "$sel"
|
||||
for pick in "${picks[@]}"; do
|
||||
pick="${pick// /}"
|
||||
[[ "$pick" =~ ^[0-9]+$ ]] || continue
|
||||
(( pick >= 1 && pick <= ${#items[@]} )) && echo "${items[$((pick-1))]}"
|
||||
done
|
||||
}
|
||||
|
||||
# Spinner around a long command. Falls back to running it plainly.
|
||||
# ui_run "Updating system..." sudo dnf update -y
|
||||
ui_run() {
|
||||
local msg="$1"
|
||||
shift
|
||||
|
||||
if have_gum; then
|
||||
if gum spin --title "$msg" --show-output -- "$@"; then
|
||||
ok "$msg"
|
||||
else
|
||||
local rc=$?
|
||||
color_red " Failed: $msg"
|
||||
return "$rc"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "$msg"
|
||||
"$@"
|
||||
local rc=$?
|
||||
if (( rc == 0 )); then
|
||||
ok "$msg"
|
||||
else
|
||||
color_red " Failed: $msg"
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
Reference in New Issue
Block a user