1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-13 01:00:13 +00:00

Uninstall function detailed upgrade

This commit is contained in:
Tw93
2025-10-11 11:40:01 +08:00
parent b55915490e
commit 65e3585f95
6 changed files with 352 additions and 68 deletions

View File

@@ -108,15 +108,14 @@ scan_applications() {
find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' ' find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' '
) )
# Check if cache is valid # Check if cache is valid unless explicitly disabled
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0") local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
# Cache is valid if: age < TTL AND app count matches # Cache is valid if: age < TTL AND app count matches
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
# Only show cache info in debug mode # Silent - cache hit, no need to show progress
[[ -n "${MOLE_DEBUG:-}" ]] && echo "Using cached app list (${cache_age}s old, $current_app_count apps) ✓" >&2
echo "$cache_file" echo "$cache_file"
return 0 return 0
fi fi
@@ -124,7 +123,6 @@ scan_applications() {
local temp_file=$(create_temp_file) local temp_file=$(create_temp_file)
echo "" >&2 # Add space before scanning output without breaking stdout return
# Pre-cache current epoch to avoid repeated calls # Pre-cache current epoch to avoid repeated calls
local current_epoch=$(date "+%s") local current_epoch=$(date "+%s")
@@ -219,6 +217,11 @@ scan_applications() {
local total_apps=${#app_data_tuples[@]} local total_apps=${#app_data_tuples[@]}
local max_parallel=10 # Process 10 apps in parallel local max_parallel=10 # Process 10 apps in parallel
local pids=() local pids=()
local inline_loading=false
if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then
inline_loading=true
printf "\033[H" >&2 # Position cursor at top of screen
fi
# Process app metadata extraction function # Process app metadata extraction function
process_app_metadata() { process_app_metadata() {
@@ -296,7 +299,11 @@ scan_applications() {
# Update progress with spinner # Update progress with spinner
local spinner_char="${spinner_chars:$((spinner_idx % 4)):1}" local spinner_char="${spinner_chars:$((spinner_idx % 4)):1}"
echo -ne "\r\033[K ${spinner_char} Scanning applications... $app_count/$total_apps" >&2 if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K${spinner_char} Scanning applications... %d/%d" "$app_count" "$total_apps" >&2
else
echo -ne "\r\033[K${spinner_char} Scanning applications... $app_count/$total_apps" >&2
fi
((spinner_idx++)) ((spinner_idx++))
# Wait if we've hit max parallel limit # Wait if we've hit max parallel limit
@@ -311,15 +318,22 @@ scan_applications() {
wait "$pid" 2>/dev/null wait "$pid" 2>/dev/null
done done
echo -e "\r\033[K ✓ Found $app_count applications" >&2
echo "" >&2
# Check if we found any applications # Check if we found any applications
if [[ ! -s "$temp_file" ]]; then if [[ ! -s "$temp_file" ]]; then
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K" >&2
else
echo -ne "\r\033[K" >&2
fi
echo "No applications found to uninstall" >&2
rm -f "$temp_file" rm -f "$temp_file"
return 1 return 1
fi fi
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K" >&2
fi
# Sort by last used (oldest first) and cache the result # Sort by last used (oldest first) and cache the result
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file"; return 1; } sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file"; return 1; }
rm -f "$temp_file" rm -f "$temp_file"
@@ -388,13 +402,13 @@ uninstall_applications() {
echo "" echo ""
# Check if app is running # Check if app is running (use app path for precise matching)
if pgrep -f "$app_name" >/dev/null 2>&1; then if pgrep -f "$app_path" >/dev/null 2>&1; then
echo -e "${YELLOW}$app_name is currently running${NC}" echo -e "${YELLOW}$app_name is currently running${NC}"
read -p " Force quit $app_name? (y/N): " -n 1 -r read -p " Force quit $app_name? (y/N): " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
pkill -f "$app_name" 2>/dev/null || true pkill -f "$app_path" 2>/dev/null || true
sleep 2 sleep 2
else else
echo -e " ${BLUE}${NC} Skipped $app_name" echo -e " ${BLUE}${NC} Skipped $app_name"
@@ -509,6 +523,10 @@ uninstall_applications() {
# Cleanup function - restore cursor and clean up # Cleanup function - restore cursor and clean up
cleanup() { cleanup() {
# Restore cursor using common function # Restore cursor using common function
if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then
leave_alt_screen
unset MOLE_ALT_SCREEN_ACTIVE
fi
show_cursor show_cursor
exit "${1:-0}" exit "${1:-0}"
} }
@@ -518,30 +536,78 @@ trap cleanup EXIT INT TERM
# Main function # Main function
main() { main() {
local use_inline_loading=false
if [[ -t 1 && -t 2 ]]; then
use_inline_loading=true
fi
# Hide cursor during operation # Hide cursor during operation
hide_cursor hide_cursor
if [[ $use_inline_loading == true ]]; then
enter_alt_screen
export MOLE_ALT_SCREEN_ACTIVE=1
export MOLE_INLINE_LOADING=1
export MOLE_MANAGED_ALT_SCREEN=1
printf "\033[2J\033[H" >&2
else
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN MOLE_ALT_SCREEN_ACTIVE
fi
# Scan applications # Scan applications
local apps_file=$(scan_applications) local apps_file=""
if ! apps_file=$(scan_applications); then
if [[ $use_inline_loading == true ]]; then
printf "\033[2J\033[H" >&2
leave_alt_screen
unset MOLE_ALT_SCREEN_ACTIVE
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
fi
return 1
fi
if [[ $use_inline_loading == true ]]; then
printf "\033[2J\033[H" >&2
fi
if [[ ! -f "$apps_file" ]]; then if [[ ! -f "$apps_file" ]]; then
echo "" # Error message already shown by scan_applications
log_error "Failed to scan applications" if [[ $use_inline_loading == true ]]; then
leave_alt_screen
unset MOLE_ALT_SCREEN_ACTIVE
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
fi
return 1 return 1
fi fi
# Load applications # Load applications
if ! load_applications "$apps_file"; then if ! load_applications "$apps_file"; then
if [[ $use_inline_loading == true ]]; then
leave_alt_screen
unset MOLE_ALT_SCREEN_ACTIVE
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
fi
rm -f "$apps_file" rm -f "$apps_file"
return 1 return 1
fi fi
# Interactive selection using paginated menu # Interactive selection using paginated menu
if ! select_apps_for_uninstall; then if ! select_apps_for_uninstall; then
if [[ $use_inline_loading == true ]]; then
leave_alt_screen
unset MOLE_ALT_SCREEN_ACTIVE
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
fi
rm -f "$apps_file" rm -f "$apps_file"
return 0 return 0
fi fi
if [[ $use_inline_loading == true ]]; then
leave_alt_screen
unset MOLE_ALT_SCREEN_ACTIVE
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
fi
# Restore cursor and show a concise summary before confirmation # Restore cursor and show a concise summary before confirmation
show_cursor show_cursor
clear clear

View File

@@ -37,10 +37,8 @@ select_apps_for_uninstall() {
menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")") menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")")
done done
# Clear screen before menu (alternate screen preserves main screen)
clear_screen
# Use paginated menu - result will be stored in MOLE_SELECTION_RESULT # Use paginated menu - result will be stored in MOLE_SELECTION_RESULT
# Note: paginated_multi_select enters alternate screen and handles clearing
MOLE_SELECTION_RESULT="" MOLE_SELECTION_RESULT=""
paginated_multi_select "Select Apps to Remove" "${menu_options[@]}" paginated_multi_select "Select Apps to Remove" "${menu_options[@]}"
local exit_code=$? local exit_code=$?

View File

@@ -24,6 +24,7 @@ batch_uninstall_applications() {
local -a sudo_apps=() local -a sudo_apps=()
local total_estimated_size=0 local total_estimated_size=0
local -a app_details=() local -a app_details=()
local -a dock_cleanup_paths=()
echo "" echo ""
# Silent analysis without spinner output (avoid visual flicker) # Silent analysis without spinner output (avoid visual flicker)
@@ -31,8 +32,8 @@ batch_uninstall_applications() {
[[ -z "$selected_app" ]] && continue [[ -z "$selected_app" ]] && continue
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
# Check if app is running # Check if app is running (use app path for precise matching)
if pgrep -f "$app_name" >/dev/null 2>&1; then if pgrep -f "$app_path" >/dev/null 2>&1; then
running_apps+=("$app_name") running_apps+=("$app_name")
fi fi
@@ -49,14 +50,46 @@ batch_uninstall_applications() {
((total_estimated_size += total_kb)) ((total_estimated_size += total_kb))
# Store details for later use # Store details for later use
# Base64 encode related_files to handle multi-line data safely # Base64 encode related_files to handle multi-line data safely (single line)
local encoded_files=$(echo "$related_files" | base64) local encoded_files
encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n')
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files") app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files")
done done
# Format size display (convert KB to bytes for bytes_to_human()) # Format size display (convert KB to bytes for bytes_to_human())
local size_display=$(bytes_to_human "$((total_estimated_size * 1024))") local size_display=$(bytes_to_human "$((total_estimated_size * 1024))")
# Display detailed file list for each app before confirmation
echo -e "${PURPLE}Files to be removed:${NC}"
echo ""
for detail in "${app_details[@]}"; do
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
local related_files=$(printf '%s' "$encoded_files" | base64 -d)
local app_size_display=$(bytes_to_human "$((total_kb * 1024))")
echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name} ${GRAY}(${app_size_display})${NC}"
echo -e " ${GREEN}${NC} $(echo "$app_path" | sed "s|$HOME|~|")"
# Show related files (limit to 5 most important ones for brevity)
local file_count=0
local max_files=5
while IFS= read -r file; do
if [[ -n "$file" && -e "$file" ]]; then
if [[ $file_count -lt $max_files ]]; then
echo -e " ${GREEN}${NC} $(echo "$file" | sed "s|$HOME|~|")"
fi
((file_count++))
fi
done <<< "$related_files"
# Show count of remaining files if truncated
if [[ $file_count -gt $max_files ]]; then
local remaining=$((file_count - max_files))
echo -e " ${GRAY} ... and ${remaining} more files${NC}"
fi
echo ""
done
# Show summary and get batch confirmation first (before asking for password) # Show summary and get batch confirmation first (before asking for password)
local app_total=${#selected_apps[@]} local app_total=${#selected_apps[@]}
local app_text="app" local app_text="app"
@@ -100,12 +133,7 @@ batch_uninstall_applications() {
if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi
# Force quit running apps first (batch) # Force quit running apps first (batch)
if [[ ${#running_apps[@]} -gt 0 ]]; then # Note: Apps are already killed in the individual uninstall loop below with app_path for precise matching
pkill -f "${running_apps[0]}" 2>/dev/null || true
for app_name in "${running_apps[@]:1}"; do pkill -f "$app_name" 2>/dev/null || true; done
sleep 2
if pgrep -f "${running_apps[0]}" >/dev/null 2>&1; then sleep 1; fi
fi
# Perform uninstallations (silent mode, show results at end) # Perform uninstallations (silent mode, show results at end)
if [[ -t 1 ]]; then stop_inline_spinner; fi if [[ -t 1 ]]; then stop_inline_spinner; fi
@@ -114,11 +142,11 @@ batch_uninstall_applications() {
local -a success_items=() local -a success_items=()
for detail in "${app_details[@]}"; do for detail in "${app_details[@]}"; do
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail" IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
local related_files=$(echo "$encoded_files" | base64 -d) local related_files=$(printf '%s' "$encoded_files" | base64 -d)
local reason="" local reason=""
local needs_sudo=false local needs_sudo=false
[[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]] && needs_sudo=true [[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]] && needs_sudo=true
if ! force_kill_app "$app_name"; then if ! force_kill_app "$app_name" "$app_path"; then
reason="still running" reason="still running"
fi fi
if [[ -z "$reason" ]]; then if [[ -z "$reason" ]]; then
@@ -139,6 +167,7 @@ batch_uninstall_applications() {
((files_cleaned++)) ((files_cleaned++))
((total_items++)) ((total_items++))
success_items+=("$app_name") success_items+=("$app_name")
dock_cleanup_paths+=("$app_path")
else else
((failed_count++)) ((failed_count++))
failed_items+=("$app_name:$reason") failed_items+=("$app_name:$reason")
@@ -148,7 +177,6 @@ batch_uninstall_applications() {
# Summary # Summary
local freed_display=$(bytes_to_human "$((total_size_freed * 1024))") local freed_display=$(bytes_to_human "$((total_size_freed * 1024))")
local bar="================================================================================" local bar="================================================================================"
echo ""
echo "$bar" echo "$bar"
if [[ $success_count -gt 0 ]]; then if [[ $success_count -gt 0 ]]; then
local success_list="${success_items[*]}" local success_list="${success_items[*]}"
@@ -179,6 +207,10 @@ batch_uninstall_applications() {
fi fi
echo "$bar" echo "$bar"
if [[ ${#dock_cleanup_paths[@]} -gt 0 ]]; then
remove_apps_from_dock "${dock_cleanup_paths[@]}"
fi
# Clean up sudo keepalive if it was started # Clean up sudo keepalive if it was started
if [[ -n "${sudo_keepalive_pid:-}" ]]; then if [[ -n "${sudo_keepalive_pid:-}" ]]; then
kill "$sudo_keepalive_pid" 2>/dev/null || true kill "$sudo_keepalive_pid" 2>/dev/null || true

View File

@@ -268,14 +268,38 @@ bytes_to_human() {
fi fi
if ((bytes >= 1073741824)); then # >= 1GB if ((bytes >= 1073741824)); then # >= 1GB
echo "$bytes" | awk '{printf "%.2fGB", $1/1073741824}' local divisor=1073741824
elif ((bytes >= 1048576)); then # >= 1MB local whole=$((bytes / divisor))
echo "$bytes" | awk '{printf "%.1fMB", $1/1048576}' local remainder=$((bytes % divisor))
elif ((bytes >= 1024)); then # >= 1KB local frac=$(( (remainder * 100 + divisor / 2) / divisor )) # Two decimals, rounded
echo "$bytes" | awk '{printf "%.0fKB", $1/1024}' if ((frac >= 100)); then
else frac=0
echo "${bytes}B" ((whole++))
fi
printf "%d.%02dGB\n" "$whole" "$frac"
return 0
fi fi
if ((bytes >= 1048576)); then # >= 1MB
local divisor=1048576
local whole=$((bytes / divisor))
local remainder=$((bytes % divisor))
local frac=$(( (remainder * 10 + divisor / 2) / divisor )) # One decimal, rounded
if ((frac >= 10)); then
frac=0
((whole++))
fi
printf "%d.%01dMB\n" "$whole" "$frac"
return 0
fi
if ((bytes >= 1024)); then # >= 1KB
local rounded_kb=$(((bytes + 512) / 1024)) # Nearest integer KB
printf "%dKB\n" "$rounded_kb"
return 0
fi
printf "%dB\n" "$bytes"
} }
# Calculate directory size in bytes # Calculate directory size in bytes
@@ -726,17 +750,130 @@ mktemp_dir() { local d; d=$(mktemp -d) || return 1; register_temp_dir "$d"; ech
# Uninstall helper abstractions # Uninstall helper abstractions
# ========================================================================= # =========================================================================
force_kill_app() { force_kill_app() {
# Args: app_name; tries graceful then force kill; returns 0 if stopped, 1 otherwise # Args: app_name [app_path]; tries graceful then force kill; returns 0 if stopped, 1 otherwise
local app="$1" local app_name="$1"
if pgrep -f "$app" >/dev/null 2>&1; then local app_path="${2:-}"
pkill -f "$app" 2>/dev/null || true
# Use app path for precise matching if provided
local match_pattern="$app_name"
if [[ -n "$app_path" && -e "$app_path" ]]; then
# Use the app bundle path for more precise matching
match_pattern="$app_path"
fi
if pgrep -f "$match_pattern" >/dev/null 2>&1; then
pkill -f "$match_pattern" 2>/dev/null || true
sleep 1 sleep 1
fi fi
if pgrep -f "$app" >/dev/null 2>&1; then if pgrep -f "$match_pattern" >/dev/null 2>&1; then
pkill -9 -f "$app" 2>/dev/null || true pkill -9 -f "$match_pattern" 2>/dev/null || true
sleep 1 sleep 1
fi fi
pgrep -f "$app" >/dev/null 2>&1 && return 1 || return 0 pgrep -f "$match_pattern" >/dev/null 2>&1 && return 1 || return 0
}
# Remove application icons from the Dock (best effort)
remove_apps_from_dock() {
if [[ $# -eq 0 ]]; then
return 0
fi
local plist="$HOME/Library/Preferences/com.apple.dock.plist"
[[ -f "$plist" ]] || return 0
if ! command -v python3 >/dev/null 2>&1; then
return 0
fi
# Execute Python helper to prune dock entries for the given app paths.
# Exit status 2 means entries were removed.
local target_count=$#
python3 - "$@" <<'PY'
import os
import plistlib
import subprocess
import sys
import urllib.parse
plist_path = os.path.expanduser('~/Library/Preferences/com.apple.dock.plist')
if not os.path.exists(plist_path):
sys.exit(0)
def normalise(path):
if not path:
return ''
return os.path.normpath(os.path.realpath(path.rstrip('/')))
targets = {normalise(arg) for arg in sys.argv[1:] if arg}
targets = {t for t in targets if t}
if not targets:
sys.exit(0)
with open(plist_path, 'rb') as fh:
try:
data = plistlib.load(fh)
except Exception:
sys.exit(0)
apps = data.get('persistent-apps')
if not isinstance(apps, list):
sys.exit(0)
changed = False
filtered = []
for item in apps:
try:
url = item['tile-data']['file-data']['_CFURLString']
except (KeyError, TypeError):
filtered.append(item)
continue
if not isinstance(url, str):
filtered.append(item)
continue
parsed = urllib.parse.urlparse(url)
path = urllib.parse.unquote(parsed.path or '')
if not path:
filtered.append(item)
continue
candidate = normalise(path)
if any(candidate == t or candidate.startswith(t + os.sep) for t in targets):
changed = True
continue
filtered.append(item)
if not changed:
sys.exit(0)
data['persistent-apps'] = filtered
with open(plist_path, 'wb') as fh:
try:
plistlib.dump(data, fh, fmt=plistlib.FMT_BINARY)
except Exception:
plistlib.dump(data, fh)
# Restart Dock to apply changes (ignore errors)
try:
subprocess.run(['killall', 'Dock'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
except Exception:
pass
sys.exit(2)
PY
local python_status=$?
if [[ $python_status -eq 2 ]]; then
if [[ $target_count -gt 1 ]]; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icons from Dock"
else
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icon from Dock"
fi
return 0
fi
return $python_status
} }
map_uninstall_reason() { map_uninstall_reason() {

View File

@@ -12,6 +12,10 @@ paginated_multi_select() {
local title="$1" local title="$1"
shift shift
local -a items=("$@") 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 # Validation
if [[ ${#items[@]} -eq 0 ]]; then if [[ ${#items[@]} -eq 0 ]]; then
@@ -55,11 +59,14 @@ paginated_multi_select() {
else else
stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true
fi fi
leave_alt_screen if [[ "${external_alt_screen:-false}" == false ]]; then
leave_alt_screen
fi
} }
# Cleanup function # Cleanup function
cleanup() { cleanup() {
trap - EXIT INT TERM
restore_terminal restore_terminal
} }
@@ -74,9 +81,13 @@ paginated_multi_select() {
# Setup terminal - preserve interrupt character # Setup terminal - preserve interrupt character
stty -echo -icanon intr ^C 2>/dev/null || true stty -echo -icanon intr ^C 2>/dev/null || true
enter_alt_screen if [[ $external_alt_screen == false ]]; then
# Clear screen once on entry to alt screen enter_alt_screen
printf "\033[2J\033[H" >&2 # Clear screen once on entry to alt screen
printf "\033[2J\033[H" >&2
else
printf "\033[H" >&2
fi
hide_cursor hide_cursor
# Helper functions # Helper functions
@@ -84,11 +95,11 @@ paginated_multi_select() {
render_item() { render_item() {
local idx=$1 is_current=$2 local idx=$1 is_current=$2
local checkbox="" local checkbox="[ ]"
[[ ${selected[idx]} == true ]] && checkbox="" [[ ${selected[idx]} == true ]] && checkbox="[x]"
if [[ $is_current == true ]]; then if [[ $is_current == true ]]; then
printf "\r\033[2K\033[7m▶ %s %s\033[0m\n" "$checkbox" "${items[idx]}" >&2 printf "\r\033[2K${BLUE}> %s %s${NC}\n" "$checkbox" "${items[idx]}" >&2
else else
printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2 printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2
fi fi
@@ -168,7 +179,10 @@ EOF
local key=$(read_key) local key=$(read_key)
case "$key" in case "$key" in
"QUIT") cleanup; return 1 ;; "QUIT")
cleanup
return 1
;;
"UP") "UP")
if [[ $cursor_pos -gt 0 ]]; then if [[ $cursor_pos -gt 0 ]]; then
((cursor_pos--)) ((cursor_pos--))

69
mole
View File

@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh" source "$SCRIPT_DIR/lib/common.sh"
# Version info # Version info
VERSION="1.7.3" VERSION="1.7.4"
MOLE_TAGLINE="can dig deep to clean your Mac." MOLE_TAGLINE="can dig deep to clean your Mac."
# Get latest version from remote repository # Get latest version from remote repository
@@ -400,28 +400,53 @@ remove_mole() {
exit 0 exit 0
} }
# Display main menu options # Display main menu options with minimal refresh to avoid flicker
show_main_menu() { show_main_menu() {
local selected="${1:-1}" local selected="${1:-1}"
local full_draw="${2:-true}" local _full_draw="${2:-true}" # Kept for compatibility (unused)
local banner="${MAIN_MENU_BANNER:-}"
local update_message="${MAIN_MENU_UPDATE_MESSAGE:-}"
# Full redraw each time (prevents ghost menu items) # Fallback if globals missing (should not happen)
clear_screen if [[ -z "$banner" ]]; then
echo "" banner="$(show_brand_banner)"
show_brand_banner MAIN_MENU_BANNER="$banner"
show_update_notification fi
echo ""
show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)" printf '\033[H' # Move cursor to home
show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)"
show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)" local line=""
show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)" # Leading spacer
show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)" printf '\r\033[2K\n'
# Brand banner
while IFS= read -r line || [[ -n "$line" ]]; do
printf '\r\033[2K%s\n' "$line"
done <<< "$banner"
# Update notification block (if present)
if [[ -n "$update_message" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
printf '\r\033[2K%s\n' "$line"
done <<< "$update_message"
fi
# Spacer before menu options
printf '\r\033[2K\n'
printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)")"
if [[ -t 0 ]]; then if [[ -t 0 ]]; then
echo "" printf '\r\033[2K\n'
echo -e " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit" printf '\r\033[2K%s\n' " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit"
fi fi
# Clear any remaining content below without full screen wipe
printf '\033[J'
} }
# Interactive main menu loop # Interactive main menu loop
@@ -439,6 +464,18 @@ interactive_main_menu() {
fi fi
local current_option=1 local current_option=1
local first_draw=true local first_draw=true
local brand_banner=""
local msg_cache="$HOME/.cache/mole/update_message"
local update_message=""
brand_banner="$(show_brand_banner)"
MAIN_MENU_BANNER="$brand_banner"
if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then
update_message="$(cat "$msg_cache" 2>/dev/null || echo "")"
fi
MAIN_MENU_UPDATE_MESSAGE="$update_message"
cleanup_and_exit() { cleanup_and_exit() {
show_cursor show_cursor
echo "" echo ""