1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 18:34:46 +00:00
Files
Mole/lib/menu.sh
2025-09-25 20:22:51 +08:00

368 lines
11 KiB
Bash
Executable File

#!/bin/bash
# Simple interactive menu selector with arrow key support
# No external dependencies, compatible with most bash versions
declare -a menu_options=()
declare -i selected=0
declare -i menu_size=0
# ANSI escape sequences
readonly ESC=$'\033'
readonly UP="${ESC}[A"
readonly DOWN="${ESC}[B"
readonly ENTER=$'\n'
readonly CLEAR_LINE="${ESC}[2K"
readonly HIDE_CURSOR="${ESC}[?25l"
readonly SHOW_CURSOR="${ESC}[?25h"
# Set terminal to raw mode for reading single characters
setup_terminal() {
# Block until at least 1 byte to avoid false ENTER on empty reads
stty -echo -icanon min 1 time 0
}
# Restore terminal to normal mode
restore_terminal() {
stty echo icanon
printf "%s" "$SHOW_CURSOR"
}
# Draw the menu
draw_menu() {
local force_full_redraw="${1:-true}"
printf "%s" "$HIDE_CURSOR"
if [[ "$force_full_redraw" == "true" ]]; then
# Full redraw: clear and redraw all lines
for ((i = 0; i < menu_size; i++)); do
printf "\r%s" "$CLEAR_LINE"
if [[ $i -eq $selected ]]; then
printf "▶ \033[1;32m%s\033[0m\n" "${menu_options[i]}"
else
printf " %s\n" "${menu_options[i]}"
fi
done
# Move cursor back to the beginning and save position
printf "${ESC}[%dA" $menu_size
printf "${ESC}7" # Save cursor position
else
# Quick update: only update changed lines
printf "${ESC}8" # Restore cursor position
for ((i = 0; i < menu_size; i++)); do
printf "\r%s" "$CLEAR_LINE"
if [[ $i -eq $selected ]]; then
printf "▶ \033[1;32m%s\033[0m\n" "${menu_options[i]}"
else
printf " %s\n" "${menu_options[i]}"
fi
done
# Move cursor back to the beginning
printf "${ESC}[%dA" $menu_size
printf "${ESC}7" # Save cursor position again
fi
}
# Read a single key
read_key() {
local key
IFS= read -rsn1 key 2>/dev/null || return 1
case "$key" in
$'\033')
local key2 key3
if IFS= read -rsn1 -t 0.2 key2 2>/dev/null; then
if [[ "$key2" == "[" ]]; then
if IFS= read -rsn1 -t 0.2 key3 2>/dev/null; then
case "$key3" in
'A') echo "UP" ;;
'B') echo "DOWN" ;;
'C') echo "RIGHT" ;;
'D') echo "LEFT" ;;
*) echo "OTHER" ;;
esac
else
echo "OTHER"
fi
else
echo "OTHER"
fi
else
echo "OTHER"
fi
;;
$'\n'|$'\r') echo "ENTER" ;;
' ') echo " " ;;
'q'|'Q') echo "QUIT" ;;
*) echo "$key" ;;
esac
}
# Main menu function
# Usage: show_menu "Title" "option1" "option2" "option3" ...
show_menu() {
local title="$1"
shift
# Initialize menu options
menu_options=("$@")
menu_size=${#menu_options[@]}
selected=0
# Check if we have options
if [[ $menu_size -eq 0 ]]; then
echo "Error: No menu options provided" >&2
return 1
fi
# Setup terminal
setup_terminal
trap restore_terminal EXIT INT TERM
# Display title
if [[ -n "$title" ]]; then
printf "\n\033[1;34m%s\033[0m\n\n" "$title"
fi
# Initial draw
draw_menu true
# Main loop
local first_iteration=true
while true; do
local key=$(read_key)
case "$key" in
"UP")
((selected--))
if [[ $selected -lt 0 ]]; then
selected=$((menu_size - 1))
fi
draw_menu false # Quick update
;;
"DOWN")
((selected++))
if [[ $selected -ge $menu_size ]]; then
selected=0
fi
draw_menu false # Quick update
;;
"ENTER")
# Clear the menu
for ((i = 0; i < menu_size; i++)); do
printf "\r%s\n" "$CLEAR_LINE" >&2
done
printf "${ESC}[%dA" $menu_size >&2
# Show selection
printf "Selected: \033[1;32m%s\033[0m\n\n" "${menu_options[selected]}"
restore_terminal
return $selected
;;
"q"|"Q")
restore_terminal
echo "Cancelled." >&2
return 255
;;
[0-9])
# Jump to numbered option
local num=$((key - 1))
if [[ $num -ge 0 && $num -lt $menu_size ]]; then
selected=$num
draw_menu
fi
;;
# Ignore other keys
esac
done
}
# Multi-select menu function
# Usage: show_multi_menu "Title" "option1" "option2" "option3" ...
show_multi_menu() {
local title="$1"
shift
# Initialize menu options
menu_options=("$@")
menu_size=${#menu_options[@]}
selected=0
# Array to track selected items
declare -a selected_items=()
for ((i = 0; i < menu_size; i++)); do
selected_items[i]=false
done
# Check if we have options
if [[ $menu_size -eq 0 ]]; then
echo "Error: No menu options provided" >&2
return 1
fi
# Setup terminal
setup_terminal
trap restore_terminal EXIT INT TERM
# Display title
if [[ -n "$title" ]]; then
printf "\n\033[1;34m%s\033[0m\n" "$title" >&2
printf "\033[0;36mUse SPACE to select/deselect, ENTER to confirm, Q to quit\033[0m\n\n" >&2
fi
# Draw multi-select menu
draw_multi_menu() {
local force_full_redraw="${1:-true}"
printf "%s" "$HIDE_CURSOR" >&2
if [[ "$force_full_redraw" == "true" ]]; then
# Full redraw
for ((i = 0; i < menu_size; i++)); do
printf "\r%s" "$CLEAR_LINE" >&2
local checkbox="☐"
if [[ ${selected_items[i]} == "true" ]]; then
checkbox="\033[1;32m☑\033[0m"
fi
if [[ $i -eq $selected ]]; then
printf "▶ %s \033[1;32m%s\033[0m\n" "$checkbox" "${menu_options[i]}" >&2
else
printf " %s %s\n" "$checkbox" "${menu_options[i]}" >&2
fi
done
# Move cursor back to the beginning and save position
printf "${ESC}[%dA" $menu_size >&2
printf "${ESC}7" >&2 # Save cursor position
else
# Quick update
printf "${ESC}8" >&2 # Restore cursor position
for ((i = 0; i < menu_size; i++)); do
printf "\r%s" "$CLEAR_LINE" >&2
local checkbox="☐"
if [[ ${selected_items[i]} == "true" ]]; then
checkbox="\033[1;32m☑\033[0m"
fi
if [[ $i -eq $selected ]]; then
printf "▶ %s \033[1;32m%s\033[0m\n" "$checkbox" "${menu_options[i]}" >&2
else
printf " %s %s\n" "$checkbox" "${menu_options[i]}" >&2
fi
done
# Move cursor back to the beginning and save position
printf "${ESC}[%dA" $menu_size >&2
printf "${ESC}7" >&2 # Save cursor position
fi
}
# Initial draw
draw_multi_menu true
# Main loop
while true; do
local key=$(read_key)
case "$key" in
"UP")
((selected--))
if [[ $selected -lt 0 ]]; then
selected=$((menu_size - 1))
fi
draw_multi_menu false # Quick update
;;
"DOWN")
((selected++))
if [[ $selected -ge $menu_size ]]; then
selected=0
fi
draw_multi_menu false # Quick update
;;
" ")
# Toggle selection
if [[ ${selected_items[selected]} == "true" ]]; then
selected_items[selected]="false"
else
selected_items[selected]="true"
fi
draw_multi_menu false # Quick update
;;
"ENTER")
# Clear the menu
for ((i = 0; i < menu_size; i++)); do
printf "\r%s\n" "$CLEAR_LINE" >&2
done
printf "${ESC}[%dA" $menu_size >&2
# Show selections to stderr so it doesn't interfere with return value
local has_selection=false
printf "Selected items:\n" >&2
for ((i = 0; i < menu_size; i++)); do
if [[ ${selected_items[i]} == "true" ]]; then
printf " \033[1;32m%s\033[0m\n" "${menu_options[i]}" >&2
has_selection=true
fi
done
if [[ $has_selection == "false" ]]; then
printf " None\n" >&2
fi
printf "\n" >&2
restore_terminal
# Return selected indices as space-separated string
local result=""
for ((i = 0; i < menu_size; i++)); do
if [[ ${selected_items[i]} == "true" ]]; then
result="$result $i"
fi
done
echo "${result# }" # Remove leading space
return 0
;;
"q"|"Q"|"ESC")
restore_terminal
echo "Cancelled." >&2
return 255
;;
esac
done
}
# Example usage function
demo_menu() {
echo "=== Single Select Demo ==="
if show_menu "Choose an action:" "Install package" "Update system" "Clean cache" "Exit"; then
local choice=$?
echo "You selected option $choice"
fi
echo -e "\n=== Multi Select Demo ==="
local selections=$(show_multi_menu "Choose packages to install:" "git" "vim" "curl" "htop" "tree")
if [[ $? -eq 0 && -n "$selections" ]]; then
echo "Selected indices: $selections"
# Convert indices to actual values
local options=("git" "vim" "curl" "htop" "tree")
echo "Selected packages:"
for idx in $selections; do
echo " - ${options[idx]}"
done
fi
}
# If script is run directly, show demo
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
demo_menu
fi