mirror of
https://github.com/tw93/Mole.git
synced 2026-02-11 00:44:18 +00:00
Reconstruct clean lib code
This commit is contained in:
748
lib/ui/menu_paginated.sh
Executable file
748
lib/ui/menu_paginated.sh
Executable file
@@ -0,0 +1,748 @@
|
||||
#!/bin/bash
|
||||
# Paginated menu with arrow key navigation
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Terminal control functions
|
||||
enter_alt_screen() {
|
||||
if command -v tput > /dev/null 2>&1 && [[ -t 1 ]]; then
|
||||
tput smcup 2> /dev/null || true
|
||||
fi
|
||||
}
|
||||
leave_alt_screen() {
|
||||
if command -v tput > /dev/null 2>&1 && [[ -t 1 ]]; then
|
||||
tput rmcup 2> /dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Get terminal height with fallback
|
||||
_pm_get_terminal_height() {
|
||||
local height=0
|
||||
|
||||
# Try stty size first (most reliable, real-time)
|
||||
# Use </dev/tty to ensure we read from terminal even if stdin is redirected
|
||||
if [[ -t 0 ]] || [[ -t 2 ]]; then
|
||||
height=$(stty size < /dev/tty 2> /dev/null | awk '{print $1}')
|
||||
fi
|
||||
|
||||
# Fallback to tput
|
||||
if [[ -z "$height" || $height -le 0 ]]; then
|
||||
if command -v tput > /dev/null 2>&1; then
|
||||
height=$(tput lines 2> /dev/null || echo "24")
|
||||
else
|
||||
height=24
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$height"
|
||||
}
|
||||
|
||||
# Calculate dynamic items per page based on terminal height
|
||||
_pm_calculate_items_per_page() {
|
||||
local term_height=$(_pm_get_terminal_height)
|
||||
local reserved=6 # header(2) + footer(3) + spacing(1)
|
||||
local available=$((term_height - reserved))
|
||||
|
||||
# Ensure minimum and maximum bounds
|
||||
if [[ $available -lt 1 ]]; then
|
||||
echo 1
|
||||
elif [[ $available -gt 50 ]]; then
|
||||
echo 50
|
||||
else
|
||||
echo "$available"
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse CSV into newline list (Bash 3.2)
|
||||
_pm_parse_csv_to_array() {
|
||||
local csv="${1:-}"
|
||||
if [[ -z "$csv" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local IFS=','
|
||||
for _tok in $csv; do
|
||||
printf "%s\n" "$_tok"
|
||||
done
|
||||
}
|
||||
|
||||
# Main paginated multi-select menu function
|
||||
paginated_multi_select() {
|
||||
local title="$1"
|
||||
shift
|
||||
local -a items=("$@")
|
||||
local external_alt_screen=false
|
||||
if [[ "${MOLE_MANAGED_ALT_SCREEN:-}" == "1" || "${MOLE_MANAGED_ALT_SCREEN:-}" == "true" ]]; then
|
||||
external_alt_screen=true
|
||||
fi
|
||||
|
||||
# Validation
|
||||
if [[ ${#items[@]} -eq 0 ]]; then
|
||||
echo "No items provided" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local total_items=${#items[@]}
|
||||
local items_per_page=$(_pm_calculate_items_per_page)
|
||||
local cursor_pos=0
|
||||
local top_index=0
|
||||
local filter_query=""
|
||||
local filter_mode="false" # filter mode toggle
|
||||
local sort_mode="${MOLE_MENU_SORT_DEFAULT:-date}" # date|name|size
|
||||
local sort_reverse="false"
|
||||
# Live query vs applied query
|
||||
local applied_query=""
|
||||
local searching="false"
|
||||
|
||||
# Metadata (optional)
|
||||
# epochs[i] -> last_used_epoch (numeric) for item i
|
||||
# sizekb[i] -> size in KB (numeric) for item i
|
||||
local -a epochs=()
|
||||
local -a sizekb=()
|
||||
local has_metadata="false"
|
||||
if [[ -n "${MOLE_MENU_META_EPOCHS:-}" ]]; then
|
||||
while IFS= read -r v; do epochs+=("${v:-0}"); done < <(_pm_parse_csv_to_array "$MOLE_MENU_META_EPOCHS")
|
||||
has_metadata="true"
|
||||
fi
|
||||
if [[ -n "${MOLE_MENU_META_SIZEKB:-}" ]]; then
|
||||
while IFS= read -r v; do sizekb+=("${v:-0}"); done < <(_pm_parse_csv_to_array "$MOLE_MENU_META_SIZEKB")
|
||||
has_metadata="true"
|
||||
fi
|
||||
|
||||
# If no metadata, force name sorting and disable sorting controls
|
||||
if [[ "$has_metadata" == "false" && "$sort_mode" != "name" ]]; then
|
||||
sort_mode="name"
|
||||
fi
|
||||
|
||||
# Index mappings
|
||||
local -a orig_indices=()
|
||||
local -a view_indices=()
|
||||
local i
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
orig_indices[i]=$i
|
||||
view_indices[i]=$i
|
||||
done
|
||||
|
||||
# Escape for shell globbing without upsetting highlighters
|
||||
_pm_escape_glob() {
|
||||
local s="${1-}" out="" c
|
||||
local i len=${#s}
|
||||
for ((i = 0; i < len; i++)); do
|
||||
c="${s:i:1}"
|
||||
case "$c" in
|
||||
'\' | '*' | '?' | '[' | ']') out+="\\$c" ;;
|
||||
*) out+="$c" ;;
|
||||
esac
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# Case-insensitive fuzzy match (substring search)
|
||||
_pm_match() {
|
||||
local hay="$1" q="$2"
|
||||
q="$(_pm_escape_glob "$q")"
|
||||
local pat="*${q}*"
|
||||
|
||||
shopt -s nocasematch
|
||||
local ok=1
|
||||
# shellcheck disable=SC2254 # intentional glob match with a computed pattern
|
||||
case "$hay" in
|
||||
$pat) ok=0 ;;
|
||||
esac
|
||||
shopt -u nocasematch
|
||||
return $ok
|
||||
}
|
||||
|
||||
local -a selected=()
|
||||
|
||||
# Initialize selection array
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
selected[i]=false
|
||||
done
|
||||
|
||||
if [[ -n "${MOLE_PRESELECTED_INDICES:-}" ]]; then
|
||||
local cleaned_preselect="${MOLE_PRESELECTED_INDICES//[[:space:]]/}"
|
||||
local -a initial_indices=()
|
||||
IFS=',' read -ra initial_indices <<< "$cleaned_preselect"
|
||||
for idx in "${initial_indices[@]}"; do
|
||||
if [[ "$idx" =~ ^[0-9]+$ && $idx -ge 0 && $idx -lt $total_items ]]; then
|
||||
selected[idx]=true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Preserve original TTY settings so we can restore them reliably
|
||||
local original_stty=""
|
||||
if [[ -t 0 ]] && command -v stty > /dev/null 2>&1; then
|
||||
original_stty=$(stty -g 2> /dev/null || echo "")
|
||||
fi
|
||||
|
||||
restore_terminal() {
|
||||
show_cursor
|
||||
if [[ -n "${original_stty-}" ]]; then
|
||||
stty "${original_stty}" 2> /dev/null || stty sane 2> /dev/null || stty echo icanon 2> /dev/null || true
|
||||
else
|
||||
stty sane 2> /dev/null || stty echo icanon 2> /dev/null || true
|
||||
fi
|
||||
if [[ "${external_alt_screen:-false}" == false ]]; then
|
||||
leave_alt_screen
|
||||
fi
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
trap - EXIT INT TERM
|
||||
restore_terminal
|
||||
unset MOLE_READ_KEY_FORCE_CHAR
|
||||
}
|
||||
|
||||
# Interrupt handler
|
||||
handle_interrupt() {
|
||||
cleanup
|
||||
exit 130 # Standard exit code for Ctrl+C
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap handle_interrupt INT TERM
|
||||
|
||||
# Setup terminal - preserve interrupt character
|
||||
stty -echo -icanon intr ^C 2> /dev/null || true
|
||||
if [[ $external_alt_screen == false ]]; then
|
||||
enter_alt_screen
|
||||
# Clear screen once on entry to alt screen
|
||||
printf "\033[2J\033[H" >&2
|
||||
else
|
||||
printf "\033[H" >&2
|
||||
fi
|
||||
hide_cursor
|
||||
|
||||
# Helper functions
|
||||
print_line() { printf "\r\033[2K%s\n" "$1" >&2; }
|
||||
|
||||
# Print footer lines wrapping only at separators
|
||||
_print_wrapped_controls() {
|
||||
local sep="$1"
|
||||
shift
|
||||
local -a segs=("$@")
|
||||
|
||||
local cols="${COLUMNS:-}"
|
||||
[[ -z "$cols" ]] && cols=$(tput cols 2> /dev/null || echo 80)
|
||||
|
||||
_strip_ansi_len() {
|
||||
local text="$1"
|
||||
local stripped
|
||||
stripped=$(printf "%s" "$text" | LC_ALL=C awk '{gsub(/\033\[[0-9;]*[A-Za-z]/,""); print}')
|
||||
printf "%d" "${#stripped}"
|
||||
}
|
||||
|
||||
local line="" s candidate
|
||||
local clear_line=$'\r\033[2K'
|
||||
for s in "${segs[@]}"; do
|
||||
if [[ -z "$line" ]]; then
|
||||
candidate="$s"
|
||||
else
|
||||
candidate="$line${sep}${s}"
|
||||
fi
|
||||
if (($(_strip_ansi_len "$candidate") > cols)); then
|
||||
printf "%s%s\n" "$clear_line" "$line" >&2
|
||||
line="$s"
|
||||
else
|
||||
line="$candidate"
|
||||
fi
|
||||
done
|
||||
printf "%s%s\n" "$clear_line" "$line" >&2
|
||||
}
|
||||
|
||||
# Rebuild the view_indices applying filter and sort
|
||||
rebuild_view() {
|
||||
# Filter
|
||||
local -a filtered=()
|
||||
local effective_query=""
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
# Live editing: empty query -> show all items
|
||||
effective_query="$filter_query"
|
||||
if [[ -z "$effective_query" ]]; then
|
||||
filtered=("${orig_indices[@]}")
|
||||
else
|
||||
local idx
|
||||
for ((idx = 0; idx < total_items; idx++)); do
|
||||
if _pm_match "${items[idx]}" "$effective_query"; then
|
||||
filtered+=("$idx")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
# Normal mode: use applied query; empty -> show all
|
||||
effective_query="$applied_query"
|
||||
if [[ -z "$effective_query" ]]; then
|
||||
filtered=("${orig_indices[@]}")
|
||||
else
|
||||
local idx
|
||||
for ((idx = 0; idx < total_items; idx++)); do
|
||||
if _pm_match "${items[idx]}" "$effective_query"; then
|
||||
filtered+=("$idx")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sort (skip if no metadata)
|
||||
if [[ "$has_metadata" == "false" ]]; then
|
||||
# No metadata: just use filtered list (already sorted by name naturally)
|
||||
view_indices=("${filtered[@]}")
|
||||
elif [[ ${#filtered[@]} -eq 0 ]]; then
|
||||
view_indices=()
|
||||
else
|
||||
# Build sort key
|
||||
local sort_key
|
||||
if [[ "$sort_mode" == "date" ]]; then
|
||||
# Date: ascending by default (oldest first)
|
||||
sort_key="-k1,1n"
|
||||
[[ "$sort_reverse" == "true" ]] && sort_key="-k1,1nr"
|
||||
elif [[ "$sort_mode" == "size" ]]; then
|
||||
# Size: descending by default (largest first)
|
||||
sort_key="-k1,1nr"
|
||||
[[ "$sort_reverse" == "true" ]] && sort_key="-k1,1n"
|
||||
else
|
||||
# Name: ascending by default (A to Z)
|
||||
sort_key="-k1,1f"
|
||||
[[ "$sort_reverse" == "true" ]] && sort_key="-k1,1fr"
|
||||
fi
|
||||
|
||||
# Create temporary file for sorting
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp 2> /dev/null) || tmpfile=""
|
||||
if [[ -n "$tmpfile" ]]; then
|
||||
local k id
|
||||
for id in "${filtered[@]}"; do
|
||||
case "$sort_mode" in
|
||||
date) k="${epochs[id]:-0}" ;;
|
||||
size) k="${sizekb[id]:-0}" ;;
|
||||
name | *) k="${items[id]}|${id}" ;;
|
||||
esac
|
||||
printf "%s\t%s\n" "$k" "$id" >> "$tmpfile"
|
||||
done
|
||||
|
||||
view_indices=()
|
||||
while IFS=$'\t' read -r _key _id; do
|
||||
[[ -z "$_id" ]] && continue
|
||||
view_indices+=("$_id")
|
||||
done < <(LC_ALL=C sort -t $'\t' $sort_key -- "$tmpfile" 2> /dev/null)
|
||||
|
||||
rm -f "$tmpfile"
|
||||
else
|
||||
# Fallback: no sorting
|
||||
view_indices=("${filtered[@]}")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clamp cursor into visible range
|
||||
local visible_count=${#view_indices[@]}
|
||||
local max_top
|
||||
if [[ $visible_count -gt $items_per_page ]]; then
|
||||
max_top=$((visible_count - items_per_page))
|
||||
else
|
||||
max_top=0
|
||||
fi
|
||||
[[ $top_index -gt $max_top ]] && top_index=$max_top
|
||||
local current_visible=$((visible_count - top_index))
|
||||
[[ $current_visible -gt $items_per_page ]] && current_visible=$items_per_page
|
||||
if [[ $cursor_pos -ge $current_visible ]]; then
|
||||
cursor_pos=$((current_visible > 0 ? current_visible - 1 : 0))
|
||||
fi
|
||||
[[ $cursor_pos -lt 0 ]] && cursor_pos=0
|
||||
}
|
||||
|
||||
# Initial view (default sort)
|
||||
rebuild_view
|
||||
|
||||
render_item() {
|
||||
# $1: visible row index (0..items_per_page-1 in current window)
|
||||
# $2: is_current flag
|
||||
local vrow=$1 is_current=$2
|
||||
local idx=$((top_index + vrow))
|
||||
local real="${view_indices[idx]:--1}"
|
||||
[[ $real -lt 0 ]] && return
|
||||
local checkbox="$ICON_EMPTY"
|
||||
[[ ${selected[real]} == true ]] && checkbox="$ICON_SOLID"
|
||||
|
||||
if [[ $is_current == true ]]; then
|
||||
printf "\r\033[2K${BLUE}${ICON_ARROW} %s %s${NC}\n" "$checkbox" "${items[real]}" >&2
|
||||
else
|
||||
printf "\r\033[2K %s %s\n" "$checkbox" "${items[real]}" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Draw the complete menu
|
||||
draw_menu() {
|
||||
# Recalculate items_per_page dynamically to handle window resize
|
||||
items_per_page=$(_pm_calculate_items_per_page)
|
||||
|
||||
printf "\033[H" >&2
|
||||
local clear_line="\r\033[2K"
|
||||
|
||||
# Count selections
|
||||
local selected_count=0
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
[[ ${selected[i]} == true ]] && ((selected_count++))
|
||||
done
|
||||
|
||||
# Header only
|
||||
printf "${clear_line}${PURPLE}%s${NC} ${GRAY}%d/%d selected${NC}\n" "${title}" "$selected_count" "$total_items" >&2
|
||||
|
||||
# Visible slice
|
||||
local visible_total=${#view_indices[@]}
|
||||
if [[ $visible_total -eq 0 ]]; then
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
# While editing: do not show "No items available"
|
||||
for ((i = 0; i < items_per_page; i++)); do
|
||||
printf "${clear_line}\n" >&2
|
||||
done
|
||||
printf "${clear_line}${GRAY}Type to filter | Delete | Enter | / Exit | ESC${NC}\n" >&2
|
||||
printf "${clear_line}" >&2
|
||||
return
|
||||
else
|
||||
if [[ "$searching" == "true" ]]; then
|
||||
printf "${clear_line}Searching…\n" >&2
|
||||
for ((i = 0; i < items_per_page; i++)); do
|
||||
printf "${clear_line}\n" >&2
|
||||
done
|
||||
printf "${clear_line}${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN} | Space | Enter | / Filter | Q Exit${NC}\n" >&2
|
||||
printf "${clear_line}" >&2
|
||||
return
|
||||
else
|
||||
# Post-search: truly empty list
|
||||
printf "${clear_line}No items available\n" >&2
|
||||
for ((i = 0; i < items_per_page; i++)); do
|
||||
printf "${clear_line}\n" >&2
|
||||
done
|
||||
printf "${clear_line}${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN} | Space | Enter | / Filter | Q Exit${NC}\n" >&2
|
||||
printf "${clear_line}" >&2
|
||||
return
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
local visible_count=$((visible_total - top_index))
|
||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||
[[ $visible_count -le 0 ]] && visible_count=1
|
||||
if [[ $cursor_pos -ge $visible_count ]]; then
|
||||
cursor_pos=$((visible_count - 1))
|
||||
[[ $cursor_pos -lt 0 ]] && cursor_pos=0
|
||||
fi
|
||||
|
||||
printf "${clear_line}\n" >&2
|
||||
|
||||
# Items for current window
|
||||
local start_idx=$top_index
|
||||
local end_idx=$((top_index + items_per_page - 1))
|
||||
[[ $end_idx -ge $visible_total ]] && end_idx=$((visible_total - 1))
|
||||
|
||||
for ((i = start_idx; i <= end_idx; i++)); do
|
||||
[[ $i -lt 0 ]] && continue
|
||||
local is_current=false
|
||||
[[ $((i - start_idx)) -eq $cursor_pos ]] && is_current=true
|
||||
render_item $((i - start_idx)) $is_current
|
||||
done
|
||||
|
||||
# Fill empty slots to clear previous content
|
||||
local items_shown=$((end_idx - start_idx + 1))
|
||||
[[ $items_shown -lt 0 ]] && items_shown=0
|
||||
for ((i = items_shown; i < items_per_page; i++)); do
|
||||
printf "${clear_line}\n" >&2
|
||||
done
|
||||
|
||||
printf "${clear_line}\n" >&2
|
||||
|
||||
# Build sort and filter status
|
||||
local sort_label=""
|
||||
case "$sort_mode" in
|
||||
date) sort_label="Date" ;;
|
||||
name) sort_label="Name" ;;
|
||||
size) sort_label="Size" ;;
|
||||
esac
|
||||
local sort_status="${sort_label}"
|
||||
|
||||
local filter_status=""
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
filter_status="${filter_query:-_}"
|
||||
elif [[ -n "$applied_query" ]]; then
|
||||
filter_status="${applied_query}"
|
||||
else
|
||||
filter_status="—"
|
||||
fi
|
||||
|
||||
# Footer: single line with controls
|
||||
local sep=" ${GRAY}|${NC} "
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
# Filter mode: simple controls without sort
|
||||
local -a _segs_filter=(
|
||||
"${GRAY}Filter: ${filter_status}${NC}"
|
||||
"${GRAY}Delete${NC}"
|
||||
"${GRAY}Enter${NC}"
|
||||
"${GRAY}/ Exit${NC}"
|
||||
"${GRAY}ESC${NC}"
|
||||
)
|
||||
_print_wrapped_controls "$sep" "${_segs_filter[@]}"
|
||||
else
|
||||
# Normal mode - single line compact format
|
||||
local reverse_arrow="↑"
|
||||
[[ "$sort_reverse" == "true" ]] && reverse_arrow="↓"
|
||||
|
||||
# Determine filter text based on whether filter is active
|
||||
local filter_text="/ Filter"
|
||||
[[ -n "$applied_query" ]] && filter_text="/ Clear"
|
||||
|
||||
if [[ "$has_metadata" == "true" ]]; then
|
||||
if [[ -n "$applied_query" ]]; then
|
||||
# Filtering: hide sort controls
|
||||
local -a _segs_all=(
|
||||
"${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}"
|
||||
"${GRAY}Space${NC}"
|
||||
"${GRAY}Enter${NC}"
|
||||
"${GRAY}${filter_text}${NC}"
|
||||
"${GRAY}Q Exit${NC}"
|
||||
)
|
||||
_print_wrapped_controls "$sep" "${_segs_all[@]}"
|
||||
else
|
||||
# Normal: show full controls
|
||||
local -a _segs_all=(
|
||||
"${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}"
|
||||
"${GRAY}Space${NC}"
|
||||
"${GRAY}Enter${NC}"
|
||||
"${GRAY}${filter_text}${NC}"
|
||||
"${GRAY}S ${sort_status}${NC}"
|
||||
"${GRAY}R ${reverse_arrow}${NC}"
|
||||
"${GRAY}Q Exit${NC}"
|
||||
)
|
||||
_print_wrapped_controls "$sep" "${_segs_all[@]}"
|
||||
fi
|
||||
else
|
||||
# Without metadata: basic controls
|
||||
local -a _segs_simple=(
|
||||
"${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}"
|
||||
"${GRAY}Space${NC}"
|
||||
"${GRAY}Enter${NC}"
|
||||
"${GRAY}${filter_text}${NC}"
|
||||
"${GRAY}Q Exit${NC}"
|
||||
)
|
||||
_print_wrapped_controls "$sep" "${_segs_simple[@]}"
|
||||
fi
|
||||
fi
|
||||
printf "${clear_line}" >&2
|
||||
}
|
||||
|
||||
# Main interaction loop
|
||||
while true; do
|
||||
draw_menu
|
||||
local key
|
||||
key=$(read_key)
|
||||
|
||||
case "$key" in
|
||||
"QUIT")
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
filter_mode="false"
|
||||
unset MOLE_READ_KEY_FORCE_CHAR
|
||||
filter_query=""
|
||||
applied_query=""
|
||||
top_index=0
|
||||
cursor_pos=0
|
||||
rebuild_view
|
||||
continue
|
||||
fi
|
||||
cleanup
|
||||
return 1
|
||||
;;
|
||||
"UP")
|
||||
if [[ ${#view_indices[@]} -eq 0 ]]; then
|
||||
:
|
||||
elif [[ $cursor_pos -gt 0 ]]; then
|
||||
((cursor_pos--))
|
||||
elif [[ $top_index -gt 0 ]]; then
|
||||
((top_index--))
|
||||
fi
|
||||
;;
|
||||
"DOWN")
|
||||
if [[ ${#view_indices[@]} -eq 0 ]]; then
|
||||
:
|
||||
else
|
||||
local absolute_index=$((top_index + cursor_pos))
|
||||
local last_index=$((${#view_indices[@]} - 1))
|
||||
if [[ $absolute_index -lt $last_index ]]; then
|
||||
local visible_count=$((${#view_indices[@]} - top_index))
|
||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||
|
||||
if [[ $cursor_pos -lt $((visible_count - 1)) ]]; then
|
||||
((cursor_pos++))
|
||||
elif [[ $((top_index + visible_count)) -lt ${#view_indices[@]} ]]; then
|
||||
((top_index++))
|
||||
visible_count=$((${#view_indices[@]} - top_index))
|
||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||
if [[ $cursor_pos -ge $visible_count ]]; then
|
||||
cursor_pos=$((visible_count - 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"SPACE")
|
||||
local idx=$((top_index + cursor_pos))
|
||||
if [[ $idx -lt ${#view_indices[@]} ]]; then
|
||||
local real="${view_indices[idx]}"
|
||||
if [[ ${selected[real]} == true ]]; then
|
||||
selected[real]=false
|
||||
else
|
||||
selected[real]=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"RETRY")
|
||||
# 'R' toggles reverse order (only if metadata available)
|
||||
if [[ "$has_metadata" == "true" ]]; then
|
||||
if [[ "$sort_reverse" == "true" ]]; then
|
||||
sort_reverse="false"
|
||||
else
|
||||
sort_reverse="true"
|
||||
fi
|
||||
rebuild_view
|
||||
fi
|
||||
;;
|
||||
"CHAR:s" | "CHAR:S")
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
local ch="${key#CHAR:}"
|
||||
filter_query+="$ch"
|
||||
elif [[ "$has_metadata" == "true" ]]; then
|
||||
# Cycle sort mode (only if metadata available)
|
||||
case "$sort_mode" in
|
||||
date) sort_mode="name" ;;
|
||||
name) sort_mode="size" ;;
|
||||
size) sort_mode="date" ;;
|
||||
esac
|
||||
rebuild_view
|
||||
fi
|
||||
;;
|
||||
"FILTER")
|
||||
# / key: toggle between filter and return
|
||||
if [[ -n "$applied_query" ]]; then
|
||||
# Already filtering, clear and return to full list
|
||||
applied_query=""
|
||||
filter_query=""
|
||||
top_index=0
|
||||
cursor_pos=0
|
||||
rebuild_view
|
||||
else
|
||||
# Enter filter mode
|
||||
filter_mode="true"
|
||||
export MOLE_READ_KEY_FORCE_CHAR=1
|
||||
filter_query=""
|
||||
top_index=0
|
||||
cursor_pos=0
|
||||
rebuild_view
|
||||
fi
|
||||
;;
|
||||
"CHAR:f" | "CHAR:F")
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
filter_query+="${key#CHAR:}"
|
||||
fi
|
||||
;;
|
||||
"CHAR:r")
|
||||
# lower-case r: behave like reverse when NOT in filter mode
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
filter_query+="r"
|
||||
else
|
||||
if [[ "$sort_reverse" == "true" ]]; then
|
||||
sort_reverse="false"
|
||||
else
|
||||
sort_reverse="true"
|
||||
fi
|
||||
rebuild_view
|
||||
fi
|
||||
;;
|
||||
"DELETE")
|
||||
# Backspace filter
|
||||
if [[ "$filter_mode" == "true" && -n "$filter_query" ]]; then
|
||||
filter_query="${filter_query%?}"
|
||||
fi
|
||||
;;
|
||||
CHAR:*)
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
local ch="${key#CHAR:}"
|
||||
# Special handling for /: exit filter mode
|
||||
if [[ "$ch" == "/" ]]; then
|
||||
filter_mode="false"
|
||||
unset MOLE_READ_KEY_FORCE_CHAR
|
||||
filter_query=""
|
||||
applied_query=""
|
||||
rebuild_view
|
||||
# avoid accidental leading spaces
|
||||
elif [[ -n "$filter_query" || "$ch" != " " ]]; then
|
||||
filter_query+="$ch"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"ENTER")
|
||||
if [[ "$filter_mode" == "true" ]]; then
|
||||
applied_query="$filter_query"
|
||||
filter_mode="false"
|
||||
unset MOLE_READ_KEY_FORCE_CHAR
|
||||
top_index=0
|
||||
cursor_pos=0
|
||||
|
||||
searching="true"
|
||||
draw_menu # paint "searching..."
|
||||
drain_pending_input # drop any extra keypresses (e.g., double-Enter)
|
||||
rebuild_view
|
||||
searching="false"
|
||||
draw_menu
|
||||
continue
|
||||
fi
|
||||
# In normal mode: smart Enter behavior
|
||||
# 1. Check if any items are already selected
|
||||
local has_selection=false
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
if [[ ${selected[i]} == true ]]; then
|
||||
has_selection=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. If nothing selected, auto-select current item
|
||||
if [[ $has_selection == false ]]; then
|
||||
local idx=$((top_index + cursor_pos))
|
||||
if [[ $idx -lt ${#view_indices[@]} ]]; then
|
||||
local real="${view_indices[idx]}"
|
||||
selected[real]=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Confirm and exit with current selections
|
||||
local -a selected_indices=()
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
if [[ ${selected[i]} == true ]]; then
|
||||
selected_indices+=("$i")
|
||||
fi
|
||||
done
|
||||
|
||||
local final_result=""
|
||||
if [[ ${#selected_indices[@]} -gt 0 ]]; then
|
||||
local IFS=','
|
||||
final_result="${selected_indices[*]}"
|
||||
fi
|
||||
|
||||
trap - EXIT INT TERM
|
||||
MOLE_SELECTION_RESULT="$final_result"
|
||||
restore_terminal
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Drain any accumulated input after processing (e.g., mouse wheel events)
|
||||
# This prevents buffered events from causing jumps, without blocking keyboard input
|
||||
drain_pending_input
|
||||
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
|
||||
Reference in New Issue
Block a user