#!/bin/bash # Paginated menu with arrow key navigation set -euo pipefail # Terminal control functions enter_alt_screen() { tput smcup 2>/dev/null || true; } leave_alt_screen() { tput rmcup 2>/dev/null || true; } # Main paginated multi-select menu function paginated_multi_select() { local title="$1" shift local -a items=("$@") # Validation if [[ ${#items[@]} -eq 0 ]]; then echo "No items provided" >&2 return 1 fi local total_items=${#items[@]} local items_per_page=10 local total_pages=$(( (total_items + items_per_page - 1) / items_per_page )) local current_page=0 local cursor_pos=0 local -a selected=() # Initialize selection array for ((i = 0; i < total_items; i++)); do selected[i]=false done # Cleanup function cleanup() { show_cursor stty echo icanon 2>/dev/null || true leave_alt_screen } trap cleanup EXIT INT TERM # Setup terminal stty -echo -icanon 2>/dev/null || true enter_alt_screen hide_cursor # Helper functions print_line() { printf "\r\033[2K%s\n" "$1" >&2; } render_item() { local idx=$1 is_current=$2 local checkbox="☐" [[ ${selected[idx]} == true ]] && checkbox="☑" if [[ $is_current == true ]]; then printf "\r\033[2K\033[7m▶ %s %s\033[0m\n" "$checkbox" "${items[idx]}" >&2 else printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2 fi } # Draw the complete menu draw_menu() { printf "\033[H\033[J" >&2 # Clear screen and move to top # Header printf "%s\n%s\n" "$title" "$(printf '=%.0s' $(seq 1 ${#title}))" >&2 # Status local selected_count=0 for ((i = 0; i < total_items; i++)); do [[ ${selected[i]} == true ]] && ((selected_count++)) done printf "Page %d/%d │ Total: %d │ Selected: %d\n\n" \ $((current_page + 1)) $total_pages $total_items $selected_count >&2 # Items for current page local start_idx=$((current_page * items_per_page)) local end_idx=$((start_idx + items_per_page - 1)) [[ $end_idx -ge $total_items ]] && end_idx=$((total_items - 1)) for ((i = start_idx; i <= end_idx; i++)); do local is_current=false [[ $((i - start_idx)) -eq $cursor_pos ]] && is_current=true render_item $i $is_current done # Fill empty slots local items_shown=$((end_idx - start_idx + 1)) for ((i = items_shown; i < items_per_page; i++)); do print_line "" done print_line "" print_line "↑↓: Navigate | Space: Select | Enter: Confirm | Q: Exit" } # Show help screen show_help() { printf "\033[H\033[J" >&2 cat >&2 << 'EOF' Help - Navigation Controls ========================== ↑ / ↓ Navigate up/down Space Select/deselect item Enter Confirm selection A Select all N Deselect all Q Exit Press any key to continue... EOF read -n 1 -s >&2 } # Main interaction loop while true; do draw_menu local key=$(read_key) case "$key" in "QUIT") cleanup; return 1 ;; "UP") if [[ $cursor_pos -gt 0 ]]; then ((cursor_pos--)) elif [[ $current_page -gt 0 ]]; then ((current_page--)) # Calculate cursor position for new page local start_idx=$((current_page * items_per_page)) local items_on_page=$((total_items - start_idx)) [[ $items_on_page -gt $items_per_page ]] && items_on_page=$items_per_page cursor_pos=$((items_on_page - 1)) fi ;; "DOWN") local start_idx=$((current_page * items_per_page)) local items_on_page=$((total_items - start_idx)) [[ $items_on_page -gt $items_per_page ]] && items_on_page=$items_per_page if [[ $cursor_pos -lt $((items_on_page - 1)) ]]; then ((cursor_pos++)) elif [[ $current_page -lt $((total_pages - 1)) ]]; then ((current_page++)) cursor_pos=0 fi ;; "SPACE") local idx=$((current_page * items_per_page + cursor_pos)) if [[ $idx -lt $total_items ]]; then if [[ ${selected[idx]} == true ]]; then selected[idx]=false else selected[idx]=true fi fi ;; "ALL") for ((i = 0; i < total_items; i++)); do selected[i]=true done ;; "NONE") for ((i = 0; i < total_items; i++)); do selected[i]=false done ;; "HELP") show_help ;; "ENTER") # Auto-select current item if nothing selected local has_selection=false for ((i = 0; i < total_items; i++)); do if [[ ${selected[i]} == true ]]; then has_selection=true break fi done if [[ $has_selection == false ]]; then local idx=$((current_page * items_per_page + cursor_pos)) [[ $idx -lt $total_items ]] && selected[idx]=true fi # Store result in global variable instead of returning via stdout local result="" for ((i = 0; i < total_items; i++)); do if [[ ${selected[i]} == true ]]; then result="$result $i" fi done local final_result="${result# }" # Remove the trap to avoid cleanup on normal exit trap - EXIT INT TERM # Store result in global variable MOLE_SELECTION_RESULT="$final_result" # Manually cleanup terminal before returning show_cursor stty echo icanon 2>/dev/null || true leave_alt_screen return 0 ;; esac done } # Export function for external use if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "This is a library file. Source it from other scripts." >&2 exit 1 fi