From 640499d302a5a593295b4c86a5e3ed22f9b46698 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 17 Oct 2025 21:19:05 +0800 Subject: [PATCH] Greatly improve scanning speed --- bin/analyze.sh | 26 ++++++++---- bin/uninstall.sh | 96 ++++++++++++++++++++++++++++++++++-------- lib/common.sh | 83 ++++++++++++++++++++++++++++++------ lib/ui_app_selector.sh | 32 ++++++++++++-- 4 files changed, 195 insertions(+), 42 deletions(-) diff --git a/bin/analyze.sh b/bin/analyze.sh index cd2cdf2..23807cb 100755 --- a/bin/analyze.sh +++ b/bin/analyze.sh @@ -19,7 +19,7 @@ source "$LIB_DIR/common.sh" # Constants readonly CACHE_DIR="${HOME}/.config/mole/cache" readonly TEMP_PREFIX="/tmp/mole_analyze_$$" -readonly MIN_LARGE_FILE_SIZE="1000000000" # 1GB +readonly MIN_LARGE_FILE_SIZE="500000000" # 500MB (more sensitive) readonly MIN_MEDIUM_FILE_SIZE="100000000" # 100MB # Emoji badges for list displays only @@ -105,9 +105,9 @@ scan_directories() { # Check if we can use parallel processing if command -v xargs &> /dev/null && [[ $depth -eq 1 ]]; then - # Fast parallel scan for depth 1 + # Fast parallel scan for depth 1 (increased parallelism) find "$target_path" -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null | - xargs -0 -P 4 -I {} du -sk {} 2> /dev/null | + xargs -0 -P 8 -I {} du -sk {} 2> /dev/null | sort -rn | while IFS=$'\t' read -r size path; do echo "$((size * 1024))|$path" @@ -1375,9 +1375,12 @@ scan_directory_contents_fast() { # Auto-detect optimal parallel jobs using common function local num_jobs num_jobs=$(get_optimal_parallel_jobs "io") - # Cap at reasonable limits for I/O operations - [[ $num_jobs -gt 24 ]] && num_jobs=24 - [[ $num_jobs -lt 12 ]] && num_jobs=12 + # Keep within practical limits to avoid IO thrashing on small machines + if [[ $num_jobs -gt 32 ]]; then + num_jobs=32 + elif [[ $num_jobs -lt 4 ]]; then + num_jobs=4 + fi local temp_dirs="$output_file.dirs" local temp_files="$output_file.files" @@ -1448,7 +1451,7 @@ scan_directory_contents_fast() { fi [[ ${#spinner[@]} -eq 0 ]] && spinner=('|' '/' '-' '\\') local i=0 - local max_wait=30 # Reduced to 30 seconds (fast fail) + local max_wait=45 # Balanced timeout (fast but not too aggressive) local elapsed=0 local tick=0 local spin_len=${#spinner[@]} @@ -1563,12 +1566,19 @@ show_volumes_overview() { [[ -d "$HOME/Library" ]] && echo "700|$HOME/Library|User Library" [[ -d "/Library" ]] && echo "600|/Library|System Library" - # External volumes (if any) + # External volumes (filter obvious system mounts) if [[ -d "/Volumes" ]]; then local vol_priority=500 find /Volumes -mindepth 1 -maxdepth 1 -type d 2> /dev/null | while IFS= read -r vol; do local vol_name vol_name=$(basename "$vol") + + # Skip internal/system volumes and dmg helper mounts, but keep user disks + case "$vol_name" in + "MacintoshHD" | "Macintosh HD" | "Macintosh HD - Data") continue ;; + dmg.* | *.dmg) continue ;; + esac + echo "$((vol_priority))|$vol|Volume: $vol_name" ((vol_priority--)) done diff --git a/bin/uninstall.sh b/bin/uninstall.sh index c28ec45..4b346e8 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -60,6 +60,44 @@ get_app_last_used() { fi } +# Compact the "last used" descriptor for aligned summaries +format_last_used_summary() { + local value="$1" + + case "$value" in + "" | "Unknown") + echo "Unknown" + return 0 + ;; + "Never" | "Recent" | "Today" | "Yesterday" | "This year" | "Old") + echo "$value" + return 0 + ;; + esac + + if [[ $value =~ ^([0-9]+)[[:space:]]+days?\ ago$ ]]; then + echo "${BASH_REMATCH[1]}d ago" + return 0 + fi + if [[ $value =~ ^([0-9]+)[[:space:]]+weeks?\ ago$ ]]; then + echo "${BASH_REMATCH[1]}w ago" + return 0 + fi + if [[ $value =~ ^([0-9]+)[[:space:]]+months?\ ago$ ]]; then + echo "${BASH_REMATCH[1]}m ago" + return 0 + fi + if [[ $value =~ ^([0-9]+)[[:space:]]+month\(s\)\ ago$ ]]; then + echo "${BASH_REMATCH[1]}m ago" + return 0 + fi + if [[ $value =~ ^([0-9]+)[[:space:]]+years?\ ago$ ]]; then + echo "${BASH_REMATCH[1]}y ago" + return 0 + fi + echo "$value" +} + # Scan applications and collect information scan_applications() { # Cache configuration @@ -195,7 +233,14 @@ scan_applications() { # Second pass: process each app with parallel size calculation local app_count=0 local total_apps=${#app_data_tuples[@]} - local max_parallel=10 # Process 10 apps in parallel + # Bound parallelism so small machines stay responsive + local max_parallel + max_parallel=$(get_optimal_parallel_jobs "io") + if [[ $max_parallel -lt 4 ]]; then + max_parallel=4 + elif [[ $max_parallel -gt 16 ]]; then + max_parallel=16 + fi local pids=() local inline_loading=false if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then @@ -215,9 +260,9 @@ scan_applications() { local app_size="N/A" local app_size_kb="0" if [[ -d "$app_path" ]]; then - # numeric size (KB) for sorting + human-readable for display + # Get size in KB, then format for display (single du call) app_size_kb=$(du -sk "$app_path" 2> /dev/null | awk '{print $1}' || echo "0") - app_size=$(du -sh "$app_path" 2> /dev/null | cut -f1 || echo "N/A") + app_size=$(bytes_to_human "$((app_size_kb * 1024))") fi # Get real last used date from macOS metadata @@ -633,26 +678,43 @@ main() { rm -f "$apps_file" return 0 fi - # Show selected apps, max 3 per line + # Show selected apps with clean alignment echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):" - local idx=0 - local line="" + local -a summary_rows=() + local max_name_width=0 + local max_size_width=0 + local name_trunc_limit=30 + for selected_app in "${selected_apps[@]}"; do IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" - local display_item="${app_name}(${size})" - if ((idx % 3 == 0)); then - # Start new line - [[ -n "$line" ]] && echo " $line" - line="$display_item" - else - # Add to current line - line="$line, $display_item" + local display_name="$app_name" + if [[ ${#display_name} -gt $name_trunc_limit ]]; then + display_name="${display_name:0:$((name_trunc_limit - 3))}..." fi - ((idx++)) + [[ ${#display_name} -gt $max_name_width ]] && max_name_width=${#display_name} + + local size_display="$size" + if [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]]; then + size_display="Unknown" + fi + [[ ${#size_display} -gt $max_size_width ]] && max_size_width=${#size_display} + + local last_display + last_display=$(format_last_used_summary "$last_used") + + summary_rows+=("$display_name|$size_display|$last_display") + done + + ((max_name_width < 16)) && max_name_width=16 + ((max_size_width < 5)) && max_size_width=5 + + local index=1 + for row in "${summary_rows[@]}"; do + IFS='|' read -r name_cell size_cell last_cell <<< "$row" + printf " %2d. %-*s %*s | Last: %s\n" "$index" "$max_name_width" "$name_cell" "$max_size_width" "$size_cell" "$last_cell" + ((index++)) done - # Print the last line - [[ -n "$line" ]] && echo " $line" echo "" # Execute batch uninstallation (handles confirmation) diff --git a/lib/common.sh b/lib/common.sh index f75ed9c..89c087d 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -217,7 +217,7 @@ read_key() { case "$key" in $'\n' | $'\r') echo "ENTER" ;; ' ') echo "SPACE" ;; - 'Q') echo "QUIT" ;; + 'q' | 'Q') echo "QUIT" ;; 'R') echo "RETRY" ;; 'o' | 'O') echo "OPEN" ;; '/') echo "FILTER" ;; # Trigger filter mode @@ -1425,13 +1425,27 @@ readonly DATA_PROTECTED_BUNDLES=( # Legacy function - preserved for backward compatibility # Use should_protect_from_uninstall() or should_protect_data() instead readonly PRESERVED_BUNDLE_PATTERNS=("${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}") + +# Check whether a bundle ID matches a pattern (supports globs) +bundle_matches_pattern() { + local bundle_id="$1" + local pattern="$2" + + [[ -z "$pattern" ]] && return 1 + + # shellcheck disable=SC2254 # allow glob pattern matching for bundle rules + case "$bundle_id" in + $pattern) return 0 ;; + esac + return 1 +} + should_preserve_bundle() { local bundle_id="$1" for pattern in "${PRESERVED_BUNDLE_PATTERNS[@]}"; do - # Use case for safer glob matching - case "$bundle_id" in - "$pattern") return 0 ;; - esac + if bundle_matches_pattern "$bundle_id" "$pattern"; then + return 0 + fi done return 1 } @@ -1440,10 +1454,9 @@ should_preserve_bundle() { should_protect_from_uninstall() { local bundle_id="$1" for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do - # Use case for safer glob matching - case "$bundle_id" in - "$pattern") return 0 ;; - esac + if bundle_matches_pattern "$bundle_id" "$pattern"; then + return 0 + fi done return 1 } @@ -1453,10 +1466,9 @@ should_protect_data() { local bundle_id="$1" # Protect both system critical and data protected bundles during cleanup for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do - # Use case for safer glob matching - case "$bundle_id" in - "$pattern") return 0 ;; - esac + if bundle_matches_pattern "$bundle_id" "$pattern"; then + return 0 + fi done return 1 } @@ -1598,6 +1610,36 @@ find_app_files() { files_to_clean+=("$framework") done < <(find ~/Library/PrivateFrameworks \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + # Audio Plug-Ins + while IFS= read -r -d '' plugin; do + files_to_clean+=("$plugin") + done < <(find ~/Library/Audio/Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + + # Components + while IFS= read -r -d '' component; do + files_to_clean+=("$component") + done < <(find ~/Library/Components \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + + # Metadata + while IFS= read -r -d '' metadata; do + files_to_clean+=("$metadata") + done < <(find ~/Library/Metadata \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + + # Workflows + [[ -d ~/Library/Workflows/"$app_name".workflow ]] && files_to_clean+=("$HOME/Library/Workflows/$app_name.workflow") + while IFS= read -r -d '' workflow; do + files_to_clean+=("$workflow") + done < <(find ~/Library/Workflows \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + + # Favorites (excluding Safari) + while IFS= read -r -d '' favorite; do + # Skip Safari favorites + case "$favorite" in + *Safari*) continue ;; + esac + files_to_clean+=("$favorite") + done < <(find ~/Library/Favorites \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + # Only print if array has elements to avoid unbound variable error if [[ ${#files_to_clean[@]} -gt 0 ]]; then printf '%s\n' "${files_to_clean[@]}" @@ -1704,6 +1746,21 @@ find_app_system_files() { [[ -d /Library/Caches/"$bundle_id" ]] && system_files+=("/Library/Caches/$bundle_id") [[ -d /Library/Caches/"$app_name" ]] && system_files+=("/Library/Caches/$app_name") + # System Audio Plug-Ins + while IFS= read -r -d '' plugin; do + system_files+=("$plugin") + done < <(find /Library/Audio/Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + + # System Components + while IFS= read -r -d '' component; do + system_files+=("$component") + done < <(find /Library/Components \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + + # System Extensions + while IFS= read -r -d '' extension; do + system_files+=("$extension") + done < <(find /Library/Extensions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) + # Only print if array has elements if [[ ${#system_files[@]} -gt 0 ]]; then printf '%s\n' "${system_files[@]}" diff --git a/lib/ui_app_selector.sh b/lib/ui_app_selector.sh index 5a86403..f8c63e1 100755 --- a/lib/ui_app_selector.sh +++ b/lib/ui_app_selector.sh @@ -7,17 +7,41 @@ set -euo pipefail format_app_display() { local display_name="$1" size="$2" last_used="$3" - # Truncate long names + # Compact last-used wording to keep column width tidy + local compact_last_used + case "$last_used" in + "" | "Unknown") compact_last_used="Unknown" ;; + "Never" | "Recent" | "Today" | "Yesterday" | "This year" | "Old") compact_last_used="$last_used" ;; + *) + if [[ $last_used =~ ^([0-9]+)[[:space:]]+days?\ ago$ ]]; then + compact_last_used="${BASH_REMATCH[1]}d ago" + elif [[ $last_used =~ ^([0-9]+)[[:space:]]+weeks?\ ago$ ]]; then + compact_last_used="${BASH_REMATCH[1]}w ago" + elif [[ $last_used =~ ^([0-9]+)[[:space:]]+months?\ ago$ ]]; then + compact_last_used="${BASH_REMATCH[1]}m ago" + elif [[ $last_used =~ ^([0-9]+)[[:space:]]+month\(s\)\ ago$ ]]; then + compact_last_used="${BASH_REMATCH[1]}m ago" + elif [[ $last_used =~ ^([0-9]+)[[:space:]]+years?\ ago$ ]]; then + compact_last_used="${BASH_REMATCH[1]}y ago" + else + compact_last_used="$last_used" + fi + ;; + esac + + # Truncate long names with consistent width local truncated_name="$display_name" - if [[ ${#display_name} -gt 24 ]]; then - truncated_name="${display_name:0:21}..." + if [[ ${#display_name} -gt 22 ]]; then + truncated_name="${display_name:0:19}..." fi # Format size local size_str="Unknown" [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size" - printf "%-24s (%s) | %s" "$truncated_name" "$size_str" "$last_used" + # Use consistent column widths for perfect alignment: + # name column (22), right-aligned size column (9), then compact last-used value. + printf "%-22s %9s | %s" "$truncated_name" "$size_str" "$compact_last_used" } # Global variable to store selection result (bash 3.2 compatible)