diff --git a/lib/common.sh b/lib/common.sh index 5eee8da..fc5afb2 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -208,12 +208,10 @@ read_key() { case "$key" in $'\n' | $'\r') echo "ENTER" ;; ' ') echo "SPACE" ;; - 'q' | 'Q') echo "QUIT" ;; - 'A') echo "ALL" ;; - 'N') echo "NONE" ;; + 'Q') echo "QUIT" ;; 'R') echo "RETRY" ;; - '?') echo "HELP" ;; 'o' | 'O') echo "OPEN" ;; + '/') echo "FILTER" ;; # Trigger filter mode $'\x03') echo "QUIT" ;; # Ctrl+C $'\x7f' | $'\x08') echo "DELETE" ;; # Backspace/Delete key $'\x1b') diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh index c858656..a52283b 100755 --- a/lib/paginated_menu.sh +++ b/lib/paginated_menu.sh @@ -4,8 +4,16 @@ set -euo pipefail # Terminal control functions -enter_alt_screen() { tput smcup 2> /dev/null || true; } -leave_alt_screen() { tput rmcup 2> /dev/null || true; } +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 +} # Parse CSV into newline list (Bash 3.2) _pm_parse_csv_to_array() { @@ -61,11 +69,19 @@ paginated_multi_select() { # 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 @@ -91,20 +107,11 @@ paginated_multi_select() { printf '%s' "$out" } - # Case-insensitive: substring by default, prefix if query starts with ' + # Case-insensitive fuzzy match (substring search) _pm_match() { - local hay="$1" q="$2" anchored=0 - if [[ "$q" == \'* ]]; then - anchored=1 - q="${q:1}" - fi + local hay="$1" q="$2" q="$(_pm_escape_glob "$q")" - local pat - if [[ $anchored -eq 1 ]]; then - pat="${q}*" - else - pat="*${q}*" - fi + local pat="*${q}*" shopt -s nocasematch local ok=1 @@ -221,10 +228,10 @@ paginated_multi_select() { local -a filtered=() local effective_query="" if [[ "$filter_mode" == "true" ]]; then - # Live editing: empty query -> show nothing; non-empty -> match + # Live editing: empty query -> show all items effective_query="$filter_query" if [[ -z "$effective_query" ]]; then - filtered=() + filtered=("${orig_indices[@]}") else local idx for ((idx = 0; idx < total_items; idx++)); do @@ -248,42 +255,54 @@ paginated_multi_select() { fi fi - # Sort - local tmpfile - tmpfile=$(mktemp) || tmpfile="" - if [[ -n "$tmpfile" ]]; then - : > "$tmpfile" - local k id - if [[ ${#filtered[@]} -gt 0 ]]; then + # 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]:-${id}}" ;; + 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 - fi - # Build sort key once and stream results into view_indices - local sort_key - if [[ "$sort_mode" == "date" || "$sort_mode" == "size" ]]; then - sort_key="-k1,1n" - [[ "$sort_reverse" == "true" ]] && sort_key="-k1,1nr" + 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 - sort_key="-k1,1f" - [[ "$sort_reverse" == "true" ]] && sort_key="-k1,1fr" + # Fallback: no sorting + view_indices=("${filtered[@]}") fi - - 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") - - rm -f "$tmpfile" - else - view_indices=("${filtered[@]}") fi # Clamp cursor into visible range @@ -334,38 +353,9 @@ paginated_multi_select() { [[ ${selected[i]} == true ]] && ((selected_count++)) done - # Header + # Header only printf "${clear_line}${PURPLE}%s${NC} ${GRAY}%d/%d selected${NC}\n" "${title}" "$selected_count" "$total_items" >&2 - # Sort + Filter status - local sort_label="" - case "$sort_mode" in - date) sort_label="Date" ;; - name) sort_label="Name" ;; - size) sort_label="Size" ;; - esac - local arrow="↑" - [[ "$sort_reverse" == "true" ]] && arrow="↓" - - local filter_label="" - if [[ "$filter_mode" == "true" ]]; then - filter_label="${YELLOW}${filter_query:-}${NC}${GRAY} [editing]${NC}" - else - if [[ -n "$applied_query" ]]; then - if [[ "$searching" == "true" ]]; then - filter_label="${GREEN}${applied_query}${NC}${GRAY} [searching…]${NC}" - else - filter_label="${GREEN}${applied_query}${NC}" - fi - else - filter_label="${GRAY}—${NC}" - fi - fi - printf "${clear_line}${GRAY}Sort:${NC} %s %s ${GRAY}|${NC} ${GRAY}Filter:${NC} %s\n" "$sort_label" "$arrow" "$filter_label" >&2 - - # Filter-mode hint line - if [[ "$filter_mode" == "true" ]]; then - printf "${clear_line}${GRAY}Tip:${NC} prefix with ${YELLOW}'${NC} to match from start\n" >&2 - fi + printf "${clear_line}\n" >&2 # Visible slice local visible_total=${#view_indices[@]} @@ -384,7 +374,7 @@ paginated_multi_select() { for ((i = 0; i < items_per_page + 2; i++)); do printf "${clear_line}\n" >&2 done - printf "${clear_line}${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}S/s${NC} Sort ${GRAY}|${NC} ${GRAY}R/r${NC} Reverse ${GRAY}|${NC} ${GRAY}F/f${NC} Filter ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit\n" >&2 + printf "${clear_line}${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Nav ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}/${NC} Filter ${GRAY}|${NC} ${GRAY}S${NC} Sort ${GRAY}|${NC} ${GRAY}Q${NC} Quit\n" >&2 printf "${clear_line}" >&2 return else @@ -393,7 +383,7 @@ paginated_multi_select() { for ((i = 0; i < items_per_page + 2; i++)); do printf "${clear_line}\n" >&2 done - printf "${clear_line}${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}S${NC} Sort ${GRAY}|${NC} ${GRAY}R/r${NC} Reverse ${GRAY}|${NC} ${GRAY}F${NC} Filter ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit\n" >&2 + printf "${clear_line}${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Nav ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}/${NC} Filter ${GRAY}|${NC} ${GRAY}S${NC} Sort ${GRAY}|${NC} ${GRAY}Q${NC} Quit\n" >&2 printf "${clear_line}" >&2 return fi @@ -430,55 +420,71 @@ paginated_multi_select() { done printf "${clear_line}\n" >&2 - # Footer with wrapped controls + + # 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 arrow="↑" + [[ "$sort_reverse" == "true" ]] && arrow="↓" + local sort_status="${sort_label} ${arrow}" + + local filter_status="" + if [[ "$filter_mode" == "true" ]]; then + filter_status="${YELLOW}${filter_query:-}${NC}" + elif [[ -n "$applied_query" ]]; then + filter_status="${GREEN}${applied_query}${NC}" + else + filter_status="${GRAY}—${NC}" + fi + + # Footer with two lines: basic controls and advanced options local sep=" ${GRAY}|${NC} " if [[ "$filter_mode" == "true" ]]; then + # Filter mode: single line with all filter controls local -a _segs_filter=( - "${GRAY}Type to filter${NC}" - "${GRAY}Delete${NC} Backspace" + "${GRAY}Filter Input:${NC} ${filter_status}" + "${GRAY}Delete${NC} Back" "${GRAY}Enter${NC} Apply" + "${GRAY}/${NC} Clear" "${GRAY}ESC${NC} Cancel" ) _print_wrapped_controls "$sep" "${_segs_filter[@]}" else - local -a _segs_normal=( - "${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate" - "${GRAY}Space${NC} Select" - "${GRAY}Enter${NC} Confirm" - "${GRAY}S/s${NC} Sort" - "${GRAY}R/r${NC} Reverse" - "${GRAY}F/f${NC} Filter" - "${GRAY}A${NC} All" - "${GRAY}N${NC} None" - "${GRAY}Q/ESC${NC} Quit" - ) - _print_wrapped_controls "$sep" "${_segs_normal[@]}" + # Normal mode + if [[ "$has_metadata" == "true" ]]; then + # With metadata: two lines (basic + advanced) + local -a _segs_basic=( + "${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate" + "${GRAY}Space${NC} Select" + "${GRAY}Enter${NC} Confirm" + "${GRAY}Q/ESC${NC} Quit" + ) + _print_wrapped_controls "$sep" "${_segs_basic[@]}" + local -a _segs_advanced=( + "${GRAY}S${NC} ${sort_status}" + "${GRAY}R${NC} Reverse" + "${GRAY}/${NC} Filter" + ) + _print_wrapped_controls "$sep" "${_segs_advanced[@]}" + else + # Without metadata: single line (basic only) + local -a _segs_simple=( + "${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate" + "${GRAY}Space${NC} Select" + "${GRAY}Enter${NC} Confirm" + "${GRAY}/${NC} Filter" + "${GRAY}Q/ESC${NC} Quit" + ) + _print_wrapped_controls "$sep" "${_segs_simple[@]}" + fi fi printf "${clear_line}" >&2 } - # Show help screen - show_help() { - printf "\033[H\033[J" >&2 - cat >&2 << EOF -Help - Navigation Controls -========================== - - ${ICON_NAV_UP} / ${ICON_NAV_DOWN} Navigate up/down - Space Select/deselect item - Enter Confirm selection - S Change sort mode (Date / Name / Size) - R Reverse current sort (asc/desc) - F Toggle filter mode, type to filter (case-insensitive; prefix with ' to match from start) - A Select all (visible items) - N Deselect all (visible items) - Delete Backspace filter (in filter mode) - Q / ESC Exit (ESC exits filter mode first) - -Press any key to continue... -EOF - read -n 1 -s >&2 - } # Main interaction loop while true; do @@ -543,37 +549,23 @@ EOF fi fi ;; - "ALL") - # Select only currently visible (filtered) rows - if [[ ${#view_indices[@]} -gt 0 ]]; then - for real in "${view_indices[@]}"; do - selected[real]=true - done - fi - ;; - "NONE") - # Deselect only currently visible (filtered) rows - if [[ ${#view_indices[@]} -gt 0 ]]; then - for real in "${view_indices[@]}"; do - selected[real]=false - done - fi - ;; "RETRY") - # 'R' toggles reverse order - if [[ "$sort_reverse" == "true" ]]; then - sort_reverse="false" - else - sort_reverse="true" + # '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 - rebuild_view ;; - "HELP") show_help ;; "CHAR:s"|"CHAR:S") if [[ "$filter_mode" == "true" ]]; then local ch="${key#CHAR:}" filter_query+="$ch" - else + elif [[ "$has_metadata" == "true" ]]; then + # Cycle sort mode (only if metadata available) case "$sort_mode" in date) sort_mode="name" ;; name) sort_mode="size" ;; @@ -582,16 +574,18 @@ EOF rebuild_view fi ;; + "FILTER") + # Trigger filter mode with / + filter_mode="true" + export MOLE_READ_KEY_FORCE_CHAR=1 + filter_query="" + top_index=0 + cursor_pos=0 + rebuild_view + ;; "CHAR:f"|"CHAR:F") if [[ "$filter_mode" == "true" ]]; then - filter_query+="f" - else - filter_mode="true" - export MOLE_READ_KEY_FORCE_CHAR=1 - filter_query="" # start empty -> 0 results - top_index=0 # reset viewport - cursor_pos=0 - rebuild_view + filter_query+="${key#CHAR:}" fi ;; "CHAR:r") @@ -616,8 +610,12 @@ EOF CHAR:*) if [[ "$filter_mode" == "true" ]]; then local ch="${key#CHAR:}" + # Special handling for /: clear filter + if [[ "$ch" == "/" ]]; then + filter_query="" + rebuild_view # avoid accidental leading spaces - if [[ -n "$filter_query" || "$ch" != " " ]]; then + elif [[ -n "$filter_query" || "$ch" != " " ]]; then filter_query+="$ch" fi fi @@ -637,6 +635,7 @@ EOF draw_menu continue fi + # In normal mode: confirm and exit with current selections local -a selected_indices=() for ((i = 0; i < total_items; i++)); do if [[ ${selected[i]} == true ]]; then @@ -644,24 +643,20 @@ EOF fi done - # Allow empty selection - don't auto-select cursor position local final_result="" if [[ ${#selected_indices[@]} -gt 0 ]]; then local IFS=',' final_result="${selected_indices[*]}" fi - # 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 restore_terminal - return 0 ;; + "HELP") + # Removed help screen, users can explore the interface + ;; esac done } diff --git a/lib/simple_menu.sh b/lib/simple_menu.sh new file mode 100755 index 0000000..4496a6b --- /dev/null +++ b/lib/simple_menu.sh @@ -0,0 +1,292 @@ +#!/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=("$@") + 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=15 + local cursor_pos=0 + local top_index=0 + 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 + } + + # 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; } + + render_item() { + local idx=$1 is_current=$2 + local checkbox="$ICON_EMPTY" + [[ ${selected[idx]} == true ]] && checkbox="$ICON_SOLID" + + if [[ $is_current == true ]]; then + printf "\r\033[2K${BLUE}${ICON_ARROW} %s %s${NC}\n" "$checkbox" "${items[idx]}" >&2 + else + printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2 + fi + } + + # Draw the complete menu + draw_menu() { + # Move to home position without clearing (reduces flicker) + printf "\033[H" >&2 + + # Clear each line as we go instead of clearing entire screen + local clear_line="\r\033[2K" + + # Count selections for header display + local selected_count=0 + for ((i = 0; i < total_items; i++)); do + [[ ${selected[i]} == true ]] && ((selected_count++)) + done + + # Header + printf "${clear_line}${PURPLE}%s${NC} ${GRAY}%d/%d selected${NC}\n" "${title}" "$selected_count" "$total_items" >&2 + + if [[ $total_items -eq 0 ]]; then + printf "${clear_line}${GRAY}No items available${NC}\n" >&2 + printf "${clear_line}\n" >&2 + printf "${clear_line}${GRAY}Q/ESC${NC} Quit\n" >&2 + printf "${clear_line}" >&2 + return + fi + + if [[ $top_index -gt $((total_items - 1)) ]]; then + if [[ $total_items -gt $items_per_page ]]; then + top_index=$((total_items - items_per_page)) + else + top_index=0 + fi + fi + + local visible_count=$((total_items - 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 $total_items ]] && end_idx=$((total_items - 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 $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 + + # Clear any remaining lines at bottom + printf "${clear_line}\n" >&2 + printf "${clear_line}${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit\n" >&2 + + # Clear one more line to ensure no artifacts + printf "${clear_line}" >&2 + } + + # Show help screen + show_help() { + printf "\033[H\033[J" >&2 + cat >&2 << EOF +Help - Navigation Controls +========================== + + ${ICON_NAV_UP} / ${ICON_NAV_DOWN} Navigate up/down + Space Select/deselect item + Enter Confirm selection + Q / ESC 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 [[ $total_items -eq 0 ]]; then + : + elif [[ $cursor_pos -gt 0 ]]; then + ((cursor_pos--)) + elif [[ $top_index -gt 0 ]]; then + ((top_index--)) + fi + ;; + "DOWN") + if [[ $total_items -eq 0 ]]; then + : + else + local absolute_index=$((top_index + cursor_pos)) + if [[ $absolute_index -lt $((total_items - 1)) ]]; then + local visible_count=$((total_items - 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 $total_items ]]; then + ((top_index++)) + visible_count=$((total_items - 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 $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") + # Store result in global variable instead of returning via stdout + local -a selected_indices=() + for ((i = 0; i < total_items; i++)); do + if [[ ${selected[i]} == true ]]; then + selected_indices+=("$i") + fi + done + + # Allow empty selection - don't auto-select cursor position + # This fixes the bug where unselecting all items would still select the last cursor position + local final_result="" + if [[ ${#selected_indices[@]} -gt 0 ]]; then + local IFS=',' + final_result="${selected_indices[*]}" + fi + + # 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 + restore_terminal + + 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 diff --git a/lib/whitelist_manager.sh b/lib/whitelist_manager.sh index 6cb2fcc..827bef7 100755 --- a/lib/whitelist_manager.sh +++ b/lib/whitelist_manager.sh @@ -7,7 +7,7 @@ set -euo pipefail # Get script directory and source dependencies SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" -source "$SCRIPT_DIR/paginated_menu.sh" +source "$SCRIPT_DIR/simple_menu.sh" # Config file path WHITELIST_CONFIG="$HOME/.config/mole/whitelist"