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

Whitelist mode is more powerful

This commit is contained in:
Tw93
2025-10-10 23:05:21 +08:00
parent b7348c1207
commit 2cf56fa96d
5 changed files with 223 additions and 363 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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() {

View File

@@ -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

View File

@@ -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> "<description>"
# 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