mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 18:34:46 +00:00
313 lines
9.8 KiB
Bash
Executable File
313 lines
9.8 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Proper paginated menu with arrow key navigation
|
|
# 10 items per page, up/down to navigate, space to select, left/right to change pages
|
|
|
|
# Terminal control functions
|
|
hide_cursor() { printf '\033[?25l' >&2; }
|
|
show_cursor() { printf '\033[?25h' >&2; }
|
|
clear_screen() { printf '\033[2J\033[H' >&2; }
|
|
enter_alt_screen() { tput smcup >/dev/null 2>&1 || true; }
|
|
leave_alt_screen() { tput rmcup >/dev/null 2>&1 || true; }
|
|
disable_wrap() { printf '\033[?7l' >&2; } # disable line wrap
|
|
enable_wrap() { printf '\033[?7h' >&2; }
|
|
|
|
# Read single key with arrow key support (macOS bash 3.2 friendly)
|
|
read_key() {
|
|
local key seq
|
|
IFS= read -rsn1 key || return 1
|
|
|
|
# Some terminals may yield empty on Enter with -n1
|
|
if [[ -z "$key" ]]; then
|
|
echo "ENTER"
|
|
return 0
|
|
fi
|
|
|
|
case "$key" in
|
|
$'\033')
|
|
# Read next two bytes within 1s: "[A", "[B", ...
|
|
if IFS= read -rsn2 -t 1 seq 2>/dev/null; then
|
|
case "$seq" in
|
|
"[A") echo "UP" ;;
|
|
"[B") echo "DOWN" ;;
|
|
"[C") echo "RIGHT" ;;
|
|
"[D") echo "LEFT" ;;
|
|
*) echo "OTHER" ;;
|
|
esac
|
|
else
|
|
echo "OTHER"
|
|
fi
|
|
;;
|
|
' ') echo "SPACE" ;;
|
|
$'\n'|$'\r') echo "ENTER" ;;
|
|
'q'|'Q') echo "QUIT" ;;
|
|
'a'|'A') echo "ALL" ;;
|
|
'n'|'N') echo "NONE" ;;
|
|
'?') echo "HELP" ;;
|
|
*) echo "OTHER" ;;
|
|
esac
|
|
}
|
|
|
|
# Paginated multi-select menu
|
|
paginated_multi_select() {
|
|
local title="$1"
|
|
shift
|
|
local -a items=("$@")
|
|
|
|
local total_items=${#items[@]}
|
|
local items_per_page=10 # Reduced for better readability
|
|
local total_pages=$(( (total_items + items_per_page - 1) / items_per_page ))
|
|
local current_page=0
|
|
local cursor_pos=0 # Position within current page (0-9)
|
|
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 2>/dev/null || true
|
|
stty icanon 2>/dev/null || true
|
|
leave_alt_screen
|
|
enable_wrap
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Setup terminal for optimal responsiveness
|
|
stty -echo -icanon min 1 time 0 2>/dev/null || true
|
|
enter_alt_screen
|
|
disable_wrap
|
|
hide_cursor
|
|
|
|
# Main display function
|
|
first_draw=1
|
|
# Helper: print one cleared line
|
|
print_line() {
|
|
printf "\r\033[2K%s\n" "$1" >&2
|
|
}
|
|
|
|
# Helper: render one item line at given page position
|
|
render_item_line() {
|
|
local page_pos=$1
|
|
local start_idx=$((current_page * items_per_page))
|
|
local i=$((start_idx + page_pos))
|
|
local checkbox="☐"
|
|
local cursor_marker=" "
|
|
[[ ${selected[i]} == true ]] && checkbox="☑"
|
|
if [[ $page_pos -eq $cursor_pos ]]; then
|
|
cursor_marker="▶ "
|
|
printf "\r\033[2K\033[7m%s%s %s\033[0m\n" "$cursor_marker" "$checkbox" "${items[i]}" >&2
|
|
else
|
|
printf "\r\033[2K%s%s %s\n" "$cursor_marker" "$checkbox" "${items[i]}" >&2
|
|
fi
|
|
}
|
|
|
|
# Helper: move cursor to top-left anchor saved by tput sc
|
|
to_anchor() { tput rc >/dev/null 2>&1 || true; }
|
|
|
|
# Full draw of entire screen - simplified for stability
|
|
draw_menu() {
|
|
# Always do full screen redraw for reliability
|
|
clear_screen
|
|
|
|
# Simple header
|
|
printf "%s\n" "$title" >&2
|
|
printf "%s\n" "$(printf '=%.0s' $(seq 1 ${#title}))" >&2
|
|
|
|
# Status bar
|
|
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" \
|
|
$((current_page + 1)) $total_pages $total_items $selected_count >&2
|
|
print_line ""
|
|
|
|
# Calculate page boundaries
|
|
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))
|
|
|
|
# Display items for current page
|
|
for ((i = start_idx; i <= end_idx; i++)); do
|
|
local page_pos=$((i - start_idx))
|
|
render_item_line "$page_pos"
|
|
done
|
|
|
|
# Fill empty slots to always print items_per_page lines
|
|
local items_on_page=$((end_idx - start_idx + 1))
|
|
for ((i = items_on_page; i < items_per_page; i++)); do
|
|
print_line ""
|
|
done
|
|
|
|
print_line ""
|
|
print_line "↑↓: Navigate | Space: Select | Enter: Confirm | Q: Exit"
|
|
}
|
|
|
|
# Help screen
|
|
show_help() {
|
|
clear_screen
|
|
echo "App Uninstaller - Help" >&2
|
|
echo "======================" >&2
|
|
echo >&2
|
|
echo " ↑ / ↓ Navigate up/down" >&2
|
|
echo " ← / → Previous/next page" >&2
|
|
echo " Space Select/deselect app" >&2
|
|
echo " Enter Confirm selection" >&2
|
|
echo " A Select all" >&2
|
|
echo " N Deselect all" >&2
|
|
echo " Q Exit" >&2
|
|
echo >&2
|
|
read -p "Press any key to continue..." -n 1 >&2
|
|
}
|
|
|
|
# Main loop - simplified to always do full redraws for stability
|
|
while true; do
|
|
draw_menu # Always full redraw to avoid display issues
|
|
|
|
local key=$(read_key)
|
|
|
|
# Immediate exit key
|
|
if [[ "$key" == "QUIT" ]]; then
|
|
cleanup
|
|
return 1
|
|
fi
|
|
|
|
case "$key" in
|
|
"UP")
|
|
if [[ $cursor_pos -gt 0 ]]; then
|
|
((cursor_pos--))
|
|
elif [[ $current_page -gt 0 ]]; then
|
|
((current_page--))
|
|
cursor_pos=$((items_per_page - 1))
|
|
local start_idx=$((current_page * items_per_page))
|
|
local end_idx=$((start_idx + items_per_page - 1))
|
|
[[ $end_idx -ge $total_items ]] && cursor_pos=$((total_items - start_idx - 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
|
|
;;
|
|
"LEFT")
|
|
if [[ $current_page -gt 0 ]]; then
|
|
((current_page--))
|
|
cursor_pos=0
|
|
fi
|
|
;;
|
|
"RIGHT")
|
|
if [[ $current_page -lt $((total_pages - 1)) ]]; then
|
|
((current_page++))
|
|
cursor_pos=0
|
|
fi
|
|
;;
|
|
"PGUP")
|
|
current_page=0
|
|
cursor_pos=0
|
|
;;
|
|
"PGDOWN")
|
|
current_page=$((total_pages - 1))
|
|
cursor_pos=0
|
|
;;
|
|
"SPACE")
|
|
local actual_idx=$((current_page * items_per_page + cursor_pos))
|
|
if [[ $actual_idx -lt $total_items ]]; then
|
|
if [[ ${selected[actual_idx]} == true ]]; then
|
|
selected[actual_idx]=false
|
|
else
|
|
selected[actual_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")
|
|
# If no items are selected, select the current item
|
|
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
|
|
# Select current item under cursor
|
|
local actual_idx=$((current_page * items_per_page + cursor_pos))
|
|
if [[ $actual_idx -lt $total_items ]]; then
|
|
selected[actual_idx]=true
|
|
fi
|
|
fi
|
|
|
|
# Build result
|
|
local result=""
|
|
for ((i = 0; i < total_items; i++)); do
|
|
if [[ ${selected[i]} == true ]]; then
|
|
result="$result $i"
|
|
fi
|
|
done
|
|
cleanup
|
|
echo "${result# }"
|
|
return 0
|
|
;;
|
|
*)
|
|
# Ignore unrecognized keys - just continue the loop
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Demo function
|
|
demo_paginated() {
|
|
echo "=== Paginated Multi-select Demo ===" >&2
|
|
|
|
# Create test data
|
|
local test_items=()
|
|
for i in {1..35}; do
|
|
test_items+=("Application $i ($(( (RANDOM % 500) + 50 ))MB)")
|
|
done
|
|
|
|
local result
|
|
result=$(paginated_multi_select "Choose Applications to Uninstall" "${test_items[@]}")
|
|
local exit_code=$?
|
|
|
|
if [[ $exit_code -eq 0 ]]; then
|
|
if [[ -n "$result" ]]; then
|
|
echo "Selected indices: $result" >&2
|
|
echo "Count: $(echo $result | wc -w | tr -d ' ')" >&2
|
|
else
|
|
echo "No items selected" >&2
|
|
fi
|
|
else
|
|
echo "Selection cancelled" >&2
|
|
fi
|
|
}
|
|
|
|
# Run demo if script is executed directly
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
demo_paginated
|
|
fi
|