557 lines
15 KiB
Bash
557 lines
15 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
# wizard_utils.sh - Interactive prompt utilities for Ralph enable wizard
|
||
# Provides consistent, user-friendly prompts for configuration
|
||
|
||
# Colors (exported for subshells)
|
||
export WIZARD_CYAN='\033[0;36m'
|
||
export WIZARD_GREEN='\033[0;32m'
|
||
export WIZARD_YELLOW='\033[1;33m'
|
||
export WIZARD_RED='\033[0;31m'
|
||
export WIZARD_BOLD='\033[1m'
|
||
export WIZARD_NC='\033[0m'
|
||
|
||
# =============================================================================
|
||
# BASIC PROMPTS
|
||
# =============================================================================
|
||
|
||
# confirm - Ask a yes/no question
|
||
#
|
||
# Parameters:
|
||
# $1 (prompt) - The question to ask
|
||
# $2 (default) - Default answer: "y" or "n" (optional, defaults to "n")
|
||
#
|
||
# Returns:
|
||
# 0 - User answered yes
|
||
# 1 - User answered no
|
||
#
|
||
# Example:
|
||
# if confirm "Continue with installation?" "y"; then
|
||
# echo "Installing..."
|
||
# fi
|
||
#
|
||
confirm() {
|
||
local prompt=$1
|
||
local default="${2:-n}"
|
||
local response
|
||
|
||
local yn_hint="[y/N]"
|
||
if [[ "$default" == [yY] ]]; then
|
||
yn_hint="[Y/n]"
|
||
fi
|
||
|
||
while true; do
|
||
# Display prompt to stderr for consistency with other prompt functions
|
||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} ${yn_hint}: " >&2
|
||
read -r response
|
||
|
||
# Handle empty response (use default)
|
||
if [[ -z "$response" ]]; then
|
||
response="$default"
|
||
fi
|
||
|
||
case "$response" in
|
||
[yY]|[yY][eE][sS])
|
||
return 0
|
||
;;
|
||
[nN]|[nN][oO])
|
||
return 1
|
||
;;
|
||
*)
|
||
echo -e "${WIZARD_YELLOW}Please answer yes (y) or no (n)${WIZARD_NC}" >&2
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# prompt_text - Ask for text input with optional default
|
||
#
|
||
# Parameters:
|
||
# $1 (prompt) - The prompt text
|
||
# $2 (default) - Default value (optional)
|
||
#
|
||
# Outputs:
|
||
# Echoes the user's input (or default if empty)
|
||
#
|
||
# Example:
|
||
# project_name=$(prompt_text "Project name" "my-project")
|
||
#
|
||
prompt_text() {
|
||
local prompt=$1
|
||
local default="${2:-}"
|
||
local response
|
||
|
||
# Display prompt to stderr so command substitution only captures the response
|
||
if [[ -n "$default" ]]; then
|
||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " >&2
|
||
else
|
||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " >&2
|
||
fi
|
||
|
||
read -r response
|
||
|
||
if [[ -z "$response" ]]; then
|
||
echo "$default"
|
||
else
|
||
echo "$response"
|
||
fi
|
||
}
|
||
|
||
# prompt_number - Ask for numeric input with optional default and range
|
||
#
|
||
# Parameters:
|
||
# $1 (prompt) - The prompt text
|
||
# $2 (default) - Default value (optional)
|
||
# $3 (min) - Minimum value (optional)
|
||
# $4 (max) - Maximum value (optional)
|
||
#
|
||
# Outputs:
|
||
# Echoes the validated number
|
||
#
|
||
prompt_number() {
|
||
local prompt=$1
|
||
local default="${2:-}"
|
||
local min="${3:-}"
|
||
local max="${4:-}"
|
||
local response
|
||
|
||
while true; do
|
||
# Display prompt to stderr so command substitution only captures the response
|
||
if [[ -n "$default" ]]; then
|
||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " >&2
|
||
else
|
||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " >&2
|
||
fi
|
||
|
||
read -r response
|
||
|
||
# Use default if empty
|
||
if [[ -z "$response" ]]; then
|
||
if [[ -n "$default" ]]; then
|
||
echo "$default"
|
||
return 0
|
||
else
|
||
echo -e "${WIZARD_YELLOW}Please enter a number${WIZARD_NC}" >&2
|
||
continue
|
||
fi
|
||
fi
|
||
|
||
# Validate it's a number
|
||
if ! [[ "$response" =~ ^[0-9]+$ ]]; then
|
||
echo -e "${WIZARD_YELLOW}Please enter a valid number${WIZARD_NC}" >&2
|
||
continue
|
||
fi
|
||
|
||
# Check range if specified
|
||
if [[ -n "$min" && "$response" -lt "$min" ]]; then
|
||
echo -e "${WIZARD_YELLOW}Value must be at least ${min}${WIZARD_NC}" >&2
|
||
continue
|
||
fi
|
||
|
||
if [[ -n "$max" && "$response" -gt "$max" ]]; then
|
||
echo -e "${WIZARD_YELLOW}Value must be at most ${max}${WIZARD_NC}" >&2
|
||
continue
|
||
fi
|
||
|
||
echo "$response"
|
||
return 0
|
||
done
|
||
}
|
||
|
||
# =============================================================================
|
||
# SELECTION PROMPTS
|
||
# =============================================================================
|
||
|
||
# select_option - Present a list of options for single selection
|
||
#
|
||
# Parameters:
|
||
# $1 (prompt) - The question/prompt text
|
||
# $@ (options) - Remaining arguments are the options
|
||
#
|
||
# Outputs:
|
||
# Echoes the selected option (the text, not the number)
|
||
#
|
||
# Example:
|
||
# choice=$(select_option "Select package manager" "npm" "yarn" "pnpm")
|
||
# echo "Selected: $choice"
|
||
#
|
||
select_option() {
|
||
local prompt=$1
|
||
shift
|
||
local options=("$@")
|
||
local num_options=${#options[@]}
|
||
|
||
# Guard against empty options array
|
||
if [[ $num_options -eq 0 ]]; then
|
||
echo ""
|
||
return 1
|
||
fi
|
||
|
||
# Display prompt and options to stderr so command substitution only captures the result
|
||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
|
||
echo "" >&2
|
||
|
||
# Display options
|
||
local i=1
|
||
for opt in "${options[@]}"; do
|
||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" >&2
|
||
((i++))
|
||
done
|
||
|
||
echo "" >&2
|
||
|
||
while true; do
|
||
echo -en "Select option [1-${num_options}]: " >&2
|
||
read -r response
|
||
|
||
# Validate it's a number in range
|
||
if [[ "$response" =~ ^[0-9]+$ ]] && \
|
||
[[ "$response" -ge 1 ]] && \
|
||
[[ "$response" -le "$num_options" ]]; then
|
||
# Return the option text (0-indexed array)
|
||
echo "${options[$((response - 1))]}"
|
||
return 0
|
||
else
|
||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2
|
||
fi
|
||
done
|
||
}
|
||
|
||
# select_multiple - Present checkboxes for multi-selection
|
||
#
|
||
# Parameters:
|
||
# $1 (prompt) - The question/prompt text
|
||
# $@ (options) - Remaining arguments are the options
|
||
#
|
||
# Outputs:
|
||
# Echoes comma-separated list of selected indices (0-based)
|
||
# Returns empty string if nothing selected
|
||
#
|
||
# Example:
|
||
# selected=$(select_multiple "Select task sources" "beads" "github" "prd")
|
||
# # If user selects first and third: selected="0,2"
|
||
# IFS=',' read -ra indices <<< "$selected"
|
||
# for idx in "${indices[@]}"; do
|
||
# echo "Selected: ${options[$idx]}"
|
||
# done
|
||
#
|
||
select_multiple() {
|
||
local prompt=$1
|
||
shift
|
||
local options=("$@")
|
||
local num_options=${#options[@]}
|
||
|
||
# Track selected state (0 = not selected, 1 = selected)
|
||
declare -a selected
|
||
for ((i = 0; i < num_options; i++)); do
|
||
selected[$i]=0
|
||
done
|
||
|
||
# Display instructions (redirect to stderr to avoid corrupting return value)
|
||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
|
||
echo -e "${WIZARD_CYAN}(Enter numbers to toggle, press Enter when done)${WIZARD_NC}" >&2
|
||
echo "" >&2
|
||
|
||
while true; do
|
||
# Display options with checkboxes
|
||
local i=1
|
||
for opt in "${options[@]}"; do
|
||
local checkbox="[ ]"
|
||
if [[ "${selected[$((i - 1))]}" == "1" ]]; then
|
||
checkbox="[${WIZARD_GREEN}x${WIZARD_NC}]"
|
||
fi
|
||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${checkbox} ${opt}" >&2
|
||
((i++)) || true
|
||
done
|
||
|
||
echo "" >&2
|
||
echo -en "Toggle [1-${num_options}] or Enter to confirm: " >&2
|
||
read -r response
|
||
|
||
# Empty input = done
|
||
if [[ -z "$response" ]]; then
|
||
break
|
||
fi
|
||
|
||
# Validate it's a number in range
|
||
if [[ "$response" =~ ^[0-9]+$ ]] && \
|
||
[[ "$response" -ge 1 ]] && \
|
||
[[ "$response" -le "$num_options" ]]; then
|
||
# Toggle the selection
|
||
local idx=$((response - 1))
|
||
if [[ "${selected[$idx]}" == "0" ]]; then
|
||
selected[$idx]=1
|
||
else
|
||
selected[$idx]=0
|
||
fi
|
||
else
|
||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2
|
||
fi
|
||
|
||
# Clear previous display (move cursor up)
|
||
# Number of lines to clear: options + 2 (prompt line + input line)
|
||
for ((j = 0; j < num_options + 2; j++)); do
|
||
echo -en "\033[A\033[K" >&2
|
||
done
|
||
done
|
||
|
||
# Build result string (comma-separated indices)
|
||
local result=""
|
||
for ((i = 0; i < num_options; i++)); do
|
||
if [[ "${selected[$i]}" == "1" ]]; then
|
||
if [[ -n "$result" ]]; then
|
||
result="$result,$i"
|
||
else
|
||
result="$i"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
echo "$result"
|
||
}
|
||
|
||
# select_with_default - Present options with a recommended default
|
||
#
|
||
# Parameters:
|
||
# $1 (prompt) - The question/prompt text
|
||
# $2 (default_index) - 1-based index of default option
|
||
# $@ (options) - Remaining arguments are the options
|
||
#
|
||
# Outputs:
|
||
# Echoes the selected option
|
||
#
|
||
select_with_default() {
|
||
local prompt=$1
|
||
local default_index=$2
|
||
shift 2
|
||
local options=("$@")
|
||
local num_options=${#options[@]}
|
||
|
||
# Display prompt and options to stderr so command substitution only captures the result
|
||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
|
||
echo "" >&2
|
||
|
||
# Display options with default marked
|
||
local i=1
|
||
for opt in "${options[@]}"; do
|
||
if [[ $i -eq $default_index ]]; then
|
||
echo -e " ${WIZARD_GREEN}${i})${WIZARD_NC} ${opt} ${WIZARD_GREEN}(recommended)${WIZARD_NC}" >&2
|
||
else
|
||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" >&2
|
||
fi
|
||
((i++))
|
||
done
|
||
|
||
echo "" >&2
|
||
|
||
while true; do
|
||
echo -en "Select option [1-${num_options}] (default: ${default_index}): " >&2
|
||
read -r response
|
||
|
||
# Use default if empty
|
||
if [[ -z "$response" ]]; then
|
||
echo "${options[$((default_index - 1))]}"
|
||
return 0
|
||
fi
|
||
|
||
# Validate it's a number in range
|
||
if [[ "$response" =~ ^[0-9]+$ ]] && \
|
||
[[ "$response" -ge 1 ]] && \
|
||
[[ "$response" -le "$num_options" ]]; then
|
||
echo "${options[$((response - 1))]}"
|
||
return 0
|
||
else
|
||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2
|
||
fi
|
||
done
|
||
}
|
||
|
||
# =============================================================================
|
||
# DISPLAY UTILITIES
|
||
# =============================================================================
|
||
|
||
# print_header - Print a section header
|
||
#
|
||
# Parameters:
|
||
# $1 (title) - The header title
|
||
# $2 (phase) - Optional phase number (e.g., "1 of 5")
|
||
#
|
||
print_header() {
|
||
local title=$1
|
||
local phase="${2:-}"
|
||
|
||
echo ""
|
||
echo -e "${WIZARD_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${WIZARD_NC}"
|
||
if [[ -n "$phase" ]]; then
|
||
echo -e "${WIZARD_BOLD} ${title}${WIZARD_NC} ${WIZARD_CYAN}(${phase})${WIZARD_NC}"
|
||
else
|
||
echo -e "${WIZARD_BOLD} ${title}${WIZARD_NC}"
|
||
fi
|
||
echo -e "${WIZARD_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${WIZARD_NC}"
|
||
echo ""
|
||
}
|
||
|
||
# print_bullet - Print a bullet point item
|
||
#
|
||
# Parameters:
|
||
# $1 (text) - The text to display
|
||
# $2 (symbol) - Optional symbol (defaults to "•")
|
||
#
|
||
print_bullet() {
|
||
local text=$1
|
||
local symbol="${2:-•}"
|
||
|
||
echo -e " ${WIZARD_CYAN}${symbol}${WIZARD_NC} ${text}"
|
||
}
|
||
|
||
# print_success - Print a success message
|
||
#
|
||
# Parameters:
|
||
# $1 (message) - The message to display
|
||
#
|
||
print_success() {
|
||
echo -e "${WIZARD_GREEN}✓${WIZARD_NC} $1"
|
||
}
|
||
|
||
# print_warning - Print a warning message
|
||
#
|
||
# Parameters:
|
||
# $1 (message) - The message to display
|
||
#
|
||
print_warning() {
|
||
echo -e "${WIZARD_YELLOW}⚠${WIZARD_NC} $1"
|
||
}
|
||
|
||
# print_error - Print an error message
|
||
#
|
||
# Parameters:
|
||
# $1 (message) - The message to display
|
||
#
|
||
print_error() {
|
||
echo -e "${WIZARD_RED}✗${WIZARD_NC} $1"
|
||
}
|
||
|
||
# print_info - Print an info message
|
||
#
|
||
# Parameters:
|
||
# $1 (message) - The message to display
|
||
#
|
||
print_info() {
|
||
echo -e "${WIZARD_CYAN}ℹ${WIZARD_NC} $1"
|
||
}
|
||
|
||
# print_detection_result - Print a detection result with status
|
||
#
|
||
# Parameters:
|
||
# $1 (label) - What was detected
|
||
# $2 (value) - The detected value
|
||
# $3 (available) - "true" or "false"
|
||
#
|
||
print_detection_result() {
|
||
local label=$1
|
||
local value=$2
|
||
local available="${3:-true}"
|
||
|
||
if [[ "$available" == "true" ]]; then
|
||
echo -e " ${WIZARD_GREEN}✓${WIZARD_NC} ${label}: ${WIZARD_BOLD}${value}${WIZARD_NC}"
|
||
else
|
||
echo -e " ${WIZARD_YELLOW}○${WIZARD_NC} ${label}: ${value}"
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# PROGRESS DISPLAY
|
||
# =============================================================================
|
||
|
||
# show_progress - Display a simple progress indicator
|
||
#
|
||
# Parameters:
|
||
# $1 (current) - Current step number
|
||
# $2 (total) - Total steps
|
||
# $3 (message) - Current step message
|
||
#
|
||
show_progress() {
|
||
local current=$1
|
||
local total=$2
|
||
local message=$3
|
||
|
||
# Guard against division by zero
|
||
if [[ $total -le 0 ]]; then
|
||
local bar_width=30
|
||
local bar=""
|
||
for ((i = 0; i < bar_width; i++)); do bar+="░"; done
|
||
echo -en "\r${WIZARD_CYAN}[${bar}]${WIZARD_NC} 0/${total} ${message}"
|
||
return 0
|
||
fi
|
||
|
||
local bar_width=30
|
||
local filled=$((current * bar_width / total))
|
||
local empty=$((bar_width - filled))
|
||
|
||
local bar=""
|
||
for ((i = 0; i < filled; i++)); do bar+="█"; done
|
||
for ((i = 0; i < empty; i++)); do bar+="░"; done
|
||
|
||
echo -en "\r${WIZARD_CYAN}[${bar}]${WIZARD_NC} ${current}/${total} ${message}"
|
||
}
|
||
|
||
# clear_line - Clear the current line
|
||
#
|
||
clear_line() {
|
||
echo -en "\r\033[K"
|
||
}
|
||
|
||
# =============================================================================
|
||
# SUMMARY DISPLAY
|
||
# =============================================================================
|
||
|
||
# print_summary - Print a summary box
|
||
#
|
||
# Parameters:
|
||
# $1 (title) - Summary title
|
||
# $@ (items) - Key=value pairs to display
|
||
#
|
||
# Example:
|
||
# print_summary "Configuration" "Project=my-app" "Type=typescript" "Tasks=15"
|
||
#
|
||
print_summary() {
|
||
local title=$1
|
||
shift
|
||
local items=("$@")
|
||
|
||
echo ""
|
||
echo -e "${WIZARD_BOLD}┌─ ${title} ───────────────────────────────────────┐${WIZARD_NC}"
|
||
echo "│"
|
||
|
||
for item in "${items[@]}"; do
|
||
local key="${item%%=*}"
|
||
local value="${item#*=}"
|
||
printf "│ ${WIZARD_CYAN}%-20s${WIZARD_NC} %s\n" "${key}:" "$value"
|
||
done
|
||
|
||
echo "│"
|
||
echo -e "${WIZARD_BOLD}└────────────────────────────────────────────────────┘${WIZARD_NC}"
|
||
echo ""
|
||
}
|
||
|
||
# =============================================================================
|
||
# EXPORTS
|
||
# =============================================================================
|
||
|
||
export -f confirm
|
||
export -f prompt_text
|
||
export -f prompt_number
|
||
export -f select_option
|
||
export -f select_multiple
|
||
export -f select_with_default
|
||
export -f print_header
|
||
export -f print_bullet
|
||
export -f print_success
|
||
export -f print_warning
|
||
export -f print_error
|
||
export -f print_info
|
||
export -f print_detection_result
|
||
export -f show_progress
|
||
export -f clear_line
|
||
export -f print_summary
|