ui_quiet_run wraps every install_packages/install_optional/group install/remove and the system update: while gum is available and the run is interactive, dnf output is hidden behind a spinner, then a single 'Installing: <pkgs> (N packages)' line. Full output is appended to $SETUP_DNF_LOG (/tmp/fedora-setup-dnf-<uid>.log); on failure the last 20 lines print in red so nothing is lost. Unattended and no-gum runs keep plain full output (logs/cron contract). Also: fallback ui_choose accepts 'x' to exit (muscle memory from the old menu), fixed a stray stdout newline leaking into captured answers (same class as the earlier '\nno' bug), fixed pluralization. Harness: piped fallback, pty gum run incl. a failing VirtualBox pick (red failure + log tail, menu survives, subsequent qemu install ok), unattended regression.
171 lines
4.7 KiB
Bash
171 lines
4.7 KiB
Bash
# 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 lives in lib/common.sh (install_packages needs it too).
|
|
|
|
# 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" >&2
|
|
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,,}" == "x" ]]; then
|
|
echo "$default"
|
|
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"
|
|
}
|