# 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 "
" "" "" 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 "" "" -> 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 "
" "" 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" }