diff --git a/README.md b/README.md index 019eac4..61be581 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,7 @@ mo --help # Show help mo --version # Show installed version ``` -> If the command is not found, run `mole update` once to upgrade to the latest version. -> -> Recommended: Start with `mo clean --dry-run` to preview what will be deleted, use `mo clean --whitelist` to protect important caches +> **Recommended:** Start with `mo clean --dry-run` to preview what will be deleted. Use `mo clean --whitelist` to protect important caches that are slow to re-download, like Gradle, npm, Homebrew, PyTorch models, Docker images. Interactive menu with 42 cache types, config saved to `~/.config/mole/whitelist`. ## Features in Detail @@ -142,16 +140,14 @@ Total: 156.8GB ## FAQ -- **Is Mole safe?** Mole only cleans caches and logs, it doesn't touch app settings, user documents, or system files. Run `mo clean --dry-run` first to preview what will be removed. +- **Is Mole safe?** Mole only cleans caches and logs, it doesn't touch app settings, user documents, or system files. Start with `mo clean --dry-run` to preview what will be removed. - **How often should I clean?** Once a month, or when disk space is running low. -- **Can I protect specific caches?** Yes. Run `mo clean --whitelist` to choose which caches to keep. Common ones like Playwright browsers and HuggingFace models are already protected. +- **Can I protect specific caches?** Yes. Run `mo clean --whitelist` to select caches to protect. - **Touch ID support?** Mole uses `sudo` for privileges, so you'll get a password prompt unless you've configured Touch ID for sudo. -- **Enable Touch ID for sudo:** - ```bash sudo nano /etc/pam.d/sudo diff --git a/bin/clean.sh b/bin/clean.sh index d7587f2..e53d903 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -23,7 +23,7 @@ readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data readonly SIZE_1GB_KB=1048576 # 1GB in kilobytes readonly SIZE_1MB_KB=1024 # 1MB in kilobytes -# Default whitelist patterns to avoid removing critical caches (can be extended by user) +# Core whitelist patterns - always protected WHITELIST_PATTERNS=( "$HOME/Library/Caches/ms-playwright*" "$HOME/.cache/huggingface*" @@ -68,6 +68,7 @@ SECTION_ACTIVITY=0 LAST_CLEAN_RESULT=0 files_cleaned=0 total_size_cleaned=0 +whitelist_skipped_count=0 SUDO_KEEPALIVE_PID="" note_activity() { @@ -204,19 +205,30 @@ safe_clean() { local removed_any=0 local total_size_bytes=0 local total_count=0 + local skipped_count=0 # Optimized parallel processing for better performance local -a existing_paths=() for path in "${targets[@]}"; do local skip=false - for w in "${WHITELIST_PATTERNS[@]}"; do - if [[ "$path" == $w ]]; then - skip=true; break - fi - done + if [[ ${#WHITELIST_PATTERNS[@]} -gt 0 ]]; then + for w in "${WHITELIST_PATTERNS[@]}"; do + # Match both exact path and glob pattern + if [[ "$path" == "$w" ]] || [[ "$path" == $w ]]; then + skip=true + ((skipped_count++)) + break + fi + done + fi [[ "$skip" == "true" ]] && continue [[ -e "$path" ]] && existing_paths+=("$path") done + + # Update global whitelist skip counter + if [[ $skipped_count -gt 0 ]]; then + ((whitelist_skipped_count += skipped_count)) + fi if [[ ${#existing_paths[@]} -eq 0 ]]; then LAST_CLEAN_RESULT=0 @@ -322,14 +334,15 @@ start_cleanup() { clear printf '\n' echo -e "${PURPLE}Clean Your Mac${NC}" + if [[ "$DRY_RUN" != "true" && -t 0 ]]; then - printf '\n' + echo "" echo -e "${YELLOW}Tip:${NC} Safety first—run 'mo clean --dry-run'. Important Macs should stop." fi if [[ "$DRY_RUN" == "true" ]]; then echo "" - echo -e "${YELLOW}Dry Run mode:${NC} showing what would be removed (no deletions)." + echo -e "${YELLOW}Dry Run Mode${NC} - Preview only, no deletions" echo "" SYSTEM_CLEAN=false return @@ -337,7 +350,7 @@ start_cleanup() { if [[ -t 0 ]]; then echo "" - echo -ne "${BLUE}System cleanup? ${GRAY}Enter to continue, any key to skip${NC} " + echo -ne "${ICON_SETTINGS} ${BLUE}System cleanup?${NC} ${GRAY}Enter to continue, any key to skip${NC} " # Use IFS= and read without -n to allow Ctrl+C to work properly IFS= read -r -s -n 1 choice @@ -351,10 +364,9 @@ start_cleanup() { # Enter or y = yes, do system cleanup if [[ -z "$choice" ]] || [[ "$choice" == $'\n' ]] || [[ "$choice" =~ ^[Yy]$ ]]; then - echo "" if request_sudo_access "System cleanup requires admin access"; then SYSTEM_CLEAN=true - echo -e "${GREEN}✓ Admin access granted${NC}" + echo -e "${GREEN}✓${NC} Admin access granted" # Start sudo keepalive with error handling ( local retry_count=0 @@ -376,27 +388,35 @@ start_cleanup() { else SYSTEM_CLEAN=false echo "" - echo -e "${YELLOW}Authentication failed, continuing with user-level cleanup${NC}" + echo -e "${YELLOW}Authentication failed${NC}, continuing with user-level cleanup" fi else # Any other key = no system cleanup SYSTEM_CLEAN=false - echo "" echo -e "Skipped system cleanup, user-level only" fi else SYSTEM_CLEAN=false echo "" - echo -e " Running in non-interactive mode" - echo " • System-level cleanup skipped (requires interaction)" - echo " • User-level cleanup will proceed automatically" + echo "Running in non-interactive mode" + echo " • System-level cleanup skipped (requires interaction)" + echo " • User-level cleanup will proceed automatically" echo "" fi } perform_cleanup() { echo "" - echo "$(detect_architecture) | Free space: $(get_free_space)" + echo "${ICON_SYSTEM} $(detect_architecture) | Free space: $(get_free_space)" + + # Show whitelist info if patterns are active + local active_count=${#WHITELIST_PATTERNS[@]} + if [[ $active_count -gt 2 ]]; then + local custom_count=$((active_count - 2)) + echo -e "${BLUE}✓${NC} Whitelist: $custom_count custom + 2 core patterns active" + elif [[ $active_count -eq 2 ]]; then + echo -e "${BLUE}✓${NC} Whitelist: 2 core patterns active" + fi # Get initial space space_before=$(df / | tail -1 | awk '{print $4}') @@ -406,9 +426,9 @@ perform_cleanup() { files_cleaned=0 total_size_cleaned=0 - # ===== 1. System cleanup (if admin) - Do this first while sudo is fresh ===== + # ===== 1. Deep system cleanup (if admin) - Do this first while sudo is fresh ===== if [[ "$SYSTEM_CLEAN" == "true" ]]; then - start_section "System-level cleanup" + start_section "Deep system-level cleanup" # Clean system caches more safely sudo find /Library/Caches -name "*.cache" -delete 2>/dev/null || true @@ -1273,8 +1293,7 @@ perform_cleanup() { # Summary if [[ $orphaned_count -gt 0 ]]; then local orphaned_mb=$(echo "$total_orphaned_kb" | awk '{printf "%.1f", $1/1024}') - echo "" - echo " ${GREEN}☺︎${NC} ${GREEN}Cleaned $orphaned_count orphaned items (~${orphaned_mb}MB)${NC}" + echo " ${BLUE}●${NC} Cleaned $orphaned_count orphaned items (~${orphaned_mb}MB)" note_activity else echo " ${BLUE}○${NC} No old orphaned app data found" @@ -1328,11 +1347,17 @@ perform_cleanup() { # Show file/category stats for dry run if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then - printf "Files to clean: %s | Categories: %s\n" "$files_cleaned" "$total_items" + printf "Files to clean: %s | Categories: %s" "$files_cleaned" "$total_items" + [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" + printf "\n" elif [[ $files_cleaned -gt 0 ]]; then - printf "Files to clean: %s\n" "$files_cleaned" + printf "Files to clean: %s" "$files_cleaned" + [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" + printf "\n" elif [[ $total_items -gt 0 ]]; then - printf "Categories: %s\n" "$total_items" + printf "Categories: %s" "$total_items" + [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" + printf "\n" fi echo "" @@ -1342,11 +1367,17 @@ perform_cleanup() { # Show file/category stats for actual cleanup if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then - printf "Files cleaned: %s | Categories: %s\n" "$files_cleaned" "$total_items" + printf "Files cleaned: %s | Categories: %s" "$files_cleaned" "$total_items" + [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" + printf "\n" elif [[ $files_cleaned -gt 0 ]]; then - printf "Files cleaned: %s\n" "$files_cleaned" + printf "Files cleaned: %s" "$files_cleaned" + [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" + printf "\n" elif [[ $total_items -gt 0 ]]; then - printf "Categories: %s\n" "$total_items" + printf "Categories: %s" "$total_items" + [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" + printf "\n" fi if [[ $(echo "$freed_gb" | awk '{print ($1 >= 1) ? 1 : 0}') -eq 1 ]]; then diff --git a/lib/common.sh b/lib/common.sh index 29cdfc2..c484084 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -28,6 +28,8 @@ readonly ICON_ERROR="✗" # Error readonly ICON_EMPTY="○" # Empty state readonly ICON_LIST="-" # List item readonly ICON_MENU="▸" # Menu item +readonly ICON_SYSTEM="☯︎" # System/Architecture info +readonly ICON_SETTINGS="⚙" # Settings/Configuration # Spinner character helpers (ASCII by default, overridable via env) mo_spinner_chars() { diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh index 9a8fda4..25cc247 100755 --- a/lib/paginated_menu.sh +++ b/lib/paginated_menu.sh @@ -75,6 +75,8 @@ paginated_multi_select() { # Setup terminal - preserve interrupt character stty -echo -icanon intr ^C 2>/dev/null || true enter_alt_screen + # Clear screen once on entry to alt screen + printf "\033[2J\033[H" >&2 hide_cursor # Helper functions @@ -94,7 +96,11 @@ paginated_multi_select() { # Draw the complete menu draw_menu() { - printf "\033[H\033[J" >&2 # Clear screen and move to top + # 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" # Header - compute underline length without external seq dependency local title_clean="${title//[^[:print:]]/}" @@ -103,14 +109,15 @@ paginated_multi_select() { # Build underline robustly (no seq); printf width then translate spaces to '=' local underline underline=$(printf '%*s' "$underline_len" '' | tr ' ' '=') - printf "${PURPLE}%s${NC}\n%s\n" "$title" "$underline" >&2 + printf "${clear_line}${PURPLE}%s${NC}\n" "$title" >&2 + printf "${clear_line}%s\n" "$underline" >&2 # Status local selected_count=0 for ((i = 0; i < total_items; i++)); do [[ ${selected[i]} == true ]] && ((selected_count++)) done - printf "Page %d/%d │ Total: %d │ Selected: %d\n\n" \ + printf "${clear_line}Page %d/%d │ Total: %d │ Selected: %d\n\n" \ $((current_page + 1)) $total_pages $total_items $selected_count >&2 # Items for current page @@ -124,14 +131,18 @@ paginated_multi_select() { render_item $i $is_current done - # Fill empty slots + # Fill empty slots to clear previous content local items_shown=$((end_idx - start_idx + 1)) for ((i = items_shown; i < items_per_page; i++)); do - print_line "" + printf "${clear_line}\n" >&2 done - print_line "" - print_line "${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit" + # Clear any remaining lines at bottom + printf "${clear_line}\n" >&2 + printf "${clear_line}${GRAY}↑/↓${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 diff --git a/lib/whitelist_manager.sh b/lib/whitelist_manager.sh index d804619..a23816e 100755 --- a/lib/whitelist_manager.sh +++ b/lib/whitelist_manager.sh @@ -12,11 +12,113 @@ source "$SCRIPT_DIR/paginated_menu.sh" # Config file path WHITELIST_CONFIG="$HOME/.config/mole/whitelist" +# Core whitelist patterns that are always protected declare -a DEFAULT_WHITELIST_PATTERNS=( "$HOME/Library/Caches/ms-playwright*" "$HOME/.cache/huggingface*" ) +# Determine if a pattern matches one of the defaults +is_default_pattern() { + local candidate="$1" + for default_pat in "${DEFAULT_WHITELIST_PATTERNS[@]}"; do + if patterns_equivalent "$candidate" "$default_pat"; then + return 0 + fi + done + return 1 +} + +# Save whitelist patterns to config +save_whitelist_patterns() { + local -a patterns + patterns=("$@") + local -a custom_patterns + custom_patterns=() + mkdir -p "$(dirname "$WHITELIST_CONFIG")" + + cat > "$WHITELIST_CONFIG" << 'EOF' +# Mole Whitelist - Protected paths won't be deleted +# Default protections: Playwright browsers, HuggingFace models +# You can add custom paths here +EOF + + if [[ ${#patterns[@]} -gt 0 ]]; then + for pattern in "${patterns[@]}"; do + if is_default_pattern "$pattern"; then + continue + fi + local duplicate="false" + if [[ ${#custom_patterns[@]} -gt 0 ]]; then + for existing in "${custom_patterns[@]}"; do + if patterns_equivalent "$pattern" "$existing"; then + duplicate="true" + break + fi + done + fi + [[ "$duplicate" == "true" ]] && continue + custom_patterns+=("$pattern") + done + + if [[ ${#custom_patterns[@]} -gt 0 ]]; then + printf '\n' >> "$WHITELIST_CONFIG" + for pattern in "${custom_patterns[@]}"; do + echo "$pattern" >> "$WHITELIST_CONFIG" + done + fi + fi +} + +# Get all cache items with their patterns +get_all_cache_items() { + # Format: "display_name|pattern|category" + cat << 'EOF' +Gradle build cache (Android Studio, Gradle projects)|$HOME/.gradle/caches/*|ide_cache +Gradle daemon processes cache|$HOME/.gradle/daemon/*|ide_cache +Xcode DerivedData (build outputs, indexes)|$HOME/Library/Developer/Xcode/DerivedData/*|ide_cache +Xcode internal cache files|$HOME/Library/Caches/com.apple.dt.Xcode/*|ide_cache +Xcode iOS device support symbols|$HOME/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Caches/*|ide_cache +Maven local repository (Java dependencies)|$HOME/.m2/repository/*|ide_cache +JetBrains IDEs cache (IntelliJ, PyCharm, WebStorm)|$HOME/Library/Caches/JetBrains/*|ide_cache +Android Studio cache and indexes|$HOME/Library/Caches/Google/AndroidStudio*/*|ide_cache +VS Code runtime cache|$HOME/Library/Application Support/Code/Cache/*|ide_cache +VS Code extension and update cache|$HOME/Library/Application Support/Code/CachedData/*|ide_cache +VS Code system cache (Cursor, VSCodium)|$HOME/Library/Caches/com.microsoft.VSCode/*|ide_cache +Cursor editor cache|$HOME/Library/Caches/com.todesktop.230313mzl4w4u92/*|ide_cache +Bazel build cache|$HOME/.cache/bazel/*|compiler_cache +Go build cache and module cache|$HOME/Library/Caches/go-build/*|compiler_cache +Rust Cargo registry cache|$HOME/.cargo/registry/cache/*|compiler_cache +Rustup toolchain downloads|$HOME/.rustup/downloads/*|compiler_cache +ccache compiler cache|$HOME/.ccache/*|compiler_cache +sccache distributed compiler cache|$HOME/.cache/sccache/*|compiler_cache +CocoaPods cache (iOS dependencies)|$HOME/Library/Caches/CocoaPods/*|package_manager +npm package cache|$HOME/.npm/_cacache/*|package_manager +pip Python package cache|$HOME/.cache/pip/*|package_manager +Homebrew downloaded packages|$HOME/Library/Caches/Homebrew/*|package_manager +Yarn package manager cache|$HOME/.cache/yarn/*|package_manager +pnpm package store|$HOME/.pnpm-store/*|package_manager +Composer PHP dependencies cache|$HOME/.composer/cache/*|package_manager +RubyGems cache|$HOME/.gem/cache/*|package_manager +Go module cache|$HOME/go/pkg/mod/cache/*|package_manager +PyTorch model cache|$HOME/.cache/torch/*|ai_ml_cache +TensorFlow model and dataset cache|$HOME/.cache/tensorflow/*|ai_ml_cache +HuggingFace models and datasets|$HOME/.cache/huggingface/*|ai_ml_cache +Playwright browser binaries|$HOME/Library/Caches/ms-playwright*|ai_ml_cache +Selenium WebDriver binaries|$HOME/.cache/selenium/*|ai_ml_cache +Ollama local AI models|$HOME/.ollama/models/*|ai_ml_cache +Safari web browser cache|$HOME/Library/Caches/com.apple.Safari/*|browser_cache +Chrome browser cache|$HOME/Library/Caches/Google/Chrome/*|browser_cache +Firefox browser cache|$HOME/Library/Caches/Firefox/*|browser_cache +Brave browser cache|$HOME/Library/Caches/BraveSoftware/Brave-Browser/*|browser_cache +Docker Desktop image cache|$HOME/Library/Containers/com.docker.docker/Data/*|container_cache +Podman container cache|$HOME/.local/share/containers/cache/*|container_cache +Font cache|$HOME/Library/Caches/com.apple.FontRegistry/*|system_cache +Spotlight metadata cache|$HOME/Library/Caches/com.apple.spotlight/*|system_cache +CloudKit cache|$HOME/Library/Caches/CloudKit/*|system_cache +EOF +} + patterns_equivalent() { local first="${1/#~/$HOME}" local second="${2/#~/$HOME}" @@ -26,136 +128,11 @@ patterns_equivalent() { return 1 } -is_default_pattern() { - local candidate="$1" - local default_pat - for default_pat in "${DEFAULT_WHITELIST_PATTERNS[@]}"; do - if patterns_equivalent "$candidate" "$default_pat"; then - return 0 - fi - done - return 1 -} - -# Run dry-run cleanup and collect what would be deleted -collect_files_to_be_cleaned() { - local clean_sh="$SCRIPT_DIR/../bin/clean.sh" - local -a items=() - - if [[ -t 1 ]]; then - start_inline_spinner "Scanning cache files..." - else - echo "Scanning cache files..." - fi - - # Run clean.sh in dry-run mode - local temp_output=$(create_temp_file) - echo "" | bash "$clean_sh" --dry-run 2>&1 > "$temp_output" || true - - if [[ -t 1 ]]; then - stop_inline_spinner - fi - echo "" - - # Strip ANSI color codes for parsing - local temp_plain=$(create_temp_file) - sed $'s/\033\[[0-9;]*m//g' "$temp_output" > "$temp_plain" - - # Parse output: " → Description (size, dry)" - local pattern='^[[:space:]]*→[[:space:]]+([^(]+)(\([^)]+\))?[[:space:]]*\(([^,)]+),.*dry\)$' - while IFS= read -r line; do - if [[ "$line" =~ $pattern ]]; then - local description="${BASH_REMATCH[1]}" - local size="${BASH_REMATCH[3]}" - - description="${description#${description%%[![:space:]]*}}" - description="${description%${description##*[![:space:]]}}" - - [[ "$description" =~ ^Orphaned ]] && continue - - # Find corresponding path from clean.sh - local path="" - while IFS= read -r src_line; do - # Match: safe_clean "" - # Path may contain escaped spaces (\ ) - if [[ "$src_line" =~ safe_clean[[:space:]]+(.+)[[:space:]]+\"$description\" ]]; then - path="${BASH_REMATCH[1]}" - break - fi - done < "$clean_sh" - - path="${path/#\~/$HOME}" - [[ -z "$path" || "$path" =~ \$ ]] && continue - - items+=("$path|$description|$size") - fi - done < "$temp_plain" - - # Temp files will be auto-cleaned by cleanup_temp_files - - # Return early if no items found - if [[ ${#items[@]} -eq 0 ]]; then - AVAILABLE_CACHE_ITEMS=() - return - fi - - # Remove duplicates - local -a unique_items=() - local -a seen_descriptions=() - - for item in "${items[@]}"; do - IFS='|' read -r path desc size <<< "$item" - local is_duplicate=false - if [[ ${#seen_descriptions[@]} -gt 0 ]]; then - for seen in "${seen_descriptions[@]}"; do - [[ "$desc" == "$seen" ]] && is_duplicate=true && break - done - fi - - if [[ "$is_duplicate" == "false" ]]; then - unique_items+=("$item") - seen_descriptions+=("$desc") - fi - done - - # Sort by size (largest first) - local -a sorted_items=() - if [[ ${#unique_items[@]} -gt 0 ]]; then - while IFS= read -r item; do - sorted_items+=("$item") - done < <( - for item in "${unique_items[@]}"; do - IFS='|' read -r path desc size <<< "$item" - local size_kb=0 - if [[ "$size" =~ ([0-9.]+)GB ]]; then - size_kb=$(echo "${BASH_REMATCH[1]}" | awk '{printf "%d", $1 * 1024 * 1024}') - elif [[ "$size" =~ ([0-9.]+)MB ]]; then - size_kb=$(echo "${BASH_REMATCH[1]}" | awk '{printf "%d", $1 * 1024}') - elif [[ "$size" =~ ([0-9.]+)KB ]]; then - size_kb=$(echo "${BASH_REMATCH[1]}" | awk '{printf "%d", $1}') - fi - printf "%010d|%s\n" "$size_kb" "$item" - done | sort -rn | cut -d'|' -f2- - ) - fi - - # Safe assignment for empty array - if [[ ${#sorted_items[@]} -gt 0 ]]; then - AVAILABLE_CACHE_ITEMS=("${sorted_items[@]}") - else - AVAILABLE_CACHE_ITEMS=() - fi -} - -declare -a AVAILABLE_CACHE_ITEMS=() load_whitelist() { - local -a patterns=() + local -a patterns=("${DEFAULT_WHITELIST_PATTERNS[@]}") - # Always include default patterns - patterns=("${DEFAULT_WHITELIST_PATTERNS[@]}") - - # Add user-defined patterns from config file + # Load user-defined patterns from config file if [[ -f "$WHITELIST_CONFIG" ]]; then while IFS= read -r line; do line="${line#${line%%[![:space:]]*}}" @@ -206,135 +183,44 @@ is_whitelisted() { return 1 } -format_whitelist_item() { - local description="$1" size="$2" is_protected="$3" - local desc_display="$description" - [[ ${#description} -gt 40 ]] && desc_display="${description:0:37}..." - local size_display=$(printf "%-15s" "$size") - local status="" - [[ "$is_protected" == "true" ]] && status=" ${GREEN}[Protected]${NC}" - printf "%-40s %s%s" "$desc_display" "$size_display" "$status" -} - -# Get friendly description for a path pattern -get_description_for_pattern() { - local pattern="$1" - local desc="" - - # Hardcoded descriptions for common patterns - case "$pattern" in - *"ms-playwright"*) - echo "Playwright Browser" - return - ;; - *"huggingface"*) - echo "HuggingFace Model" - return - ;; - esac - - # Try to match with safe_clean in clean.sh - # Use fuzzy matching by removing trailing /* or * - local pattern_base="${pattern%/\*}" - pattern_base="${pattern_base%\*}" - - while IFS= read -r line; do - if [[ "$line" =~ safe_clean[[:space:]]+(.+)[[:space:]]+\"([^\"]+)\" ]]; then - local clean_path="${BASH_REMATCH[1]}" - local clean_desc="${BASH_REMATCH[2]}" - clean_path="${clean_path/#\~/$HOME}" - - # Remove trailing /* or * for comparison - local clean_base="${clean_path%/\*}" - clean_base="${clean_base%\*}" - - # Check if base paths match - if [[ "$pattern_base" == "$clean_base" || "$clean_path" == "$pattern" || "$pattern" == "$clean_path" ]]; then - echo "$clean_desc" - return - fi - fi - done < "$SCRIPT_DIR/../bin/clean.sh" - - # If no match found, return short path - echo "${pattern/#$HOME/~}" -} manage_whitelist() { + manage_whitelist_categories +} + +manage_whitelist_categories() { clear echo "" echo -e "${PURPLE}Whitelist Manager${NC}" echo "" + echo -e "${GRAY}Select caches to protect from cleanup.${NC}" + echo "" - # Load user-defined whitelist - CURRENT_WHITELIST_PATTERNS=() + # Load currently enabled patterns from both sources load_whitelist - echo "Select the cache files that need to be protected" - echo -e "${GRAY}Protected items are pre-selected. You can also edit ${WHITELIST_CONFIG} directly.${NC}" - echo "" - - collect_files_to_be_cleaned - - # Add items from config that are not in the scan results - local -a all_items=() - if [[ ${#AVAILABLE_CACHE_ITEMS[@]} -gt 0 ]]; then - all_items=("${AVAILABLE_CACHE_ITEMS[@]}") - fi - - # Add saved patterns that are not in scan results - if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -gt 0 ]]; then - for pattern in "${CURRENT_WHITELIST_PATTERNS[@]}"; do - local pattern_expanded="${pattern/#\~/$HOME}" - local found="false" - - if [[ ${#all_items[@]} -gt 0 ]]; then - for item in "${all_items[@]}"; do - IFS='|' read -r path _ _ <<< "$item" - if patterns_equivalent "$path" "$pattern_expanded"; then - found="true" - break - fi - done - fi - - if [[ "$found" == "false" ]]; then - local desc=$(get_description_for_pattern "$pattern_expanded") - all_items+=("$pattern_expanded|$desc|0B") - fi - done - fi - - if [[ ${#all_items[@]} -eq 0 ]]; then - echo -e "${GREEN}✓${NC} No cache files found - system is clean!" - echo "" - echo "Press any key to exit..." - read -n 1 -s - return 0 - fi - - # Update global array with all items - AVAILABLE_CACHE_ITEMS=("${all_items[@]}") - - echo -e "${GREEN}✓${NC} Found ${#AVAILABLE_CACHE_ITEMS[@]} items" - echo "" - + # Build cache items list + local -a cache_items=() + local -a cache_patterns=() local -a menu_options=() local -a preselected_indices=() local index=0 - for item in "${AVAILABLE_CACHE_ITEMS[@]}"; do - IFS='|' read -r path description size <<< "$item" - local is_protected="false" - if is_whitelisted "$path"; then - is_protected="true" + while IFS='|' read -r display_name pattern category; do + # Expand $HOME in pattern + pattern="${pattern/\$HOME/$HOME}" + + cache_items+=("$display_name") + cache_patterns+=("$pattern") + menu_options+=("$display_name") + + # Check if this pattern is currently whitelisted + if is_whitelisted "$pattern"; then preselected_indices+=("$index") fi - menu_options+=("$(format_whitelist_item "$description" "$size" "$is_protected")") - ((index++)) - done - echo -e "${GRAY}↑↓ Navigate | Space Toggle | Enter Save | Q Quit${NC}" + ((index++)) + done < <(get_all_cache_items) if [[ ${#preselected_indices[@]} -gt 0 ]]; then local IFS=',' @@ -344,106 +230,40 @@ manage_whitelist() { fi MOLE_SELECTION_RESULT="" - paginated_multi_select "Select items to protect" "${menu_options[@]}" + paginated_multi_select "Select caches to protect" "${menu_options[@]}" unset MOLE_PRESELECTED_INDICES local exit_code=$? if [[ $exit_code -ne 0 ]]; then echo "" - echo -e "${YELLOW}Cancelled${NC} - No changes made" + echo -e "${YELLOW}Cancelled${NC}" return 1 fi - local -a selected_indices=() - if [[ -n "$MOLE_SELECTION_RESULT" ]]; then - IFS=',' read -ra selected_indices <<< "$MOLE_SELECTION_RESULT" - fi - - save_whitelist "${selected_indices[@]}" -} - -save_whitelist() { - local -a selected_indices=("$@") - mkdir -p "$(dirname "$WHITELIST_CONFIG")" - + # Convert selected indices to patterns local -a selected_patterns=() - local selected_default_count=0 - local selected_custom_count=0 - - for idx in "${selected_indices[@]}"; do - if [[ $idx -ge 0 && $idx -lt ${#AVAILABLE_CACHE_ITEMS[@]} ]]; then - local item="${AVAILABLE_CACHE_ITEMS[$idx]}" - IFS='|' read -r path description size <<< "$item" - local portable_path="${path/#$HOME/~}" - - local duplicate="false" - if [[ ${#selected_patterns[@]} -gt 0 ]]; then - for existing in "${selected_patterns[@]}"; do - if patterns_equivalent "$portable_path" "$existing"; then - duplicate="true" - break - fi - done + if [[ -n "$MOLE_SELECTION_RESULT" ]]; then + local -a selected_indices + IFS=',' read -ra selected_indices <<< "$MOLE_SELECTION_RESULT" + for idx in "${selected_indices[@]}"; do + if [[ $idx -ge 0 && $idx -lt ${#cache_patterns[@]} ]]; then + local pattern="${cache_patterns[$idx]}" + # Convert back to portable format with ~ + pattern="${pattern/#$HOME/~}" + selected_patterns+=("$pattern") fi - [[ "$duplicate" == "true" ]] && continue - - if is_default_pattern "$portable_path"; then - ((selected_default_count++)) - else - ((selected_custom_count++)) - fi - - selected_patterns+=("$portable_path") - fi - done - - cat > "$WHITELIST_CONFIG" << 'EOF' -# Mole Whitelist - Protected paths won't be deleted -# Default: Playwright browsers, HuggingFace models -EOF - - # Only save custom (non-default) patterns - local -a custom_patterns=() - for pattern in "${selected_patterns[@]}"; do - if ! is_default_pattern "$pattern"; then - custom_patterns+=("$pattern") - fi - done - - if [[ ${#custom_patterns[@]} -gt 0 ]]; then - printf '\n' >> "$WHITELIST_CONFIG" - for pattern in "${custom_patterns[@]}"; do - echo "$pattern" >> "$WHITELIST_CONFIG" done fi - local total_count=${#selected_patterns[@]} - local -a summary_parts=() - if [[ $selected_default_count -gt 0 ]]; then - local default_label="default" - [[ $selected_default_count -ne 1 ]] && default_label+="s" - summary_parts+=("$selected_default_count $default_label") - fi - if [[ $selected_custom_count -gt 0 ]]; then - local custom_label="custom" - [[ $selected_custom_count -ne 1 ]] && custom_label+="s" - summary_parts+=("$selected_custom_count $custom_label") - fi - - local summary="" - if [[ ${#summary_parts[@]} -gt 0 ]]; then - summary=" (${summary_parts[0]}" - for ((i = 1; i < ${#summary_parts[@]}; i++)); do - summary+=", ${summary_parts[$i]}" - done - summary+=")" - fi + # Save to whitelist config + save_whitelist_patterns "${selected_patterns[@]}" echo "" - echo -e "${GREEN}✓${NC} Protected $total_count items${summary}" + echo -e "${GREEN}✓${NC} Protected ${#selected_patterns[@]} cache(s)" echo -e "${GRAY}Config: ${WHITELIST_CONFIG}${NC}" } + if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then manage_whitelist fi