mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-07-31 07:49:07 -04:00
- Deleted the backup.sh and deploy.sh scripts as they are no longer needed. - Updated documentation to reflect the removal of these scripts and their functionalities. - Cleaned up related GitHub Actions workflows to remove references to the deleted scripts.
418 lines
10 KiB
Bash
Executable File
418 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
||
# Cross-distro TUI: gum (real terminals) | styled bash (IDE) | plain fallback.
|
||
set -euo pipefail
|
||
|
||
TUI_BACKEND="bash"
|
||
GUM_STYLE=false
|
||
|
||
has_gum() {
|
||
command -v gum &>/dev/null
|
||
}
|
||
|
||
terminal_supports_gum_interactive() {
|
||
has_gum || return 1
|
||
[[ -t 0 && -t 1 ]] || return 1
|
||
[[ -n "${TERM:-}" && "${TERM:-}" != "dumb" ]] || return 1
|
||
case "${TERM_PROGRAM:-}" in
|
||
vscode|Visual\ Studio\ Code) return 1 ;;
|
||
esac
|
||
[[ -n "${VSCODE_GIT_IPC_HANDLE:-}" ]] && return 1
|
||
[[ -n "${CURSOR_TRACE_ID:-}" ]] && return 1
|
||
[[ -n "${CURSOR_AGENT:-}" ]] && return 1
|
||
return 0
|
||
}
|
||
|
||
detect_tui_backend() {
|
||
if [[ -n "${ADVENTURELOG_TUI:-}" ]]; then
|
||
TUI_BACKEND="$ADVENTURELOG_TUI"
|
||
return 0
|
||
fi
|
||
if terminal_supports_gum_interactive; then
|
||
TUI_BACKEND="gum"
|
||
elif has_gum; then
|
||
TUI_BACKEND="styled"
|
||
elif command -v whiptail &>/dev/null; then
|
||
TUI_BACKEND="whiptail"
|
||
elif command -v dialog &>/dev/null; then
|
||
TUI_BACKEND="dialog"
|
||
else
|
||
TUI_BACKEND="bash"
|
||
fi
|
||
}
|
||
|
||
offer_gum_install() {
|
||
if has_gum; then
|
||
return 0
|
||
fi
|
||
tui_print_box "Enhanced UI" \
|
||
"Install gum for polished menus, spinners, and progress bars.\nhttps://github.com/charmbracelet/gum"
|
||
if ! tui_confirm_bash "Install gum now?" "y"; then
|
||
log_muted "Continuing with built-in styled UI."
|
||
return 1
|
||
fi
|
||
if [[ "$OSTYPE" == "darwin"* ]] && command -v brew &>/dev/null; then
|
||
tui_spinner_bash "Installing gum via Homebrew" brew install gum
|
||
elif command -v apt-get &>/dev/null; then
|
||
tui_spinner_bash "Installing gum via apt" bash -c '
|
||
sudo mkdir -p /etc/apt/keyrings &&
|
||
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg &&
|
||
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list >/dev/null &&
|
||
sudo apt-get update -qq &&
|
||
sudo apt-get install -y gum
|
||
'
|
||
else
|
||
log_warning "Automatic gum install is not supported on this OS."
|
||
log_muted "See https://github.com/charmbracelet/gum#installation"
|
||
return 1
|
||
fi
|
||
has_gum
|
||
}
|
||
|
||
init_tui() {
|
||
GUM_STYLE=false
|
||
TUI_BACKEND="bash"
|
||
if has_gum; then
|
||
GUM_STYLE=true
|
||
fi
|
||
if [[ "$DRY_RUN" == true ]] || [[ "${ADVENTURELOG_SKIP_GUM:-}" == "1" ]]; then
|
||
TUI_BACKEND="bash"
|
||
return 0
|
||
fi
|
||
if ! has_gum && [[ "${ADVENTURELOG_SKIP_GUM:-}" != "1" ]]; then
|
||
offer_gum_install && GUM_STYLE=true
|
||
fi
|
||
detect_tui_backend
|
||
}
|
||
|
||
tui_styled() {
|
||
local fg="${1:-212}"
|
||
shift
|
||
if [[ "$GUM_STYLE" == true ]]; then
|
||
gum style --foreground "$fg" "$@"
|
||
else
|
||
echo -e "${PURPLE}${BOLD}$*${NC}"
|
||
fi
|
||
}
|
||
|
||
tui_print_box() {
|
||
local title="$1"
|
||
local body="$2"
|
||
body="$(printf '%b' "$body")"
|
||
echo "" >&2
|
||
if [[ "$GUM_STYLE" == true ]]; then
|
||
gum join --align left --vertical \
|
||
"$(gum style --border rounded --border-foreground 212 --padding "0 1" --bold "$title")" \
|
||
"$(gum style --border rounded --border-foreground 240 --padding "0 1" "$body")" >&2
|
||
else
|
||
local width=54
|
||
echo -e " ${CYAN}╭$(printf '─%.0s' $(seq 1 "$width"))╮${NC}" >&2
|
||
echo -e " ${CYAN}│${NC} ${BOLD}${title}${NC}" >&2
|
||
echo -e " ${CYAN}├$(printf '─%.0s' $(seq 1 "$width"))┤${NC}" >&2
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
[[ -z "$line" ]] && echo -e " ${CYAN}│${NC}" >&2 && continue
|
||
echo -e " ${CYAN}│${NC} ${line}" >&2
|
||
done <<< "$body"
|
||
echo -e " ${CYAN}╰$(printf '─%.0s' $(seq 1 "$width"))╯${NC}" >&2
|
||
fi
|
||
echo "" >&2
|
||
}
|
||
|
||
tui_progress_bar() {
|
||
local step="$1"
|
||
local total="$2"
|
||
local label="$3"
|
||
local width=28
|
||
local filled=$(( step * width / total ))
|
||
local empty=$(( width - filled ))
|
||
local bar
|
||
bar="$(printf '█%.0s' $(seq 1 "$filled" 2>/dev/null || true))$(printf '░%.0s' $(seq 1 "$empty" 2>/dev/null || true))"
|
||
local step_text="Step ${step} of ${total}"
|
||
if [[ "$GUM_STYLE" == true ]]; then
|
||
if ! {
|
||
gum style --foreground 212 "$bar" >&2
|
||
gum style --bold "$step_text" >&2
|
||
gum style --foreground 245 "$label" >&2
|
||
} 2>/dev/null; then
|
||
echo -e " ${CYAN}${bar}${NC} ${BOLD}${step_text}${NC}" >&2
|
||
echo -e " ${DIM}${label}${NC}" >&2
|
||
fi
|
||
else
|
||
echo -e " ${CYAN}${bar}${NC} ${BOLD}${step_text}${NC}" >&2
|
||
echo -e " ${DIM}${label}${NC}" >&2
|
||
fi
|
||
echo "" >&2
|
||
}
|
||
|
||
tui_confirm_bash() {
|
||
local prompt="$1"
|
||
local default="${2:-n}"
|
||
local hint="y/N"
|
||
[[ "$default" == "y" ]] && hint="Y/n"
|
||
if [[ "$TUI_BACKEND" == "styled" ]] || [[ "$TUI_BACKEND" == "gum" ]]; then
|
||
tui_print_box "Confirm" "$prompt"
|
||
fi
|
||
echo "" >&2
|
||
read -r -p "$(echo -e " ${BOLD}?${NC} ${prompt} ${DIM}[${hint}]${NC}: ")" reply
|
||
reply="${reply:-$default}"
|
||
[[ "$reply" =~ ^[Yy] ]]
|
||
}
|
||
|
||
tui_confirm() {
|
||
local prompt="$1"
|
||
local default="${2:-n}"
|
||
if [[ "${DRY_RUN:-false}" == true ]]; then
|
||
[[ "$default" == "y" ]]
|
||
return
|
||
fi
|
||
case "$TUI_BACKEND" in
|
||
gum)
|
||
local gum_rc=0
|
||
if [[ "$default" == "y" ]]; then
|
||
gum confirm "$prompt" --affirmative " Yes " --negative " No " --default=true 2>/dev/null || gum_rc=$?
|
||
else
|
||
gum confirm "$prompt" --affirmative " Yes " --negative " No " --default=false 2>/dev/null || gum_rc=$?
|
||
fi
|
||
if [[ $gum_rc -eq 0 ]]; then return 0; fi
|
||
if [[ $gum_rc -eq 1 ]]; then return 1; fi
|
||
tui_confirm_bash "$prompt" "$default"
|
||
;;
|
||
whiptail)
|
||
if whiptail --yesno "$prompt" 12 70; then return 0; else return 1; fi
|
||
;;
|
||
dialog)
|
||
if dialog --yesno "$prompt" 12 70; then return 0; else return 1; fi
|
||
;;
|
||
*)
|
||
tui_confirm_bash "$prompt" "$default"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
tui_input_bash() {
|
||
local prompt="$1"
|
||
local default="${2:-}"
|
||
local value
|
||
if [[ "$TUI_BACKEND" == "styled" ]]; then
|
||
tui_print_box "Input" "$prompt"
|
||
fi
|
||
echo "" >&2
|
||
if [[ -n "$default" ]]; then
|
||
read -r -p "$(echo -e " ${BOLD}›${NC} ${prompt} ${DIM}[${default}]${NC}: ")" value
|
||
echo "${value:-$default}"
|
||
else
|
||
read -r -p "$(echo -e " ${BOLD}›${NC} ${prompt}: ")" value
|
||
echo "$value"
|
||
fi
|
||
}
|
||
|
||
tui_input() {
|
||
local prompt="$1"
|
||
local default="${2:-}"
|
||
if [[ "${DRY_RUN:-false}" == true ]]; then
|
||
echo "$default"
|
||
return 0
|
||
fi
|
||
local value
|
||
case "$TUI_BACKEND" in
|
||
gum)
|
||
if [[ -n "$default" ]]; then
|
||
value="$(gum input --placeholder "$default" --prompt "$prompt " --value "$default" --width 60 2>/dev/null || true)"
|
||
else
|
||
value="$(gum input --prompt "$prompt " --width 60 2>/dev/null || true)"
|
||
fi
|
||
if [[ -n "$value" ]]; then
|
||
echo "$value"
|
||
return 0
|
||
fi
|
||
tui_input_bash "$prompt" "$default"
|
||
;;
|
||
whiptail)
|
||
whiptail --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3
|
||
;;
|
||
dialog)
|
||
dialog --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3
|
||
;;
|
||
*)
|
||
tui_input_bash "$prompt" "$default"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
tui_password() {
|
||
local prompt="$1"
|
||
if [[ "${DRY_RUN:-false}" == true ]]; then
|
||
echo "dry-run-password"
|
||
return 0
|
||
fi
|
||
local value
|
||
case "$TUI_BACKEND" in
|
||
gum)
|
||
value="$(gum input --password --prompt "$prompt " --width 60 2>/dev/null || true)"
|
||
if [[ -n "$value" ]]; then
|
||
echo "$value"
|
||
return 0
|
||
fi
|
||
;&
|
||
*)
|
||
if [[ "$TUI_BACKEND" == "styled" ]]; then
|
||
tui_print_box "Secret" "$prompt"
|
||
fi
|
||
echo "" >&2
|
||
read -r -s -p "$(echo -e " ${BOLD}🔒${NC} ${prompt}: ")" value
|
||
echo "" >&2
|
||
echo "$value"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
tui_choose_styled() {
|
||
local prompt="$1"
|
||
shift
|
||
local options=("$@")
|
||
local choice i default="${TUI_CHOOSE_DEFAULT_INDEX:-1}"
|
||
|
||
tui_print_box "$prompt" "Enter the number for your choice."
|
||
echo "" >&2
|
||
i=1
|
||
for opt in "${options[@]}"; do
|
||
if (( i == default )); then
|
||
if [[ "$GUM_STYLE" == true ]]; then
|
||
echo -e " $(gum style --foreground 212 --bold "❯ ${i}") $(gum style --bold "$opt")" >&2
|
||
else
|
||
echo -e " ${CYAN}${BOLD}❯ ${i}${NC} ${BOLD}${opt}${NC}" >&2
|
||
fi
|
||
else
|
||
echo -e " ${DIM}${i}${NC} ${opt}" >&2
|
||
fi
|
||
((i++)) || true
|
||
done
|
||
echo "" >&2
|
||
read -r -p "$(echo -e " ${BOLD}›${NC} Choice ${DIM}[${default}]${NC}: ")" choice
|
||
choice="${choice:-$default}"
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#options[@]} )); then
|
||
echo "${options[$((choice - 1))]}"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
tui_choose() {
|
||
local prompt="$1"
|
||
shift
|
||
local options=("$@")
|
||
local choice result default_idx="${TUI_CHOOSE_DEFAULT_INDEX:-}"
|
||
if [[ "${DRY_RUN:-false}" == true ]]; then
|
||
if [[ -n "$default_idx" ]]; then
|
||
echo "${options[$((default_idx - 1))]}"
|
||
else
|
||
echo "${options[0]}"
|
||
fi
|
||
return 0
|
||
fi
|
||
case "$TUI_BACKEND" in
|
||
gum)
|
||
if [[ -z "$default_idx" ]] && result="$(gum choose --header "$prompt" --cursor "❯ " --selected.foreground "212" --height 12 "${options[@]}" 2>/dev/null)" && [[ -n "$result" ]]; then
|
||
echo "$result"
|
||
return 0
|
||
fi
|
||
tui_choose_styled "$prompt" "${options[@]}"
|
||
;;
|
||
styled|bash)
|
||
tui_choose_styled "$prompt" "${options[@]}"
|
||
;;
|
||
whiptail|dialog)
|
||
local menu_args=()
|
||
local idx=1
|
||
for opt in "${options[@]}"; do
|
||
menu_args+=("$idx" "$opt")
|
||
((idx++)) || true
|
||
done
|
||
local cmd="whiptail"
|
||
[[ "$TUI_BACKEND" == "dialog" ]] && cmd="dialog"
|
||
choice=$($cmd --menu "$prompt" 20 78 12 "${menu_args[@]}" 3>&1 1>&2 2>&3) || return 1
|
||
echo "${options[$((choice - 1))]}"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
tui_spinner_bash() {
|
||
local title="$1"
|
||
shift
|
||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||
local i=0
|
||
local tmp_out tmp_err
|
||
tmp_out="$(mktemp)"
|
||
tmp_err="$(mktemp)"
|
||
("$@" >"$tmp_out" 2>"$tmp_err") &
|
||
local pid=$!
|
||
while kill -0 "$pid" 2>/dev/null; do
|
||
printf "\r ${CYAN}${frames[$i]}${NC} ${title}" >&2
|
||
i=$(( (i + 1) % ${#frames[@]} ))
|
||
sleep 0.08
|
||
done
|
||
wait "$pid"
|
||
local rc=$?
|
||
printf "\r\033[K" >&2
|
||
if [[ $rc -eq 0 ]]; then
|
||
log_success "$title" >&2
|
||
else
|
||
log_error "$title failed" >&2
|
||
cat "$tmp_err" >&2
|
||
fi
|
||
cat "$tmp_out"
|
||
rm -f "$tmp_out" "$tmp_err"
|
||
return $rc
|
||
}
|
||
|
||
tui_spinner() {
|
||
local title="$1"
|
||
shift
|
||
case "$TUI_BACKEND" in
|
||
gum)
|
||
gum spin --spinner dot --spinner.foreground "212" --title "$title" -- "$@" 2>/dev/null || {
|
||
tui_spinner_bash "$title" "$@"
|
||
}
|
||
;;
|
||
styled|bash)
|
||
tui_spinner_bash "$title" "$@"
|
||
;;
|
||
*)
|
||
log_info "$title"
|
||
"$@"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
tui_press_enter() {
|
||
local msg="${1:-Press Enter to continue}"
|
||
echo "" >&2
|
||
if [[ "$GUM_STYLE" == true ]]; then
|
||
gum style --foreground 245 " ${msg}…" >&2
|
||
else
|
||
echo -e " ${DIM}${msg}…${NC}" >&2
|
||
fi
|
||
read -r -p "" _
|
||
}
|
||
|
||
prompt_with_default() {
|
||
local prompt="$1"
|
||
local default="$2"
|
||
local validator="${3:-}"
|
||
local value
|
||
while true; do
|
||
value="$(tui_input "$prompt" "$default")"
|
||
value="${value:-$default}"
|
||
if [[ -z "$validator" ]] || "$validator" "$value"; then
|
||
echo "$value"
|
||
return 0
|
||
fi
|
||
log_error "Invalid value — please try again."
|
||
done
|
||
}
|
||
|
||
prompt_yes_no() {
|
||
local prompt="$1"
|
||
local default="${2:-n}"
|
||
tui_confirm "$prompt" "$default"
|
||
}
|