mirror of
https://github.com/tw93/Mole.git
synced 2026-02-12 15:09:28 +00:00
refactor(uninstall): remove search and optimize scan performance
- Remove complex search/filter feature from menu (simplify UX) - Optimize Homebrew cask detection (only check Caskroom symlinks) - Cache whoami outside loop, use bash parameter expansion - Fix spinner overlap with error messages - Add trap for Ctrl+C cleanup - Use ICON_ERROR for failure messages
This commit is contained in:
@@ -87,13 +87,8 @@ paginated_multi_select() {
|
|||||||
local items_per_page=$(_pm_calculate_items_per_page)
|
local items_per_page=$(_pm_calculate_items_per_page)
|
||||||
local cursor_pos=0
|
local cursor_pos=0
|
||||||
local top_index=0
|
local top_index=0
|
||||||
local filter_query=""
|
|
||||||
local filter_mode="false" # filter mode toggle
|
|
||||||
local sort_mode="${MOLE_MENU_SORT_MODE:-${MOLE_MENU_SORT_DEFAULT:-date}}" # date|name|size
|
local sort_mode="${MOLE_MENU_SORT_MODE:-${MOLE_MENU_SORT_DEFAULT:-date}}" # date|name|size
|
||||||
local sort_reverse="${MOLE_MENU_SORT_REVERSE:-false}"
|
local sort_reverse="${MOLE_MENU_SORT_REVERSE:-false}"
|
||||||
# Live query vs applied query
|
|
||||||
local applied_query=""
|
|
||||||
local searching="false"
|
|
||||||
|
|
||||||
# Metadata (optional)
|
# Metadata (optional)
|
||||||
# epochs[i] -> last_used_epoch (numeric) for item i
|
# epochs[i] -> last_used_epoch (numeric) for item i
|
||||||
@@ -124,36 +119,6 @@ paginated_multi_select() {
|
|||||||
view_indices[i]=$i
|
view_indices[i]=$i
|
||||||
done
|
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=()
|
local -a selected=()
|
||||||
local selected_count=0 # Cache selection count to avoid O(n) loops on every draw
|
local selected_count=0 # Cache selection count to avoid O(n) loops on every draw
|
||||||
|
|
||||||
@@ -267,44 +232,13 @@ paginated_multi_select() {
|
|||||||
printf "%s%s\n" "$clear_line" "$line" >&2
|
printf "%s%s\n" "$clear_line" "$line" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
# Rebuild the view_indices applying filter and sort
|
# Rebuild the view_indices applying sort
|
||||||
rebuild_view() {
|
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)
|
# Sort (skip if no metadata)
|
||||||
if [[ "$has_metadata" == "false" ]]; then
|
if [[ "$has_metadata" == "false" ]]; then
|
||||||
# No metadata: just use filtered list (already sorted by name naturally)
|
# No metadata: just use original indices
|
||||||
view_indices=("${filtered[@]}")
|
view_indices=("${orig_indices[@]}")
|
||||||
elif [[ ${#filtered[@]} -eq 0 ]]; then
|
elif [[ ${#orig_indices[@]} -eq 0 ]]; then
|
||||||
view_indices=()
|
view_indices=()
|
||||||
else
|
else
|
||||||
# Build sort key
|
# Build sort key
|
||||||
@@ -328,7 +262,7 @@ paginated_multi_select() {
|
|||||||
tmpfile=$(mktemp 2> /dev/null) || tmpfile=""
|
tmpfile=$(mktemp 2> /dev/null) || tmpfile=""
|
||||||
if [[ -n "$tmpfile" ]]; then
|
if [[ -n "$tmpfile" ]]; then
|
||||||
local k id
|
local k id
|
||||||
for id in "${filtered[@]}"; do
|
for id in "${orig_indices[@]}"; do
|
||||||
case "$sort_mode" in
|
case "$sort_mode" in
|
||||||
date) k="${epochs[id]:-0}" ;;
|
date) k="${epochs[id]:-0}" ;;
|
||||||
size) k="${sizekb[id]:-0}" ;;
|
size) k="${sizekb[id]:-0}" ;;
|
||||||
@@ -346,7 +280,7 @@ paginated_multi_select() {
|
|||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
else
|
else
|
||||||
# Fallback: no sorting
|
# Fallback: no sorting
|
||||||
view_indices=("${filtered[@]}")
|
view_indices=("${orig_indices[@]}")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -404,34 +338,13 @@ paginated_multi_select() {
|
|||||||
# Visible slice
|
# Visible slice
|
||||||
local visible_total=${#view_indices[@]}
|
local visible_total=${#view_indices[@]}
|
||||||
if [[ $visible_total -eq 0 ]]; then
|
if [[ $visible_total -eq 0 ]]; then
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
printf "${clear_line}No items available\n" >&2
|
||||||
# While editing: do not show "No items available"
|
for ((i = 0; i < items_per_page; i++)); do
|
||||||
for ((i = 0; i < items_per_page; i++)); do
|
printf "${clear_line}\n" >&2
|
||||||
printf "${clear_line}\n" >&2
|
done
|
||||||
done
|
printf "${clear_line}${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN} | Space | Enter | Q Exit${NC}\n" >&2
|
||||||
printf "${clear_line}${GRAY}Type to filter | Delete | Enter Confirm | ESC Cancel${NC}\n" >&2
|
printf "${clear_line}" >&2
|
||||||
printf "${clear_line}" >&2
|
return
|
||||||
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
|
fi
|
||||||
|
|
||||||
local visible_count=$((visible_total - top_index))
|
local visible_count=$((visible_total - top_index))
|
||||||
@@ -465,7 +378,7 @@ paginated_multi_select() {
|
|||||||
|
|
||||||
printf "${clear_line}\n" >&2
|
printf "${clear_line}\n" >&2
|
||||||
|
|
||||||
# Build sort and filter status
|
# Build sort status
|
||||||
local sort_label=""
|
local sort_label=""
|
||||||
case "$sort_mode" in
|
case "$sort_mode" in
|
||||||
date) sort_label="Date" ;;
|
date) sort_label="Date" ;;
|
||||||
@@ -474,15 +387,6 @@ paginated_multi_select() {
|
|||||||
esac
|
esac
|
||||||
local sort_status="${sort_label}"
|
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
|
# Footer: single line with controls
|
||||||
local sep=" ${GRAY}|${NC} "
|
local sep=" ${GRAY}|${NC} "
|
||||||
|
|
||||||
@@ -497,77 +401,54 @@ paginated_multi_select() {
|
|||||||
# Common menu items
|
# Common menu items
|
||||||
local nav="${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}"
|
local nav="${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}"
|
||||||
local space_select="${GRAY}Space Select${NC}"
|
local space_select="${GRAY}Space Select${NC}"
|
||||||
local space="${GRAY}Space${NC}"
|
|
||||||
local enter="${GRAY}Enter${NC}"
|
local enter="${GRAY}Enter${NC}"
|
||||||
local exit="${GRAY}Q Exit${NC}"
|
local exit="${GRAY}Q Exit${NC}"
|
||||||
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
local reverse_arrow="↑"
|
||||||
# Filter mode: simple controls without sort
|
[[ "$sort_reverse" == "true" ]] && reverse_arrow="↓"
|
||||||
local -a _segs_filter=(
|
|
||||||
"${GRAY}Search: ${filter_status}${NC}"
|
|
||||||
"${GRAY}Delete${NC}"
|
|
||||||
"${GRAY}Enter Confirm${NC}"
|
|
||||||
"${GRAY}ESC Cancel${NC}"
|
|
||||||
)
|
|
||||||
_print_wrapped_controls "$sep" "${_segs_filter[@]}"
|
|
||||||
else
|
|
||||||
# Normal mode - prepare dynamic items
|
|
||||||
local reverse_arrow="↑"
|
|
||||||
[[ "$sort_reverse" == "true" ]] && reverse_arrow="↓"
|
|
||||||
|
|
||||||
local filter_text="/ Search"
|
local refresh="${GRAY}R Refresh${NC}"
|
||||||
[[ -n "$applied_query" ]] && filter_text="/ Clear"
|
local sort_ctrl="${GRAY}S ${sort_status}${NC}"
|
||||||
|
local order_ctrl="${GRAY}O ${reverse_arrow}${NC}"
|
||||||
|
|
||||||
local refresh="${GRAY}R Refresh${NC}"
|
if [[ "$has_metadata" == "true" ]]; then
|
||||||
local search="${GRAY}${filter_text}${NC}"
|
# With metadata: show sort controls
|
||||||
local sort_ctrl="${GRAY}S ${sort_status}${NC}"
|
local term_width="${COLUMNS:-}"
|
||||||
local order_ctrl="${GRAY}O ${reverse_arrow}${NC}"
|
[[ -z "$term_width" ]] && term_width=$(tput cols 2> /dev/null || echo 80)
|
||||||
|
[[ "$term_width" =~ ^[0-9]+$ ]] || term_width=80
|
||||||
|
|
||||||
if [[ "$has_metadata" == "true" ]]; then
|
# Full controls
|
||||||
if [[ -n "$applied_query" ]]; then
|
local -a _segs=("$nav" "$space_select" "$enter" "$refresh" "$sort_ctrl" "$order_ctrl" "$exit")
|
||||||
# Filtering active: hide sort controls
|
|
||||||
local -a _segs_all=("$nav" "$space" "$enter" "$refresh" "$search" "$exit")
|
|
||||||
_print_wrapped_controls "$sep" "${_segs_all[@]}"
|
|
||||||
else
|
|
||||||
# Normal: show full controls with dynamic reduction
|
|
||||||
local term_width="${COLUMNS:-}"
|
|
||||||
[[ -z "$term_width" ]] && term_width=$(tput cols 2> /dev/null || echo 80)
|
|
||||||
[[ "$term_width" =~ ^[0-9]+$ ]] || term_width=80
|
|
||||||
|
|
||||||
# Level 0: Full controls
|
# Calculate width
|
||||||
local -a _segs=("$nav" "$space_select" "$enter" "$refresh" "$search" "$sort_ctrl" "$order_ctrl" "$exit")
|
local total_len=0 seg_count=${#_segs[@]}
|
||||||
|
for i in "${!_segs[@]}"; do
|
||||||
|
total_len=$((total_len + $(_calc_len "${_segs[i]}")))
|
||||||
|
[[ $i -lt $((seg_count - 1)) ]] && total_len=$((total_len + 3))
|
||||||
|
done
|
||||||
|
|
||||||
# Calculate width
|
# Level 1: Remove "Space Select" if too wide
|
||||||
local total_len=0 seg_count=${#_segs[@]}
|
if [[ $total_len -gt $term_width ]]; then
|
||||||
for i in "${!_segs[@]}"; do
|
_segs=("$nav" "$enter" "$refresh" "$sort_ctrl" "$order_ctrl" "$exit")
|
||||||
total_len=$((total_len + $(_calc_len "${_segs[i]}")))
|
|
||||||
[[ $i -lt $((seg_count - 1)) ]] && total_len=$((total_len + 3))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Level 1: Remove "Space Select"
|
total_len=0
|
||||||
if [[ $total_len -gt $term_width ]]; then
|
seg_count=${#_segs[@]}
|
||||||
_segs=("$nav" "$enter" "$refresh" "$search" "$sort_ctrl" "$order_ctrl" "$exit")
|
for i in "${!_segs[@]}"; do
|
||||||
|
total_len=$((total_len + $(_calc_len "${_segs[i]}")))
|
||||||
|
[[ $i -lt $((seg_count - 1)) ]] && total_len=$((total_len + 3))
|
||||||
|
done
|
||||||
|
|
||||||
total_len=0
|
# Level 2: Remove sort label if still too wide
|
||||||
seg_count=${#_segs[@]}
|
if [[ $total_len -gt $term_width ]]; then
|
||||||
for i in "${!_segs[@]}"; do
|
_segs=("$nav" "$enter" "$refresh" "$order_ctrl" "$exit")
|
||||||
total_len=$((total_len + $(_calc_len "${_segs[i]}")))
|
|
||||||
[[ $i -lt $((seg_count - 1)) ]] && total_len=$((total_len + 3))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Level 2: Remove "S ${sort_status}"
|
|
||||||
if [[ $total_len -gt $term_width ]]; then
|
|
||||||
_segs=("$nav" "$enter" "$refresh" "$search" "$order_ctrl" "$exit")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
_print_wrapped_controls "$sep" "${_segs[@]}"
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
# Without metadata: basic controls
|
|
||||||
local -a _segs_simple=("$nav" "$space_select" "$enter" "$refresh" "$search" "$exit")
|
|
||||||
_print_wrapped_controls "$sep" "${_segs_simple[@]}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_print_wrapped_controls "$sep" "${_segs[@]}"
|
||||||
|
else
|
||||||
|
# Without metadata: basic controls
|
||||||
|
local -a _segs_simple=("$nav" "$space_select" "$enter" "$refresh" "$exit")
|
||||||
|
_print_wrapped_controls "$sep" "${_segs_simple[@]}"
|
||||||
fi
|
fi
|
||||||
printf "${clear_line}" >&2
|
printf "${clear_line}" >&2
|
||||||
}
|
}
|
||||||
@@ -592,16 +473,6 @@ paginated_multi_select() {
|
|||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
"QUIT")
|
"QUIT")
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_mode="false"
|
|
||||||
filter_query=""
|
|
||||||
applied_query=""
|
|
||||||
top_index=0
|
|
||||||
cursor_pos=0
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
cleanup
|
cleanup
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
@@ -759,13 +630,7 @@ paginated_multi_select() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"CHAR:s" | "CHAR:S")
|
"CHAR:s" | "CHAR:S")
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
if [[ "$has_metadata" == "true" ]]; then
|
||||||
local ch="${key#CHAR:}"
|
|
||||||
filter_query+="$ch"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
elif [[ "$has_metadata" == "true" ]]; then
|
|
||||||
# Cycle sort mode (only if metadata available)
|
# Cycle sort mode (only if metadata available)
|
||||||
case "$sort_mode" in
|
case "$sort_mode" in
|
||||||
date) sort_mode="name" ;;
|
date) sort_mode="name" ;;
|
||||||
@@ -776,135 +641,43 @@ paginated_multi_select() {
|
|||||||
need_full_redraw=true
|
need_full_redraw=true
|
||||||
fi
|
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
|
|
||||||
need_full_redraw=true
|
|
||||||
else
|
|
||||||
# Enter filter mode
|
|
||||||
filter_mode="true"
|
|
||||||
filter_query=""
|
|
||||||
top_index=0
|
|
||||||
cursor_pos=0
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"CHAR:j")
|
"CHAR:j")
|
||||||
if [[ "$filter_mode" != "true" ]]; then
|
# Down navigation (vim style)
|
||||||
# Down navigation
|
if [[ ${#view_indices[@]} -gt 0 ]]; then
|
||||||
if [[ ${#view_indices[@]} -gt 0 ]]; then
|
local absolute_index=$((top_index + cursor_pos))
|
||||||
local absolute_index=$((top_index + cursor_pos))
|
local last_index=$((${#view_indices[@]} - 1))
|
||||||
local last_index=$((${#view_indices[@]} - 1))
|
if [[ $absolute_index -lt $last_index ]]; then
|
||||||
if [[ $absolute_index -lt $last_index ]]; then
|
local visible_count=$((${#view_indices[@]} - top_index))
|
||||||
local visible_count=$((${#view_indices[@]} - top_index))
|
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
if [[ $cursor_pos -lt $((visible_count - 1)) ]]; then
|
||||||
if [[ $cursor_pos -lt $((visible_count - 1)) ]]; then
|
((cursor_pos++))
|
||||||
((cursor_pos++))
|
elif [[ $((top_index + visible_count)) -lt ${#view_indices[@]} ]]; then
|
||||||
elif [[ $((top_index + visible_count)) -lt ${#view_indices[@]} ]]; then
|
((top_index++))
|
||||||
((top_index++))
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
need_full_redraw=true
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
filter_query+="j"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"CHAR:k")
|
"CHAR:k")
|
||||||
if [[ "$filter_mode" != "true" ]]; then
|
# Up navigation (vim style)
|
||||||
# Up navigation
|
if [[ ${#view_indices[@]} -gt 0 ]]; then
|
||||||
if [[ ${#view_indices[@]} -gt 0 ]]; then
|
if [[ $cursor_pos -gt 0 ]]; then
|
||||||
if [[ $cursor_pos -gt 0 ]]; then
|
((cursor_pos--))
|
||||||
((cursor_pos--))
|
need_full_redraw=true
|
||||||
elif [[ $top_index -gt 0 ]]; then
|
elif [[ $top_index -gt 0 ]]; then
|
||||||
((top_index--))
|
((top_index--))
|
||||||
fi
|
need_full_redraw=true
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
filter_query+="k"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"TOUCHID")
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_query+="t"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"RIGHT")
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_query+="l"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"LEFT")
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_query+="h"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"MORE")
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_query+="m"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"UPDATE")
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_query+="u"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"CHAR:f" | "CHAR:F")
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
filter_query+="${key#CHAR:}"
|
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# F is currently unbound in normal mode to avoid conflict with Refresh (R)
|
|
||||||
;;
|
|
||||||
"CHAR:r" | "CHAR:R")
|
"CHAR:r" | "CHAR:R")
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
# Trigger Refresh signal
|
||||||
filter_query+="${key#CHAR:}"
|
cleanup
|
||||||
rebuild_view
|
return 10
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
# Trigger Refresh signal (Unified with Analyze)
|
|
||||||
cleanup
|
|
||||||
return 10
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
"CHAR:o" | "CHAR:O")
|
"CHAR:o" | "CHAR:O")
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
if [[ "$has_metadata" == "true" ]]; then
|
||||||
filter_query+="${key#CHAR:}"
|
# O toggles reverse order
|
||||||
rebuild_view
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
elif [[ "$has_metadata" == "true" ]]; then
|
|
||||||
# O toggles reverse order (Unified Sort Order)
|
|
||||||
if [[ "$sort_reverse" == "true" ]]; then
|
if [[ "$sort_reverse" == "true" ]]; then
|
||||||
sort_reverse="false"
|
sort_reverse="false"
|
||||||
else
|
else
|
||||||
@@ -914,40 +687,8 @@ paginated_multi_select() {
|
|||||||
need_full_redraw=true
|
need_full_redraw=true
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"DELETE")
|
|
||||||
# Backspace filter
|
|
||||||
if [[ "$filter_mode" == "true" && -n "$filter_query" ]]; then
|
|
||||||
filter_query="${filter_query%?}"
|
|
||||||
# Rebuild view to apply filter in real-time
|
|
||||||
rebuild_view
|
|
||||||
# Trigger redraw and continue to avoid drain_pending_input
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
CHAR:*)
|
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
|
||||||
local ch="${key#CHAR:}"
|
|
||||||
# avoid accidental leading spaces
|
|
||||||
if [[ -n "$filter_query" || "$ch" != " " ]]; then
|
|
||||||
filter_query+="$ch"
|
|
||||||
# Rebuild view to apply filter in real-time
|
|
||||||
rebuild_view
|
|
||||||
# Trigger redraw and continue to avoid drain_pending_input
|
|
||||||
need_full_redraw=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"ENTER")
|
"ENTER")
|
||||||
if [[ "$filter_mode" == "true" ]]; then
|
# Smart Enter behavior
|
||||||
applied_query="$filter_query"
|
|
||||||
filter_mode="false"
|
|
||||||
# Preserve cursor/top_index so navigation during search is respected
|
|
||||||
rebuild_view
|
|
||||||
# Fall through to confirmation logic
|
|
||||||
fi
|
|
||||||
# In normal mode: smart Enter behavior
|
|
||||||
# 1. Check if any items are already selected
|
# 1. Check if any items are already selected
|
||||||
local has_selection=false
|
local has_selection=false
|
||||||
for ((i = 0; i < total_items; i++)); do
|
for ((i = 0; i < total_items; i++)); do
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ remove_file_list() {
|
|||||||
batch_uninstall_applications() {
|
batch_uninstall_applications() {
|
||||||
local total_size_freed=0
|
local total_size_freed=0
|
||||||
|
|
||||||
|
# Trap to clean up spinner on interrupt
|
||||||
|
trap 'stop_inline_spinner 2>/dev/null; echo ""; return 130' INT TERM
|
||||||
|
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
if [[ ${#selected_apps[@]} -eq 0 ]]; then
|
if [[ ${#selected_apps[@]} -eq 0 ]]; then
|
||||||
log_warning "No applications selected for uninstallation"
|
log_warning "No applications selected for uninstallation"
|
||||||
@@ -181,33 +184,37 @@ batch_uninstall_applications() {
|
|||||||
local total_estimated_size=0
|
local total_estimated_size=0
|
||||||
local -a app_details=()
|
local -a app_details=()
|
||||||
|
|
||||||
|
# Cache current user outside loop
|
||||||
|
local current_user=$(whoami)
|
||||||
|
|
||||||
if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi
|
if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi
|
||||||
for selected_app in "${selected_apps[@]}"; do
|
for selected_app in "${selected_apps[@]}"; do
|
||||||
[[ -z "$selected_app" ]] && continue
|
[[ -z "$selected_app" ]] && continue
|
||||||
IFS='|' read -r _ app_path app_name bundle_id _ _ <<< "$selected_app"
|
IFS='|' read -r _ app_path app_name bundle_id _ _ <<< "$selected_app"
|
||||||
|
|
||||||
# Check running app by bundle executable if available.
|
# Check running app by bundle executable if available
|
||||||
local exec_name=""
|
local exec_name=""
|
||||||
if [[ -e "$app_path/Contents/Info.plist" ]]; then
|
local info_plist="$app_path/Contents/Info.plist"
|
||||||
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "")
|
if [[ -e "$info_plist" ]]; then
|
||||||
|
exec_name=$(defaults read "$info_plist" CFBundleExecutable 2>/dev/null || echo "")
|
||||||
fi
|
fi
|
||||||
local check_pattern="${exec_name:-$app_name}"
|
if pgrep -qx "${exec_name:-$app_name}" 2>/dev/null; then
|
||||||
if pgrep -x "$check_pattern" > /dev/null 2>&1; then
|
|
||||||
running_apps+=("$app_name")
|
running_apps+=("$app_name")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if it's a Homebrew cask (deterministic: resolved path in Caskroom)
|
# Check if it's a Homebrew cask (only if app is symlinked from Caskroom)
|
||||||
local cask_name=""
|
local cask_name="" is_brew_cask="false"
|
||||||
cask_name=$(get_brew_cask_name "$app_path" || echo "")
|
local resolved_path=$(readlink "$app_path" 2>/dev/null || echo "")
|
||||||
local is_brew_cask="false"
|
if [[ "$resolved_path" == */Caskroom/* ]]; then
|
||||||
[[ -n "$cask_name" ]] && is_brew_cask="true"
|
# Extract cask name using bash parameter expansion (faster than sed)
|
||||||
|
local tmp="${resolved_path#*/Caskroom/}"
|
||||||
|
cask_name="${tmp%%/*}"
|
||||||
|
[[ -n "$cask_name" ]] && is_brew_cask="true"
|
||||||
|
fi
|
||||||
|
|
||||||
# Full file scanning for ALL apps (including Homebrew casks)
|
# Check if sudo is needed
|
||||||
# brew uninstall --cask does NOT remove user data (caches, prefs, app support)
|
|
||||||
# Mole's value is cleaning those up, so we must scan for them
|
|
||||||
local needs_sudo=false
|
local needs_sudo=false
|
||||||
local app_owner=$(get_file_owner "$app_path")
|
local app_owner=$(get_file_owner "$app_path")
|
||||||
local current_user=$(whoami)
|
|
||||||
if [[ ! -w "$(dirname "$app_path")" ]] ||
|
if [[ ! -w "$(dirname "$app_path")" ]] ||
|
||||||
[[ "$app_owner" == "root" ]] ||
|
[[ "$app_owner" == "root" ]] ||
|
||||||
[[ -n "$app_owner" && "$app_owner" != "$current_user" ]]; then
|
[[ -n "$app_owner" && "$app_owner" != "$current_user" ]]; then
|
||||||
@@ -406,11 +413,12 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove the application only if not running.
|
# Remove the application only if not running.
|
||||||
|
# Stop spinner before any removal attempt (avoids mixed output on errors)
|
||||||
|
[[ -t 1 ]] && stop_inline_spinner
|
||||||
|
|
||||||
local used_brew_successfully=false
|
local used_brew_successfully=false
|
||||||
if [[ -z "$reason" ]]; then
|
if [[ -z "$reason" ]]; then
|
||||||
if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then
|
if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then
|
||||||
# Stop spinner before brew output
|
|
||||||
[[ -t 1 ]] && stop_inline_spinner
|
|
||||||
# Use brew_uninstall_cask helper (handles env vars, timeout, verification)
|
# Use brew_uninstall_cask helper (handles env vars, timeout, verification)
|
||||||
if brew_uninstall_cask "$cask_name" "$app_path"; then
|
if brew_uninstall_cask "$cask_name" "$app_path"; then
|
||||||
used_brew_successfully=true
|
used_brew_successfully=true
|
||||||
@@ -425,7 +433,6 @@ batch_uninstall_applications() {
|
|||||||
elif [[ "$needs_sudo" == true ]]; then
|
elif [[ "$needs_sudo" == true ]]; then
|
||||||
if ! safe_sudo_remove "$app_path"; then
|
if ! safe_sudo_remove "$app_path"; then
|
||||||
local app_owner=$(get_file_owner "$app_path")
|
local app_owner=$(get_file_owner "$app_path")
|
||||||
local current_user=$(whoami)
|
|
||||||
if [[ -n "$app_owner" && "$app_owner" != "$current_user" && "$app_owner" != "root" ]]; then
|
if [[ -n "$app_owner" && "$app_owner" != "$current_user" && "$app_owner" != "root" ]]; then
|
||||||
reason="owned by $app_owner"
|
reason="owned by $app_owner"
|
||||||
else
|
else
|
||||||
@@ -460,13 +467,12 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Stop spinner and show success
|
# Show success
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
stop_inline_spinner
|
|
||||||
if [[ ${#app_details[@]} -gt 1 ]]; then
|
if [[ ${#app_details[@]} -gt 1 ]]; then
|
||||||
echo -e "\r\033[K${GREEN}✓${NC} [$current_index/${#app_details[@]}] ${app_name}"
|
echo -e "${GREEN}✓${NC} [$current_index/${#app_details[@]}] ${app_name}"
|
||||||
else
|
else
|
||||||
echo -e "\r\033[K${GREEN}✓${NC} ${app_name}"
|
echo -e "${GREEN}✓${NC} ${app_name}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -477,13 +483,12 @@ batch_uninstall_applications() {
|
|||||||
((total_items++))
|
((total_items++))
|
||||||
success_items+=("$app_name")
|
success_items+=("$app_name")
|
||||||
else
|
else
|
||||||
# Stop spinner and show failure
|
# Show failure
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
stop_inline_spinner
|
|
||||||
if [[ ${#app_details[@]} -gt 1 ]]; then
|
if [[ ${#app_details[@]} -gt 1 ]]; then
|
||||||
echo -e "\r\033[K${RED}✗${NC} [$current_index/${#app_details[@]}] ${app_name} ${GRAY}($reason)${NC}"
|
echo -e "${ICON_ERROR} [$current_index/${#app_details[@]}] ${app_name} ${GRAY}($reason)${NC}"
|
||||||
else
|
else
|
||||||
echo -e "\r\033[K${RED}✗${NC} ${app_name} failed: $reason"
|
echo -e "${ICON_ERROR} ${app_name} failed: $reason"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user