# Pure-bash prompt helpers — no external TUI, no dependencies. # # Every prompt shows its default, re-asks on invalid input, and lets Ctrl+C # abort cleanly (the trap is installed in deploy.sh). Results are returned via # the REPLY-style globals documented on each function. # -- colours (disabled if not a TTY or NO_COLOR set) ----------------------- if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m' C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_RED=$'\033[31m' else C_RESET=''; C_BOLD=''; C_DIM=''; C_CYAN=''; C_GREEN=''; C_YELLOW=''; C_RED='' fi ui_title() { printf '\n%s%s%s\n' "$C_BOLD$C_CYAN" "$1" "$C_RESET"; } ui_info() { printf '%s\n' "$1"; } ui_dim() { printf '%s%s%s\n' "$C_DIM" "$1" "$C_RESET"; } ui_ok() { printf '%s✓ %s%s\n' "$C_GREEN" "$1" "$C_RESET"; } ui_warn() { printf '%s! %s%s\n' "$C_YELLOW" "$1" "$C_RESET"; } ui_err() { printf '%s✗ %s%s\n' "$C_RED" "$1" "$C_RESET" >&2; } # ui_input PROMPT DEFAULT [VALIDATOR_FN] # Sets UI_VALUE. VALIDATOR_FN (optional) receives the candidate value and # returns 0 to accept; on rejection it should print why (to stderr). ui_input() { local prompt="$1" default="${2:-}" validator="${3:-}" ans while true; do if [[ -n "$default" ]]; then read -r -p "$prompt [${C_DIM}${default}${C_RESET}]: " ans || exit 130 ans="${ans:-$default}" else read -r -p "$prompt: " ans || exit 130 fi if [[ -n "$validator" ]]; then if ! "$validator" "$ans"; then continue; fi fi UI_VALUE="$ans" return 0 done } # ui_secret PROMPT — no echo. Sets UI_VALUE. ui_secret() { local prompt="$1" ans read -r -s -p "$prompt: " ans || exit 130 echo UI_VALUE="$ans" } # ui_yesno PROMPT DEFAULT(yes|no) — sets UI_BOOL to "yes"/"no", returns 0/1. ui_yesno() { local prompt="$1" default="${2:-yes}" ans hint [[ "$default" == "yes" ]] && hint="[Y/n]" || hint="[y/N]" while true; do read -r -p "$prompt $hint: " ans || exit 130 ans="${ans:-$default}" case "${ans,,}" in y|yes|да|д) UI_BOOL="yes"; return 0 ;; n|no|нет|н) UI_BOOL="no"; return 1 ;; *) ui_warn "$(t invalid_input)" ;; esac done } # ui_select PROMPT "label1" "label2" ... # Numbered single-choice menu. Sets UI_INDEX (1-based) and UI_VALUE (label). ui_select() { local prompt="$1"; shift local -a opts=("$@") local i ans ui_info "$prompt" for i in "${!opts[@]}"; do printf ' %s%d%s) %s\n' "$C_CYAN" "$((i + 1))" "$C_RESET" "${opts[$i]}" done while true; do read -r -p "> [${C_DIM}1${C_RESET}]: " ans || exit 130 ans="${ans:-1}" if [[ "$ans" =~ ^[0-9]+$ ]] && ((ans >= 1 && ans <= ${#opts[@]})); then UI_INDEX="$ans" UI_VALUE="${opts[$((ans - 1))]}" return 0 fi ui_warn "$(t invalid_input)" done } # ui_multiselect PROMPT "key1:label1:on" "key2:label2:off" "key3:label3:locked" # Toggle items by number; 'locked' items cannot be turned off. Empty line # confirms. Sets UI_SELECTED (space-separated keys that ended up on). ui_multiselect() { local prompt="$1"; shift local -a keys=() labels=() states=() locks=() local spec key label state for spec in "$@"; do key="${spec%%:*}"; spec="${spec#*:}" label="${spec%%:*}"; state="${spec#*:}" keys+=("$key"); labels+=("$label") if [[ "$state" == "locked" ]]; then states+=("on"); locks+=("yes") else states+=("$state"); locks+=("no"); fi done local i ans while true; do ui_info "$prompt" for i in "${!keys[@]}"; do local mark="[ ]"; [[ "${states[$i]}" == "on" ]] && mark="[x]" local lock=""; [[ "${locks[$i]}" == "yes" ]] && lock=" ${C_DIM}(required)${C_RESET}" printf ' %s%d%s) %s %s%s\n' "$C_CYAN" "$((i + 1))" "$C_RESET" "$mark" "${labels[$i]}" "$lock" done ui_dim "$(t enter_to_keep) — number toggles, Enter confirms" read -r -p "> " ans || exit 130 [[ -z "$ans" ]] && break if [[ "$ans" =~ ^[0-9]+$ ]] && ((ans >= 1 && ans <= ${#keys[@]})); then local idx=$((ans - 1)) if [[ "${locks[$idx]}" == "yes" ]]; then ui_warn "$(t backend_required)" elif [[ "${states[$idx]}" == "on" ]]; then states[$idx]="off" else states[$idx]="on"; fi else ui_warn "$(t invalid_input)" fi done UI_SELECTED="" for i in "${!keys[@]}"; do [[ "${states[$i]}" == "on" ]] && UI_SELECTED+="${keys[$i]} " done UI_SELECTED="${UI_SELECTED% }" } # -- common validators (for ui_input) -------------------------------------- v_nonempty() { [[ -n "$1" ]] || { ui_warn "$(t invalid_input)"; return 1; }; } v_port() { [[ "$1" =~ ^[0-9]+$ ]] && (($1 >= 1 && $1 <= 65535)) || { ui_warn "$(t invalid_input)"; return 1; }; } v_url() { [[ "$1" =~ ^https?:// ]] || { ui_warn "$(t invalid_input)"; return 1; }; } v_redis() { [[ "$1" =~ ^rediss?:// ]] || { ui_warn "$(t invalid_input)"; return 1; }; } v_domain() { [[ "$1" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] || { ui_warn "$(t invalid_input)"; return 1; }; }