mirror of
https://github.com/tw93/Mole.git
synced 2026-02-05 19:38:01 +00:00
🎨 Continue to upgrade to easy to use
This commit is contained in:
360
bin/clean.sh
360
bin/clean.sh
@@ -13,135 +13,6 @@ SYSTEM_CLEAN=false
|
||||
IS_M_SERIES=$([ "$(uname -m)" = "arm64" ] && echo "true" || echo "false")
|
||||
total_items=0
|
||||
|
||||
# Critical system settings that should NEVER be deleted
|
||||
PRESERVED_BUNDLE_PATTERNS=(
|
||||
"com.apple.*" # All Apple system services and settings
|
||||
"com.microsoft.*" # Microsoft Office and system apps
|
||||
"com.tencent.inputmethod.*" # Tencent input methods (WeType)
|
||||
"com.sogou.*" # Sogou input method
|
||||
"com.baidu.*" # Baidu input method
|
||||
"*.inputmethod.*" # Any input method bundles
|
||||
"*input*" # Any input-related bundles
|
||||
"loginwindow" # Login window settings
|
||||
"dock" # Dock settings
|
||||
"systempreferences" # System preferences
|
||||
"finder" # Finder settings
|
||||
"safari" # Safari settings
|
||||
"keychain*" # Keychain settings
|
||||
"security*" # Security settings
|
||||
"bluetooth*" # Bluetooth settings
|
||||
"wifi*" # WiFi settings
|
||||
"network*" # Network settings
|
||||
"tcc" # Privacy & Security permissions
|
||||
"notification*" # Notification settings
|
||||
"accessibility*" # Accessibility settings
|
||||
"universalaccess*" # Universal access settings
|
||||
"HIToolbox*" # Input method core settings
|
||||
"textinput*" # Text input settings
|
||||
"TextInput*" # Text input settings
|
||||
"keyboard*" # Keyboard settings
|
||||
"Keyboard*" # Keyboard settings
|
||||
"inputsource*" # Input source settings
|
||||
"InputSource*" # Input source settings
|
||||
"keylayout*" # Keyboard layout settings
|
||||
"KeyLayout*" # Keyboard layout settings
|
||||
# Additional critical system preference files that should never be deleted
|
||||
"GlobalPreferences" # System-wide preferences
|
||||
".GlobalPreferences" # Hidden global preferences
|
||||
"com.apple.systempreferences*" # System Preferences app settings
|
||||
"com.apple.controlstrip*" # Control Strip settings (TouchBar)
|
||||
"com.apple.trackpad*" # Trackpad settings
|
||||
"com.apple.driver.AppleBluetoothMultitouch.trackpad*" # Trackpad driver settings
|
||||
"com.apple.preference.*" # System preference modules
|
||||
"com.apple.LaunchServices*" # Launch Services (file associations)
|
||||
"com.apple.loginitems*" # Login items
|
||||
"com.apple.loginwindow*" # Login window settings
|
||||
"com.apple.screensaver*" # Screen saver settings
|
||||
"com.apple.desktopservices*" # Desktop services
|
||||
"com.apple.spaces*" # Mission Control/Spaces settings
|
||||
"com.apple.exposé*" # Exposé settings
|
||||
"com.apple.menuextra.*" # Menu bar extras
|
||||
"com.apple.systemuiserver*" # System UI server
|
||||
"com.apple.notificationcenterui*" # Notification Center settings
|
||||
"com.apple.MultitouchSupport*" # Multitouch/trackpad support
|
||||
"com.apple.AppleMultitouchTrackpad*" # Trackpad configuration
|
||||
"com.apple.universalaccess*" # Accessibility settings
|
||||
"com.apple.sound.*" # Sound settings
|
||||
"com.apple.AudioDevices*" # Audio device settings
|
||||
"com.apple.HIToolbox*" # Human Interface Toolbox
|
||||
"com.apple.LaunchServices*" # Launch Services
|
||||
"com.apple.loginwindow*" # Login window
|
||||
"com.apple.PowerChime*" # Power sounds
|
||||
"com.apple.WindowManager*" # Window management
|
||||
)
|
||||
|
||||
# Function to check if a bundle should be preserved (supports wildcards)
|
||||
should_preserve_bundle() {
|
||||
local bundle_id="$1"
|
||||
|
||||
# First check against preserved patterns
|
||||
for pattern in "${PRESERVED_BUNDLE_PATTERNS[@]}"; do
|
||||
# Use bash's built-in pattern matching which supports * and ? wildcards
|
||||
if [[ "$bundle_id" == $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Additional safety checks for critical system components
|
||||
case "$bundle_id" in
|
||||
# All Apple system services and apps
|
||||
com.apple.*)
|
||||
return 0
|
||||
;;
|
||||
# Critical system preferences and settings
|
||||
*dock*|*Dock*|*trackpad*|*Trackpad*|*mouse*|*Mouse*)
|
||||
return 0
|
||||
;;
|
||||
*keyboard*|*Keyboard*|*hotkey*|*HotKey*|*shortcut*|*Shortcut*)
|
||||
return 0
|
||||
;;
|
||||
*systempreferences*|*SystemPreferences*|*controlcenter*|*ControlCenter*)
|
||||
return 0
|
||||
;;
|
||||
*menubar*|*MenuBar*|*statusbar*|*StatusBar*)
|
||||
return 0
|
||||
;;
|
||||
*notification*|*Notification*|*alert*|*Alert*)
|
||||
return 0
|
||||
;;
|
||||
# Input methods and language settings
|
||||
*inputmethod*|*InputMethod*|*ime*|*IME*)
|
||||
return 0
|
||||
;;
|
||||
# Network and connectivity settings
|
||||
*wifi*|*WiFi*|*bluetooth*|*Bluetooth*|*network*|*Network*)
|
||||
return 0
|
||||
;;
|
||||
# Security and privacy settings
|
||||
*security*|*Security*|*privacy*|*Privacy*|*keychain*|*Keychain*)
|
||||
return 0
|
||||
;;
|
||||
# Display and graphics settings
|
||||
*display*|*Display*|*graphics*|*Graphics*|*screen*|*Screen*)
|
||||
return 0
|
||||
;;
|
||||
# Audio and sound settings
|
||||
*audio*|*Audio*|*sound*|*Sound*|*volume*|*Volume*)
|
||||
return 0
|
||||
;;
|
||||
# System services and daemons
|
||||
*daemon*|*Daemon*|*service*|*Service*|*agent*|*Agent*)
|
||||
return 0
|
||||
;;
|
||||
# Accessibility and universal access
|
||||
*accessibility*|*Accessibility*|*universalaccess*|*UniversalAccess*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Tracking variables
|
||||
TRACK_SECTION=0
|
||||
SECTION_ACTIVITY=0
|
||||
@@ -228,7 +99,8 @@ stop_spinner() {
|
||||
start_section() {
|
||||
TRACK_SECTION=1
|
||||
SECTION_ACTIVITY=0
|
||||
log_header "$1"
|
||||
echo ""
|
||||
echo -e "${PURPLE}▶ $1${NC}"
|
||||
}
|
||||
|
||||
end_section() {
|
||||
@@ -255,15 +127,15 @@ safe_clean() {
|
||||
fi
|
||||
|
||||
local removed_any=0
|
||||
local total_size_bytes=0
|
||||
local total_count=0
|
||||
|
||||
for path in "${targets[@]}"; do
|
||||
local size_bytes=0
|
||||
local size_human="0B"
|
||||
local count=0
|
||||
|
||||
if [[ -e "$path" ]]; then
|
||||
size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
size_human=$(du -sh "$path" 2>/dev/null | awk '{print $1}' || echo "0B")
|
||||
count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$count" -eq 0 || "$size_bytes" -eq 0 ]]; then
|
||||
@@ -271,47 +143,55 @@ safe_clean() {
|
||||
fi
|
||||
|
||||
rm -rf "$path" 2>/dev/null || true
|
||||
|
||||
((total_size_bytes += size_bytes))
|
||||
((total_count += count))
|
||||
removed_any=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Only show output if something was actually cleaned
|
||||
if [[ $removed_any -eq 1 ]]; then
|
||||
local size_human
|
||||
if [[ $total_size_bytes -gt 1048576 ]]; then # > 1GB
|
||||
size_human=$(echo "$total_size_bytes" | awk '{printf "%.1fGB", $1/1024/1024}')
|
||||
elif [[ $total_size_bytes -gt 1024 ]]; then # > 1MB
|
||||
size_human=$(echo "$total_size_bytes" | awk '{printf "%.1fMB", $1/1024}')
|
||||
else
|
||||
# For non-existent paths, show as cleaned with realistic placeholder values
|
||||
size_human="4.0K"
|
||||
size_human="${total_size_bytes}KB"
|
||||
fi
|
||||
|
||||
local label="$description"
|
||||
if [[ ${#targets[@]} -gt 1 ]]; then
|
||||
label+=" [$(basename "$path")]"
|
||||
label+=" (${#targets[@]} items)"
|
||||
fi
|
||||
|
||||
echo -e " ${GREEN}✓${NC} $label ${GREEN}($size_human)${NC}"
|
||||
((files_cleaned+=count))
|
||||
((total_size_cleaned+=size_bytes))
|
||||
((files_cleaned+=total_count))
|
||||
((total_size_cleaned+=total_size_bytes))
|
||||
((total_items++))
|
||||
removed_any=1
|
||||
note_activity
|
||||
done
|
||||
fi
|
||||
|
||||
LAST_CLEAN_RESULT=$removed_any
|
||||
return 0
|
||||
}
|
||||
|
||||
start_cleanup() {
|
||||
clear
|
||||
echo "🕳️ Mole - Deeper system cleanup"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
echo "This will clean: App caches & logs, Browser data, Developer tools, Temporary files & more..."
|
||||
echo "Removing app caches, browser data, developer tools, and temporary files..."
|
||||
echo ""
|
||||
|
||||
# Check if we're in an interactive terminal
|
||||
if [[ -t 0 ]]; then
|
||||
# Interactive mode - ask for password
|
||||
echo "For deeper system cleanup, administrator password is needed."
|
||||
echo -n "Enter password (or press Enter to skip): "
|
||||
echo "Enter admin password for system-level cleanup (or press Enter to skip):"
|
||||
echo -n "> "
|
||||
read -s password
|
||||
echo ""
|
||||
else
|
||||
# Non-interactive mode - skip password prompt
|
||||
password=""
|
||||
echo "Running in non-interactive mode, skipping system-level cleanup."
|
||||
log_info "Running in non-interactive mode, skipping system-level cleanup."
|
||||
fi
|
||||
|
||||
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
|
||||
@@ -331,16 +211,7 @@ start_cleanup() {
|
||||
|
||||
perform_cleanup() {
|
||||
echo ""
|
||||
echo "🕳️ Mole - Deeper system cleanup"
|
||||
echo "========================"
|
||||
echo "🍎 Detected: $(detect_architecture) | 💾 Free space: $(get_free_space)"
|
||||
|
||||
if [[ "$SYSTEM_CLEAN" == "true" ]]; then
|
||||
echo "🚀 Mode: System-level cleanup (admin privileges)"
|
||||
else
|
||||
echo "🚀 Mode: User-level cleanup (no password required)"
|
||||
fi
|
||||
echo ""
|
||||
echo "🍎 $(detect_architecture) | 💾 Free space: $(get_free_space)"
|
||||
|
||||
# Get initial space
|
||||
space_before=$(df / | tail -1 | awk '{print $4}')
|
||||
@@ -365,6 +236,7 @@ perform_cleanup() {
|
||||
end_section
|
||||
fi
|
||||
|
||||
|
||||
# ===== 2. User essentials =====
|
||||
start_section "System essentials"
|
||||
safe_clean ~/Library/Caches/* "User app cache"
|
||||
@@ -390,6 +262,7 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Caches/com.apple.bird* "iCloud cache"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 2. Browsers =====
|
||||
start_section "Browser cleanup"
|
||||
# Safari
|
||||
@@ -415,6 +288,7 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 3. Developer tools =====
|
||||
start_section "Developer tools"
|
||||
# Node.js ecosystem
|
||||
@@ -495,7 +369,6 @@ perform_cleanup() {
|
||||
safe_clean ~/.pnpm-store/* "pnpm store cache"
|
||||
safe_clean ~/.cache/typescript/* "TypeScript cache"
|
||||
safe_clean ~/.cache/electron/* "Electron cache"
|
||||
safe_clean ~/.cache/yarn/* "Yarn cache"
|
||||
safe_clean ~/.turbo/* "Turbo cache"
|
||||
safe_clean ~/.next/* "Next.js cache"
|
||||
safe_clean ~/.vite/* "Vite cache"
|
||||
@@ -542,13 +415,11 @@ perform_cleanup() {
|
||||
# Cloud and container tools
|
||||
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
|
||||
safe_clean ~/.cache/terraform/* "Terraform cache"
|
||||
safe_clean ~/.kube/cache/* "Kubernetes cache"
|
||||
|
||||
# API and network development tools
|
||||
safe_clean ~/Library/Caches/com.getpaw.Paw/* "Paw API cache"
|
||||
safe_clean ~/Library/Caches/com.charlesproxy.charles/* "Charles Proxy cache"
|
||||
safe_clean ~/Library/Caches/com.proxyman.NSProxy/* "Proxyman cache"
|
||||
safe_clean ~/Library/Caches/redis-desktop-manager/* "Redis Desktop Manager cache"
|
||||
|
||||
# CI/CD tools
|
||||
safe_clean ~/.grafana/cache/* "Grafana cache"
|
||||
@@ -575,17 +446,17 @@ perform_cleanup() {
|
||||
safe_clean ~/.ivy2/cache/* "Ivy cache"
|
||||
safe_clean ~/.pub-cache/* "Dart Pub cache"
|
||||
|
||||
# Network tools cache (safe)
|
||||
# Network tools cache
|
||||
safe_clean ~/.cache/curl/* "curl cache"
|
||||
safe_clean ~/.cache/wget/* "wget cache"
|
||||
safe_clean ~/Library/Caches/curl/* "curl cache"
|
||||
safe_clean ~/Library/Caches/wget/* "wget cache"
|
||||
safe_clean ~/Library/Caches/curl/* "curl cache (macOS)"
|
||||
safe_clean ~/Library/Caches/wget/* "wget cache (macOS)"
|
||||
|
||||
# Git and version control
|
||||
safe_clean ~/.cache/pre-commit/* "pre-commit cache"
|
||||
safe_clean ~/.gitconfig.bak* "Git config backup"
|
||||
|
||||
# Mobile development
|
||||
# Mobile development additional
|
||||
safe_clean ~/.cache/flutter/* "Flutter cache"
|
||||
safe_clean ~/.gradle/daemon/* "Gradle daemon logs"
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "iOS device cache"
|
||||
@@ -602,26 +473,15 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Caches/com.eggerapps.Sequel-Pro/* "Sequel Pro cache"
|
||||
safe_clean ~/Library/Caches/redis-desktop-manager/* "Redis Desktop Manager cache"
|
||||
|
||||
# Terminal and shell tools
|
||||
safe_clean ~/.oh-my-zsh/cache/* "Oh My Zsh cache"
|
||||
safe_clean ~/.config/fish/fish_history.bak* "Fish shell backup"
|
||||
safe_clean ~/.bash_history.bak* "Bash history backup"
|
||||
safe_clean ~/.zsh_history.bak* "Zsh history backup"
|
||||
|
||||
# Code quality and analysis
|
||||
safe_clean ~/.sonar/* "SonarQube cache"
|
||||
safe_clean ~/.cache/eslint/* "ESLint cache"
|
||||
safe_clean ~/.cache/prettier/* "Prettier cache"
|
||||
|
||||
# Crash reports and debugging
|
||||
safe_clean ~/Library/Caches/SentryCrash/* "Sentry crash reports"
|
||||
safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports"
|
||||
safe_clean ~/Library/Caches/com.crashlytics.data/* "Crashlytics data"
|
||||
# REMOVED: ~/Library/Saved\ Application\ State/* - This contains important app state including Dock settings
|
||||
safe_clean ~/Library/HTTPStorages/* "HTTP storage cache"
|
||||
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 4. Applications =====
|
||||
start_section "Applications"
|
||||
|
||||
@@ -678,24 +538,24 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Caches/com.valvesoftware.steam/* "Steam cache"
|
||||
safe_clean ~/Library/Caches/com.epicgames.EpicGamesLauncher/* "Epic Games cache"
|
||||
|
||||
# Utilities and productivity
|
||||
safe_clean ~/Library/Caches/com.nektony.App-Cleaner-SIIICn/* "App Cleaner cache"
|
||||
# Utilities and productivity (only cache, avoid license/settings data)
|
||||
safe_clean ~/Library/Caches/com.runjuu.Input-Source-Pro/* "Input Source Pro cache"
|
||||
safe_clean ~/Library/Caches/macos-wakatime.WakaTime/* "WakaTime cache"
|
||||
safe_clean ~/Library/Caches/notion.id/* "Notion cache"
|
||||
safe_clean ~/Library/Caches/md.obsidian/* "Obsidian cache"
|
||||
safe_clean ~/Library/Caches/com.1password.*/* "1Password cache"
|
||||
safe_clean ~/Library/Caches/com.runningwithcrayons.Alfred/* "Alfred cache"
|
||||
safe_clean ~/Library/Caches/cx.c3.theunarchiver/* "The Unarchiver cache"
|
||||
safe_clean ~/Library/Caches/com.freemacsoft.AppCleaner/* "AppCleaner cache"
|
||||
|
||||
# Note: Skipping App Cleaner, 1Password and similar apps to preserve licenses
|
||||
|
||||
end_section
|
||||
|
||||
# ===== 5. Orphaned leftovers =====
|
||||
|
||||
# ===== Orphaned leftovers =====
|
||||
start_section "Orphaned app files"
|
||||
|
||||
# Build a list of installed application bundle identifiers
|
||||
echo -e " ${BLUE}🔍${NC} Building app list..."
|
||||
echo -n " ${BLUE}🔍${NC} Scanning installed applications..."
|
||||
local installed_bundles=$(mktemp)
|
||||
# More robust approach that won't hang
|
||||
for app in /Applications/*.app; do
|
||||
@@ -705,13 +565,15 @@ perform_cleanup() {
|
||||
fi
|
||||
done
|
||||
local app_count=$(wc -l < "$installed_bundles" | tr -d ' ')
|
||||
echo -e " ${GREEN}✓${NC} Found $app_count apps"
|
||||
echo " ${GREEN}✓${NC} Found $app_count apps"
|
||||
|
||||
local found_orphaned=false
|
||||
local cache_count=0
|
||||
local data_count=0
|
||||
local pref_count=0
|
||||
|
||||
# Check for orphaned caches (with protection for critical system settings)
|
||||
echo -e " ${BLUE}🔍${NC} Checking caches..."
|
||||
local cache_count=0
|
||||
echo -n " ${BLUE}🔍${NC} Scanning cache directories..."
|
||||
if ls ~/Library/Caches/com.* >/dev/null 2>&1; then
|
||||
for cache_dir in ~/Library/Caches/com.*; do
|
||||
[[ -d "$cache_dir" ]] || continue
|
||||
@@ -727,11 +589,10 @@ perform_cleanup() {
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e " ${GREEN}✓${NC} Checked caches ($cache_count removed)"
|
||||
echo " ${GREEN}✓${NC} Complete ($cache_count removed)"
|
||||
|
||||
# Check for orphaned application support data (with protection for critical system settings)
|
||||
echo -e " ${BLUE}🔍${NC} Checking app data..."
|
||||
local data_count=0
|
||||
echo -n " ${BLUE}🔍${NC} Scanning application data..."
|
||||
if ls ~/Library/Application\ Support/com.* >/dev/null 2>&1; then
|
||||
for support_dir in ~/Library/Application\ Support/com.*; do
|
||||
[[ -d "$support_dir" ]] || continue
|
||||
@@ -740,11 +601,20 @@ perform_cleanup() {
|
||||
if should_preserve_bundle "$bundle_id"; then
|
||||
continue
|
||||
fi
|
||||
# Extra safety for Application Support data
|
||||
# Extra safety for Application Support data (preserve licenses and critical settings)
|
||||
case "$bundle_id" in
|
||||
# System components
|
||||
*dock*|*Dock*|*controlcenter*|*ControlCenter*|*systempreferences*|*SystemPreferences*)
|
||||
continue
|
||||
;;
|
||||
# Paid software and license-critical apps
|
||||
*nektony*|*macpaw*|*jetbrains*|*sublimetext*|*adobe*|*1password*|*agilebits*|*omnigroup*|*culturedcode*)
|
||||
continue
|
||||
;;
|
||||
# Security and password managers
|
||||
*lastpass*|*dashlane*|*bitwarden*|*keepass*)
|
||||
continue
|
||||
;;
|
||||
*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*)
|
||||
continue
|
||||
;;
|
||||
@@ -756,11 +626,10 @@ perform_cleanup() {
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e " ${GREEN}✓${NC} Checked app data ($data_count removed)"
|
||||
echo " ${GREEN}✓${NC} Complete ($data_count removed)"
|
||||
|
||||
# Check for orphaned preferences (with protection for critical system settings)
|
||||
echo -e " ${BLUE}🔍${NC} Checking preferences..."
|
||||
local pref_count=0
|
||||
echo -n " ${BLUE}🔍${NC} Scanning preference files..."
|
||||
if ls ~/Library/Preferences/com.*.plist >/dev/null 2>&1; then
|
||||
for pref_file in ~/Library/Preferences/com.*.plist; do
|
||||
[[ -f "$pref_file" ]] || continue
|
||||
@@ -769,8 +638,9 @@ perform_cleanup() {
|
||||
if should_preserve_bundle "$bundle_id"; then
|
||||
continue
|
||||
fi
|
||||
# Extra safety: Never delete preference files that might affect system behavior
|
||||
# Extra safety: Never delete preference files that might affect system behavior or paid app licenses
|
||||
case "$bundle_id" in
|
||||
# System components
|
||||
*dock*|*Dock*|*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*)
|
||||
continue
|
||||
;;
|
||||
@@ -780,6 +650,16 @@ perform_cleanup() {
|
||||
*menubar*|*MenuBar*|*hotkeys*|*HotKeys*|*shortcuts*|*Shortcuts*)
|
||||
continue
|
||||
;;
|
||||
# Licensed software and critical apps (preserve activation data)
|
||||
*nektony*|*macpaw*|*jetbrains*|*sublimetext*|*adobe*|*1password*|*agilebits*)
|
||||
continue
|
||||
;;
|
||||
*omnigroup*|*culturedcode*|*lastpass*|*dashlane*|*bitwarden*|*keepass*)
|
||||
continue
|
||||
;;
|
||||
*bohemiancoding*|*figma*|*framer*|*panic*|*sequelpro*|*tinyapp*|*pixelmator*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if ! grep -q "$bundle_id" "$installed_bundles" 2>/dev/null; then
|
||||
safe_clean "$pref_file" "Orphaned preference: $bundle_id"
|
||||
@@ -788,17 +668,12 @@ perform_cleanup() {
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo -e " ${GREEN}✓${NC} Checked preferences ($pref_count removed)"
|
||||
echo " ${GREEN}✓${NC} Complete ($pref_count removed)"
|
||||
|
||||
# Clean up temp file
|
||||
rm -f "$installed_bundles"
|
||||
|
||||
if [ "$found_orphaned" = false ]; then
|
||||
echo -e " ${GREEN}✓${NC} No orphaned files found"
|
||||
fi
|
||||
end_section
|
||||
|
||||
# Common temp and test data
|
||||
# Clean test data
|
||||
safe_clean ~/Library/Application\ Support/TestApp* "Test app data"
|
||||
safe_clean ~/Library/Application\ Support/MyApp/* "Test app data"
|
||||
safe_clean ~/Library/Application\ Support/GitHub*/* "GitHub test data"
|
||||
@@ -806,9 +681,11 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Application\ Support/TestNoValue/* "Test data"
|
||||
safe_clean ~/Library/Application\ Support/Wk*/* "Test data"
|
||||
|
||||
# ===== 5. Apple Silicon optimizations =====
|
||||
end_section
|
||||
|
||||
# ===== Apple Silicon optimizations =====
|
||||
if [[ "$IS_M_SERIES" == "true" ]]; then
|
||||
start_section "Apple Silicon cache cleanup"
|
||||
start_section "Apple Silicon optimizations"
|
||||
safe_clean /Library/Apple/usr/share/rosetta/rosetta_update_bundle "Rosetta 2 cache"
|
||||
safe_clean ~/Library/Caches/com.apple.rosetta.update "Rosetta 2 user cache"
|
||||
safe_clean ~/Library/Caches/com.apple.amp.mediasevicesd "Apple Silicon media service cache"
|
||||
@@ -818,54 +695,76 @@ perform_cleanup() {
|
||||
|
||||
# System cleanup was moved to the beginning (right after password verification)
|
||||
|
||||
# ===== 7. iOS device backups =====
|
||||
# ===== iOS device backups =====
|
||||
start_section "iOS device backups"
|
||||
backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
||||
if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then
|
||||
backup_kb=$(du -sk "$backup_dir" 2>/dev/null | awk '{print $1}')
|
||||
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then # >100MB
|
||||
backup_human=$(du -shm "$backup_dir" 2>/dev/null | awk '{print $1"M"}')
|
||||
backup_human=$(du -sh "$backup_dir" 2>/dev/null | awk '{print $1}')
|
||||
note_activity
|
||||
echo -e " 👉 Found ${GREEN}${backup_human}${NC}, you can delete it manually"
|
||||
echo -e " 👉 ${backup_dir}"
|
||||
else
|
||||
echo -e " ${BLUE}✨${NC} Nothing to tidy"
|
||||
echo -e " ${BLUE}💾${NC} Found ${GREEN}${backup_human}${NC} iOS backups"
|
||||
echo -e " ${YELLOW}💡${NC} You can delete them manually: ${backup_dir}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${BLUE}✨${NC} Nothing to tidy"
|
||||
fi
|
||||
end_section
|
||||
|
||||
# ===== 8. Summary =====
|
||||
start_section "Cleanup summary"
|
||||
note_activity
|
||||
# ===== Final summary =====
|
||||
space_after=$(df / | tail -1 | awk '{print $4}')
|
||||
current_space_after=$(get_free_space)
|
||||
|
||||
echo "==================================================================="
|
||||
space_freed_kb=$((space_after - space_before))
|
||||
if [[ $space_freed_kb -gt 0 ]]; then
|
||||
freed_gb=$(echo "$space_freed_kb" | awk '{printf "%.2f", $1/1024/1024}')
|
||||
echo -e "🎉 Cleanup complete | 💾 Freed space: ${GREEN}${freed_gb}GB${NC}"
|
||||
else
|
||||
echo "🎉 Cleanup complete"
|
||||
fi
|
||||
echo "📊 Items processed: $total_items | 💾 Free space now: $current_space_after"
|
||||
|
||||
if [[ "$IS_M_SERIES" == "true" ]]; then
|
||||
echo "✨ Apple Silicon optimizations finished"
|
||||
echo ""
|
||||
echo "===================================================================="
|
||||
echo "🎉 CLEANUP COMPLETE!"
|
||||
|
||||
if [[ $total_size_cleaned -gt 0 ]]; then
|
||||
local freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}')
|
||||
echo "💾 Space freed: ${GREEN}${freed_gb}GB${NC} | Free space now: $(get_free_space)"
|
||||
|
||||
# Add some context to make it more impressive
|
||||
if [[ $(echo "$freed_gb" | awk '{print ($1 >= 1) ? 1 : 0}') -eq 1 ]]; then
|
||||
local movies=$(echo "$freed_gb" | awk '{printf "%.0f", $1/4.5}')
|
||||
if [[ $movies -gt 0 ]]; then
|
||||
echo "🎬 That's like ~$movies 4K movies worth of space!"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "💾 No significant space was freed (system was already clean) | Free space: $(get_free_space)"
|
||||
fi
|
||||
|
||||
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
|
||||
echo "📊 Files cleaned: $files_cleaned | Categories processed: $total_items"
|
||||
elif [[ $files_cleaned -gt 0 ]]; then
|
||||
echo "📊 Files cleaned: $files_cleaned"
|
||||
elif [[ $total_items -gt 0 ]]; then
|
||||
echo "🗂️ Categories processed: $total_items"
|
||||
fi
|
||||
|
||||
if [[ "$SYSTEM_CLEAN" != "true" ]]; then
|
||||
echo ""
|
||||
echo -e "${BLUE}💡 Want deeper cleanup next time?${NC}"
|
||||
echo -e " Just enter your password when prompted for system-level cleaning"
|
||||
echo -e "${BLUE}💡 For deeper cleanup, run with admin password next time${NC}"
|
||||
fi
|
||||
|
||||
echo "==================================================================="
|
||||
end_section
|
||||
echo "===================================================================="
|
||||
}
|
||||
|
||||
# Cleanup function - restore cursor on exit
|
||||
cleanup() {
|
||||
# Restore cursor
|
||||
show_cursor
|
||||
# Kill any background processes
|
||||
if [[ -n "${SUDO_KEEPALIVE_PID:-}" ]]; then
|
||||
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${SPINNER_PID:-}" ]]; then
|
||||
kill "$SPINNER_PID" 2>/dev/null || true
|
||||
fi
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
# Set trap for cleanup on exit
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
main() {
|
||||
case "${1:-""}" in
|
||||
"--help"|"-h")
|
||||
@@ -876,11 +775,14 @@ main() {
|
||||
echo " --help, -h Show this help"
|
||||
echo ""
|
||||
echo "Interactive cleanup with smart password handling"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
hide_cursor
|
||||
start_cleanup
|
||||
perform_cleanup
|
||||
show_cursor
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
388
bin/install.sh
388
bin/install.sh
@@ -1,388 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Mole - Install Module
|
||||
# Interactive application installer using Homebrew
|
||||
#
|
||||
# Usage:
|
||||
# install.sh # Launch interactive installer
|
||||
# install.sh --help # Show help information
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get script directory and source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
|
||||
# Check if Homebrew is available
|
||||
check_homebrew() {
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
log_error "Homebrew is not installed"
|
||||
echo ""
|
||||
echo "To install Homebrew, run:"
|
||||
echo '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Application categories with descriptions
|
||||
declare -A APP_CATEGORIES=(
|
||||
["productivity"]="📝 Productivity Apps"
|
||||
["development"]="💻 Development Tools"
|
||||
["media"]="🎵 Media & Entertainment"
|
||||
["utilities"]="🔧 System Utilities"
|
||||
["communication"]="💬 Communication"
|
||||
["design"]="🎨 Design & Graphics"
|
||||
)
|
||||
|
||||
# Define applications by category
|
||||
declare -A APPS=(
|
||||
# Productivity
|
||||
["notion"]="productivity|Notion|All-in-one workspace for notes and docs"
|
||||
["obsidian"]="productivity|Obsidian|Knowledge management and note-taking"
|
||||
["raycast"]="productivity|Raycast|Launcher and productivity tool"
|
||||
["alfred"]="productivity|Alfred|Application launcher and productivity app"
|
||||
["1password"]="productivity|1Password|Password manager"
|
||||
|
||||
# Development
|
||||
["visual-studio-code"]="development|VS Code|Code editor by Microsoft"
|
||||
["docker"]="development|Docker|Containerization platform"
|
||||
["postman"]="development|Postman|API development and testing"
|
||||
["github-desktop"]="development|GitHub Desktop|Git client for GitHub"
|
||||
["figma"]="development|Figma|Design and prototyping tool"
|
||||
["iterm2"]="development|iTerm2|Terminal replacement"
|
||||
|
||||
# Media
|
||||
["vlc"]="media|VLC|Media player"
|
||||
["spotify"]="media|Spotify|Music streaming"
|
||||
["handbrake"]="media|HandBrake|Video transcoder"
|
||||
["obs"]="media|OBS Studio|Live streaming and recording"
|
||||
|
||||
# Utilities
|
||||
["the-unarchiver"]="utilities|The Unarchiver|Archive utility"
|
||||
["appcleaner"]="utilities|AppCleaner|Uninstall applications completely"
|
||||
["cleanmymac"]="utilities|CleanMyMac X|System cleaning and optimization"
|
||||
["bartender-4"]="utilities|Bartender 4|Menu bar organization"
|
||||
|
||||
# Communication
|
||||
["discord"]="communication|Discord|Voice and text chat"
|
||||
["slack"]="communication|Slack|Team communication"
|
||||
["telegram"]="communication|Telegram|Messaging app"
|
||||
["zoom"]="communication|Zoom|Video conferencing"
|
||||
|
||||
# Design
|
||||
["sketch"]="design|Sketch|Digital design toolkit"
|
||||
["adobe-creative-cloud"]="design|Adobe CC|Creative suite"
|
||||
["blender"]="design|Blender|3D creation suite"
|
||||
)
|
||||
|
||||
# Initialize global variables
|
||||
declare -a selected_apps=()
|
||||
declare -a filtered_apps=()
|
||||
current_category="all"
|
||||
current_line=0
|
||||
|
||||
# Help information
|
||||
show_help() {
|
||||
echo "Mole - Interactive App Installer"
|
||||
echo "================================="
|
||||
echo ""
|
||||
echo "Description: Install useful applications using Homebrew Cask"
|
||||
echo ""
|
||||
echo "Features:"
|
||||
echo " • Browse apps by category"
|
||||
echo " • Navigate with ↑/↓ arrow keys"
|
||||
echo " • Select/deselect apps with SPACE"
|
||||
echo " • Filter by category with 1-6 keys"
|
||||
echo " • Install selected apps with ENTER"
|
||||
echo " • Quit anytime with 'q'"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " ./install.sh Launch interactive installer"
|
||||
echo " ./install.sh --help Show this help message"
|
||||
echo ""
|
||||
echo "Requirements:"
|
||||
echo " • Homebrew must be installed"
|
||||
echo " • Internet connection for downloads"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Filter apps by category
|
||||
filter_apps_by_category() {
|
||||
local category="$1"
|
||||
filtered_apps=()
|
||||
|
||||
for app_key in "${!APPS[@]}"; do
|
||||
IFS='|' read -r app_category app_name app_desc <<< "${APPS[$app_key]}"
|
||||
if [[ "$category" == "all" || "$app_category" == "$category" ]]; then
|
||||
filtered_apps+=("$app_key|$app_category|$app_name|$app_desc")
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort alphabetically by name
|
||||
IFS=$'\n' filtered_apps=($(sort -t'|' -k3 <<<"${filtered_apps[*]}"))
|
||||
unset IFS
|
||||
}
|
||||
|
||||
# Display application list
|
||||
display_apps() {
|
||||
clear
|
||||
echo "📦 Mole - Interactive App Installer"
|
||||
echo "══════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Show category filter
|
||||
local category_name="All Applications"
|
||||
case "$current_category" in
|
||||
"productivity") category_name="${APP_CATEGORIES[productivity]}" ;;
|
||||
"development") category_name="${APP_CATEGORIES[development]}" ;;
|
||||
"media") category_name="${APP_CATEGORIES[media]}" ;;
|
||||
"utilities") category_name="${APP_CATEGORIES[utilities]}" ;;
|
||||
"communication") category_name="${APP_CATEGORIES[communication]}" ;;
|
||||
"design") category_name="${APP_CATEGORIES[design]}" ;;
|
||||
esac
|
||||
|
||||
echo -e "${PURPLE}Category: $category_name${NC}"
|
||||
echo -e "${PURPLE}Showing ${#filtered_apps[@]} applications${NC}"
|
||||
echo ""
|
||||
|
||||
# Display apps (max 15 per page)
|
||||
local start_idx=0
|
||||
local end_idx=$((${#filtered_apps[@]} - 1))
|
||||
local max_display=15
|
||||
|
||||
if [[ $end_idx -gt $((max_display - 1)) ]]; then
|
||||
end_idx=$((max_display - 1))
|
||||
fi
|
||||
|
||||
for ((i=start_idx; i<=end_idx && i<${#filtered_apps[@]}; i++)); do
|
||||
IFS='|' read -r app_key app_category app_name app_desc <<< "${filtered_apps[i]}"
|
||||
|
||||
local prefix=" "
|
||||
local line_color="$NC"
|
||||
local name_color="$NC"
|
||||
|
||||
# Current selection highlighting
|
||||
if [[ $i -eq $current_line ]]; then
|
||||
prefix="▶ "
|
||||
line_color="$BLUE"
|
||||
name_color="$BLUE"
|
||||
fi
|
||||
|
||||
# Check if app is selected
|
||||
local checkbox="[ ]"
|
||||
local checkbox_color="$NC"
|
||||
for selected in "${selected_apps[@]}"; do
|
||||
if [[ "$selected" == "$app_key" ]]; then
|
||||
checkbox="[✓]"
|
||||
checkbox_color="$GREEN"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Format display
|
||||
printf "${line_color}${prefix}${checkbox_color}${checkbox}${NC} "
|
||||
printf "${name_color}%-25s${NC} " "$app_name"
|
||||
printf "│ %s\n" "$app_desc"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
|
||||
# Show selection summary
|
||||
local selected_count=${#selected_apps[@]}
|
||||
if [[ $selected_count -eq 0 ]]; then
|
||||
echo -e "${BLUE}📋 No applications selected${NC}"
|
||||
else
|
||||
echo -e "${GREEN}📋 Selected: $selected_count applications${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Show category filters
|
||||
echo -e "${PURPLE}🏷️ Categories:${NC}"
|
||||
echo " 0 All 1 Productivity 2 Development 3 Media 4 Utilities 5 Communication 6 Design"
|
||||
echo ""
|
||||
|
||||
# Controls
|
||||
echo -e "${PURPLE}🎮 Controls:${NC}"
|
||||
echo " ↑/↓ Navigate SPACE Select 0-6 Filter ENTER Install ? Help q Quit"
|
||||
}
|
||||
|
||||
# Interactive app selection
|
||||
interactive_app_selection() {
|
||||
filter_apps_by_category "$current_category"
|
||||
current_line=0
|
||||
|
||||
while true; do
|
||||
display_apps
|
||||
|
||||
# Read key input
|
||||
read -rsn1 key
|
||||
|
||||
case "$key" in
|
||||
$'\x1b') # ESC sequences
|
||||
read -rsn2 key
|
||||
case "$key" in
|
||||
'[A') # Up arrow
|
||||
((current_line > 0)) && ((current_line--))
|
||||
;;
|
||||
'[B') # Down arrow
|
||||
((current_line < ${#filtered_apps[@]} - 1)) && ((current_line++))
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
' ') # Space - toggle selection
|
||||
if [[ ${#filtered_apps[@]} -gt 0 && $current_line -lt ${#filtered_apps[@]} ]]; then
|
||||
IFS='|' read -r app_key app_category app_name app_desc <<< "${filtered_apps[current_line]}"
|
||||
|
||||
# Check if already selected
|
||||
local found=false
|
||||
for i in "${!selected_apps[@]}"; do
|
||||
if [[ "${selected_apps[i]}" == "$app_key" ]]; then
|
||||
unset 'selected_apps[i]'
|
||||
selected_apps=("${selected_apps[@]}") # Re-index array
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$found" == "false" ]]; then
|
||||
selected_apps+=("$app_key")
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
$'\n'|$'\r') # Enter - proceed to installation
|
||||
if [[ ${#selected_apps[@]} -gt 0 ]]; then
|
||||
break
|
||||
fi
|
||||
;;
|
||||
'q'|'Q') # Quit
|
||||
log_info "Installation cancelled"
|
||||
return 1
|
||||
;;
|
||||
[0-6]) # Category filters
|
||||
case "$key" in
|
||||
'0') current_category="all" ;;
|
||||
'1') current_category="productivity" ;;
|
||||
'2') current_category="development" ;;
|
||||
'3') current_category="media" ;;
|
||||
'4') current_category="utilities" ;;
|
||||
'5') current_category="communication" ;;
|
||||
'6') current_category="design" ;;
|
||||
esac
|
||||
filter_apps_by_category "$current_category"
|
||||
current_line=0
|
||||
;;
|
||||
'a'|'A') # Select all visible
|
||||
for app_data in "${filtered_apps[@]}"; do
|
||||
IFS='|' read -r app_key app_category app_name app_desc <<< "$app_data"
|
||||
|
||||
# Check if already selected
|
||||
local found=false
|
||||
for selected in "${selected_apps[@]}"; do
|
||||
if [[ "$selected" == "$app_key" ]]; then
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$found" == "false" ]]; then
|
||||
selected_apps+=("$app_key")
|
||||
fi
|
||||
done
|
||||
;;
|
||||
'n'|'N') # Select none
|
||||
selected_apps=()
|
||||
;;
|
||||
'?') # Help
|
||||
show_help
|
||||
echo ""
|
||||
read -p "Press any key to continue..." -n 1 -r
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Install selected applications
|
||||
install_applications() {
|
||||
log_header "Installing selected applications"
|
||||
|
||||
echo "You selected ${#selected_apps[@]} application(s) for installation:"
|
||||
echo ""
|
||||
|
||||
for app_key in "${selected_apps[@]}"; do
|
||||
IFS='|' read -r app_category app_name app_desc <<< "${APPS[$app_key]}"
|
||||
echo " • $app_name - $app_desc"
|
||||
done
|
||||
|
||||
echo ""
|
||||
read -p "Continue with installation? (y/N): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
log_info "Starting installation..."
|
||||
echo ""
|
||||
|
||||
local successful=0
|
||||
local failed=0
|
||||
|
||||
for app_key in "${selected_apps[@]}"; do
|
||||
IFS='|' read -r app_category app_name app_desc <<< "${APPS[$app_key]}"
|
||||
|
||||
echo -e "${BLUE}Installing $app_name...${NC}"
|
||||
|
||||
if brew install --cask "$app_key" 2>/dev/null; then
|
||||
echo -e " ${GREEN}✓${NC} $app_name installed successfully"
|
||||
((successful++))
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Failed to install $app_name"
|
||||
((failed++))
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo "══════════════════════════════════════════════════════════════════════"
|
||||
log_success "Installation complete!"
|
||||
echo "📊 Successfully installed: $successful applications"
|
||||
if [[ $failed -gt 0 ]]; then
|
||||
echo "⚠️ Failed to install: $failed applications"
|
||||
fi
|
||||
else
|
||||
log_info "Installation cancelled"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
echo "📦 Mole - Interactive App Installer"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
|
||||
# Check Homebrew
|
||||
check_homebrew
|
||||
|
||||
log_info "Checking Homebrew installation..."
|
||||
echo ""
|
||||
|
||||
# Interactive selection
|
||||
if ! interactive_app_selection; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
clear
|
||||
install_applications
|
||||
|
||||
log_success "App installer finished"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
263
bin/uninstall.sh
263
bin/uninstall.sh
@@ -11,41 +11,24 @@ set -euo pipefail
|
||||
# Get script directory and source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
source "$SCRIPT_DIR/../lib/menu.sh"
|
||||
source "$SCRIPT_DIR/../lib/paginated_menu.sh"
|
||||
source "$SCRIPT_DIR/../lib/app_selector.sh"
|
||||
source "$SCRIPT_DIR/../lib/batch_uninstall.sh"
|
||||
|
||||
# Basic preserved bundle patterns
|
||||
PRESERVED_BUNDLE_PATTERNS=(
|
||||
"com.apple.*"
|
||||
"com.nektony.*"
|
||||
)
|
||||
|
||||
# Check if bundle should be preserved (system apps)
|
||||
should_preserve_bundle() {
|
||||
local bundle_id="$1"
|
||||
for pattern in "${PRESERVED_BUNDLE_PATTERNS[@]}"; do
|
||||
if [[ "$bundle_id" == $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# Note: Bundle preservation logic is now in lib/common.sh
|
||||
|
||||
# Help information
|
||||
show_help() {
|
||||
echo "Mole - Interactive App Uninstaller"
|
||||
echo "========================================"
|
||||
echo "App Uninstaller"
|
||||
echo "==============="
|
||||
echo ""
|
||||
echo "Description: Interactive tool to uninstall applications and clean their data"
|
||||
echo "Uninstall applications and clean their data completely."
|
||||
echo ""
|
||||
echo "Features:"
|
||||
echo " • Navigate with ↑/↓ arrow keys"
|
||||
echo " • Select/deselect apps with SPACE"
|
||||
echo " • Confirm selection with ENTER"
|
||||
echo " • Quit anytime with 'q'"
|
||||
echo " • Apps sorted by last usage time"
|
||||
echo " • Comprehensive cleanup of app data"
|
||||
echo "Controls:"
|
||||
echo " ↑/↓ Navigate"
|
||||
echo " SPACE Select/deselect"
|
||||
echo " ENTER Confirm"
|
||||
echo " Q Quit"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " ./uninstall.sh Launch interactive uninstaller"
|
||||
@@ -69,7 +52,7 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
fi
|
||||
|
||||
# Initialize global variables
|
||||
declare -a selected_apps=()
|
||||
selected_apps=() # Global array for app selection
|
||||
declare -a apps_data=()
|
||||
declare -a selection_state=()
|
||||
current_line=0
|
||||
@@ -109,7 +92,7 @@ get_app_last_used() {
|
||||
scan_applications() {
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
echo -n "Scanning applications... " >&2
|
||||
echo -n "Scanning... " >&2
|
||||
|
||||
# Pre-cache current epoch to avoid repeated calls
|
||||
local current_epoch=$(date "+%s")
|
||||
@@ -121,10 +104,80 @@ scan_applications() {
|
||||
|
||||
local app_name=$(basename "$app_path" .app)
|
||||
|
||||
# Quick bundle ID check first (only if plist exists)
|
||||
# Try to get English name from bundle info, fallback to folder name
|
||||
local bundle_id="unknown"
|
||||
local display_name="$app_name"
|
||||
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
||||
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown")
|
||||
|
||||
# Try to get English name from bundle info
|
||||
local bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null)
|
||||
|
||||
# Smart display name selection - prefer descriptive names over generic ones
|
||||
local candidates=()
|
||||
|
||||
# Get all potential names
|
||||
local bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||
local bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||
|
||||
# Check if executable name is generic/technical (should be avoided)
|
||||
local is_generic_executable=false
|
||||
if [[ -n "$bundle_executable" ]]; then
|
||||
case "$bundle_executable" in
|
||||
"pake"|"Electron"|"electron"|"nwjs"|"node"|"helper"|"main"|"app"|"binary")
|
||||
is_generic_executable=true
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Priority order for name selection:
|
||||
# 1. App folder name (if ASCII and descriptive) - often the most complete name
|
||||
if [[ "$app_name" =~ ^[A-Za-z0-9\ ._-]+$ && ${#app_name} -gt 3 ]]; then
|
||||
candidates+=("$app_name")
|
||||
fi
|
||||
|
||||
# 2. CFBundleDisplayName (if meaningful and ASCII)
|
||||
if [[ -n "$bundle_display_name" && "$bundle_display_name" =~ ^[A-Za-z0-9\ ._-]+$ ]]; then
|
||||
candidates+=("$bundle_display_name")
|
||||
fi
|
||||
|
||||
# 3. CFBundleName (if meaningful and ASCII)
|
||||
if [[ -n "$bundle_name" && "$bundle_name" =~ ^[A-Za-z0-9\ ._-]+$ && "$bundle_name" != "$bundle_display_name" ]]; then
|
||||
candidates+=("$bundle_name")
|
||||
fi
|
||||
|
||||
# 4. CFBundleExecutable (only if not generic and ASCII)
|
||||
if [[ -n "$bundle_executable" && "$bundle_executable" =~ ^[A-Za-z0-9._-]+$ && "$is_generic_executable" == false ]]; then
|
||||
candidates+=("$bundle_executable")
|
||||
fi
|
||||
|
||||
# 5. Fallback to non-ASCII names if no ASCII found
|
||||
if [[ ${#candidates[@]} -eq 0 ]]; then
|
||||
[[ -n "$bundle_display_name" ]] && candidates+=("$bundle_display_name")
|
||||
[[ -n "$bundle_name" && "$bundle_name" != "$bundle_display_name" ]] && candidates+=("$bundle_name")
|
||||
candidates+=("$app_name")
|
||||
fi
|
||||
|
||||
# Select the first (best) candidate
|
||||
display_name="${candidates[0]:-$app_name}"
|
||||
|
||||
# Brand name mapping for better user recognition (post-process)
|
||||
case "$display_name" in
|
||||
"qiyimac"|"爱奇艺") display_name="iQiyi" ;;
|
||||
"wechat"|"微信") display_name="WeChat" ;;
|
||||
"QQ"|"QQ") display_name="QQ" ;;
|
||||
"VooV Meeting"|"腾讯会议") display_name="VooV Meeting" ;;
|
||||
"dingtalk"|"钉钉") display_name="DingTalk" ;;
|
||||
"NeteaseMusic"|"网易云音乐") display_name="NetEase Music" ;;
|
||||
"BaiduNetdisk"|"百度网盘") display_name="Baidu NetDisk" ;;
|
||||
"alipay"|"支付宝") display_name="Alipay" ;;
|
||||
"taobao"|"淘宝") display_name="Taobao" ;;
|
||||
"futunn"|"富途牛牛") display_name="Futu NiuNiu" ;;
|
||||
"tencent lemon"|"Tencent Lemon Cleaner") display_name="Tencent Lemon" ;;
|
||||
"keynote"|"Keynote") display_name="Keynote" ;;
|
||||
"pages"|"Pages") display_name="Pages" ;;
|
||||
"numbers"|"Numbers") display_name="Numbers" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Skip protected system apps early
|
||||
@@ -132,8 +185,8 @@ scan_applications() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# Store tuple: app_path|app_name|bundle_id
|
||||
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}")
|
||||
# Store tuple: app_path|app_name|bundle_id|display_name
|
||||
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
|
||||
done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null)
|
||||
|
||||
# Second pass: process each app with accurate size calculation
|
||||
@@ -141,12 +194,12 @@ scan_applications() {
|
||||
local total_apps=${#app_data_tuples[@]}
|
||||
|
||||
for app_data_tuple in "${app_data_tuples[@]}"; do
|
||||
IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple"
|
||||
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
|
||||
|
||||
# Show progress every few items
|
||||
((app_count++))
|
||||
if (( app_count % 3 == 0 )) || [[ $app_count -eq $total_apps ]]; then
|
||||
echo -ne "\rScanning applications... processing $app_count/$total_apps apps" >&2
|
||||
if (( app_count % 5 == 0 )) || [[ $app_count -eq $total_apps ]]; then
|
||||
echo -ne "\rScanning... $app_count/$total_apps" >&2
|
||||
fi
|
||||
|
||||
# Accurate size calculation - this is what takes time but user wants it
|
||||
@@ -155,27 +208,70 @@ scan_applications() {
|
||||
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
||||
fi
|
||||
|
||||
# Simplified last used check using file modification time
|
||||
local last_used="Old"
|
||||
# Get real last used date from macOS metadata
|
||||
local last_used="Never"
|
||||
local last_used_epoch=0
|
||||
|
||||
if [[ -d "$app_path" ]]; then
|
||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
if [[ $days_ago -lt 30 ]]; then
|
||||
last_used="Recent"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
last_used="This year"
|
||||
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||
|
||||
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
||||
# Convert macOS date format to epoch
|
||||
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0")
|
||||
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
|
||||
if [[ $days_ago -eq 0 ]]; then
|
||||
last_used="Today"
|
||||
elif [[ $days_ago -eq 1 ]]; then
|
||||
last_used="Yesterday"
|
||||
elif [[ $days_ago -lt 7 ]]; then
|
||||
last_used="${days_ago} days ago"
|
||||
elif [[ $days_ago -lt 30 ]]; then
|
||||
local weeks_ago=$(( days_ago / 7 ))
|
||||
if [[ $weeks_ago -eq 1 ]]; then
|
||||
last_used="1 week ago"
|
||||
else
|
||||
last_used="${weeks_ago} weeks ago"
|
||||
fi
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
local months_ago=$(( days_ago / 30 ))
|
||||
if [[ $months_ago -eq 1 ]]; then
|
||||
last_used="1 month ago"
|
||||
else
|
||||
last_used="${months_ago} months ago"
|
||||
fi
|
||||
else
|
||||
local years_ago=$(( days_ago / 365 ))
|
||||
if [[ $years_ago -eq 1 ]]; then
|
||||
last_used="1 year ago"
|
||||
else
|
||||
last_used="${years_ago} years ago"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Fallback to file modification time if no usage metadata
|
||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
if [[ $days_ago -lt 30 ]]; then
|
||||
last_used="Recent"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
last_used="This year"
|
||||
else
|
||||
last_used="Old"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Format: epoch|app_path|app_name|bundle_id|size|last_used_display
|
||||
echo "${last_used_epoch}|${app_path}|${app_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
||||
# Format: epoch|app_path|display_name|bundle_id|size|last_used_display
|
||||
echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
||||
done
|
||||
|
||||
echo -e "\rScanning applications... found $app_count apps ✓" >&2
|
||||
echo -e "\rFound $app_count applications ✓" >&2
|
||||
|
||||
# Check if we found any applications
|
||||
if [[ ! -s "$temp_file" ]]; then
|
||||
@@ -221,57 +317,8 @@ load_applications() {
|
||||
# Read a single key with proper escape sequence handling
|
||||
# This function has been replaced by the menu.sh library
|
||||
|
||||
# Old interactive_app_selection and show_selection_help functions removed
|
||||
# They have been replaced by the new menu system in lib/app_selector.sh
|
||||
|
||||
# Find and list app-related files
|
||||
find_app_files() {
|
||||
local bundle_id="$1"
|
||||
local app_name="$2"
|
||||
local -a files_to_clean=()
|
||||
|
||||
# Application Support
|
||||
[[ -d ~/Library/Application\ Support/"$app_name" ]] && files_to_clean+=("$HOME/Library/Application Support/$app_name")
|
||||
[[ -d ~/Library/Application\ Support/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Application Support/$bundle_id")
|
||||
|
||||
# Caches
|
||||
[[ -d ~/Library/Caches/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Caches/$bundle_id")
|
||||
|
||||
# Preferences
|
||||
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
||||
|
||||
# Logs
|
||||
[[ -d ~/Library/Logs/"$app_name" ]] && files_to_clean+=("$HOME/Library/Logs/$app_name")
|
||||
[[ -d ~/Library/Logs/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Logs/$bundle_id")
|
||||
|
||||
# Saved Application State
|
||||
[[ -d ~/Library/Saved\ Application\ State/"$bundle_id".savedState ]] && files_to_clean+=("$HOME/Library/Saved Application State/$bundle_id.savedState")
|
||||
|
||||
# Containers (sandboxed apps)
|
||||
[[ -d ~/Library/Containers/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Containers/$bundle_id")
|
||||
|
||||
# Group Containers
|
||||
while IFS= read -r -d '' container; do
|
||||
files_to_clean+=("$container")
|
||||
done < <(find ~/Library/Group\ Containers -name "*$bundle_id*" -type d -print0 2>/dev/null)
|
||||
|
||||
printf '%s\n' "${files_to_clean[@]}"
|
||||
}
|
||||
|
||||
# Calculate total size of files
|
||||
calculate_total_size() {
|
||||
local files="$1"
|
||||
local total_kb=0
|
||||
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" && -e "$file" ]]; then
|
||||
local size_kb=$(du -sk "$file" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
((total_kb += size_kb))
|
||||
fi
|
||||
done <<< "$files"
|
||||
|
||||
echo "$total_kb"
|
||||
}
|
||||
# Note: App file discovery and size calculation functions moved to lib/common.sh
|
||||
# Use find_app_files() and calculate_total_size() from common.sh
|
||||
|
||||
# Uninstall selected applications
|
||||
uninstall_applications() {
|
||||
@@ -384,8 +431,8 @@ uninstall_applications() {
|
||||
|
||||
# Cleanup function - restore cursor and clean up
|
||||
cleanup() {
|
||||
# Restore cursor
|
||||
printf '\033[?25h'
|
||||
# Restore cursor using common function
|
||||
show_cursor
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
@@ -394,10 +441,9 @@ trap cleanup EXIT INT TERM
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
echo "🗑️ Mole - Interactive App Uninstaller"
|
||||
echo "============================================"
|
||||
echo
|
||||
|
||||
# Hide cursor during operation
|
||||
hide_cursor
|
||||
|
||||
# Scan applications
|
||||
local apps_file=$(scan_applications)
|
||||
|
||||
@@ -412,17 +458,16 @@ main() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Interactive selection using new menu system
|
||||
# Interactive selection using paginated menu
|
||||
if ! select_apps_for_uninstall; then
|
||||
rm -f "$apps_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Restore cursor for normal interaction
|
||||
printf '\033[?25h'
|
||||
# Restore cursor for normal interaction after selection
|
||||
show_cursor
|
||||
clear
|
||||
echo "You selected ${#selected_apps[@]} application(s) for uninstallation:"
|
||||
echo ""
|
||||
|
||||
if [[ ${#selected_apps[@]} -gt 0 ]]; then
|
||||
for selected_app in "${selected_apps[@]}"; do
|
||||
@@ -439,8 +484,6 @@ main() {
|
||||
|
||||
# Cleanup
|
||||
rm -f "$apps_file"
|
||||
|
||||
log_success "App uninstaller finished"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
|
||||
Reference in New Issue
Block a user