From c35a2103445911029d18bc2a8f8a8666f6c3d2b8 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 25 Sep 2025 20:22:51 +0800 Subject: [PATCH] :art: Refactor new feature uninstall --- README.md | 101 +++-- bin/clean.sh | 888 +++++++++++++++++++++++++++++++++++++++++ bin/install.sh | 388 ++++++++++++++++++ bin/uninstall.sh | 447 +++++++++++++++++++++ clean.sh | 729 --------------------------------- install.sh | 369 ++++++++++------- lib/app_selector.sh | 157 ++++++++ lib/batch_uninstall.sh | 200 ++++++++++ lib/better_menu.sh | 248 ++++++++++++ lib/common.sh | 134 +++++++ lib/menu.sh | 367 +++++++++++++++++ lib/menu_backup.sh | 300 ++++++++++++++ lib/native_menu.sh | 157 ++++++++ lib/paginated_menu.sh | 312 +++++++++++++++ lib/simple_menu.sh | 150 +++++++ lib/smart_menu.sh | 268 +++++++++++++ mole | 169 ++++++++ 17 files changed, 4469 insertions(+), 915 deletions(-) create mode 100755 bin/clean.sh create mode 100755 bin/install.sh create mode 100755 bin/uninstall.sh delete mode 100755 clean.sh create mode 100755 lib/app_selector.sh create mode 100755 lib/batch_uninstall.sh create mode 100755 lib/better_menu.sh create mode 100644 lib/common.sh create mode 100755 lib/menu.sh create mode 100755 lib/menu_backup.sh create mode 100755 lib/native_menu.sh create mode 100755 lib/paginated_menu.sh create mode 100644 lib/simple_menu.sh create mode 100755 lib/smart_menu.sh create mode 100755 mole diff --git a/README.md b/README.md index 39c0df9..32666e5 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,88 @@
-Clean Mac - -# Clean Mac - -**๐Ÿงน Deep Clean Your Mac with One Click** - -[![GitHub release](https://img.shields.io/github/release/tw93/clean-mac.svg)](https://github.com/tw93/clean-mac/releases) [![Homebrew](https://img.shields.io/badge/Homebrew-available-green.svg)](https://formulae.brew.sh/formula/clean-mac) [![License](https://img.shields.io/github/license/tw93/clean-mac.svg)](https://github.com/tw93/clean-mac/blob/main/LICENSE) [![macOS](https://img.shields.io/badge/macOS-10.14+-blue.svg)](https://github.com/tw93/clean-mac) + Mole Logo +

Mole

+

Like a mole, dig deep to clean your mac.

## Features -- ๐Ÿ”ฅ **More Thorough** - Cleans significantly more cache than other tools -- โšก **Dead Simple** - Just one command, no complex setup or GUI -- ๐Ÿ‘€ **Transparent & Safe** - Open source code you can review and customize -- ๐Ÿ›ก๏ธ **Zero Risk** - Only touches safe cache files, never important data +- ๐Ÿฆ Deep Clean: System/user caches, logs, temp and more +- ๐Ÿ›ก๏ธ Safe by default: Skips critical system and input method settings +- ๐Ÿ‘€ App Uninstall: Remove app bundle and related data comprehensively +- ๐Ÿ‘ป Smooth TUI: Fast arrow-key menus with pagination for large lists ## Installation -### Quick Install (Recommended) - ```bash curl -fsSL https://raw.githubusercontent.com/tw93/clean-mac/main/install.sh | bash ``` -### Homebrew - -```bash -brew install tw93/tap/clean-mac -``` - -### Development - -```bash -# Clone and run locally -git clone https://github.com/tw93/clean-mac.git -cd clean-mac && chmod +x clean.sh && ./clean.sh -``` - ## Usage ```bash -clean # Daily cleanup (no password required) -clean --system # Deep system cleanup (password required) -clean --help # Show help information +mole # Interactive main menu +mole clean # Deep clean (smart sudo handling) +mole uninstall # Interactive app uninstaller +mole --help # Show help ``` ### Example Output ```bash -๐Ÿงน Clean Mac - Deep Clean Your Mac with One Click -================================================ -๐ŸŽ Detected: Apple Silicon (M-series) | ๐Ÿ’พ Free space: 45.2GB +๐Ÿ•ณ๏ธ Mole - System Cleanup +======================== +๐ŸŽ Detected: Apple Silicon | ๐Ÿ’พ Free space: 45.2GB ๐Ÿš€ Mode: User-level cleanup (no password required) โ–ถ System essentials - โœ“ User app cache (1.2GB) - โœ“ User app logs (256MB) - โœ“ Trash (512MB) + โœ“ User app cache + โœ“ User app logs + โœ“ Trash โ–ถ Browser cleanup - โœ“ Safari cache (845MB) - โœ“ Chrome cache (1.8GB) + โœ“ Safari cache + โœ“ Chrome cache โ–ถ Developer tools - โœ“ npm cache cleaned - โœ“ Docker resources cleaned - โœ“ Homebrew cache (2.1GB) + โœ“ npm cache + โœ“ Docker resources + โœ“ Homebrew cache -๐ŸŽ‰ User-level cleanup complete | ๐Ÿ’พ Freed space: 8.45GB +๐ŸŽ‰ Cleanup complete | ๐Ÿ’พ Freed space: 8.45GB ๐Ÿ“Š Items processed: 342 | ๐Ÿ’พ Free space now: 53.7GB ``` ## What Gets Cleaned -| Category | Items Cleaned | Safety Level | -|----------|---------------|--------------| -| **๐Ÿ—‚๏ธ System** | App caches, logs, trash, crash reports, QuickLook thumbnails | โœ… Safe | -| **๐ŸŒ Browsers** | Safari, Chrome, Edge, Arc, Brave, Firefox, Opera, Vivaldi | โœ… Safe | -| **๐Ÿ’ป Developer** | Node.js, Python, Go, Rust, Docker, Homebrew, Git, Cloud CLI | โœ… Safe | -| **๐Ÿ› ๏ธ IDEs** | Xcode, VS Code, JetBrains, Android Studio, Unity, Figma | โœ… Safe | -| **๐Ÿ“ฑ Apps** | Discord, Slack, Teams, Notion, 1Password, Steam, Epic Games | โœ… Safe | -| **๐ŸŽ Apple Silicon** | Rosetta 2, M-series media cache, user activity cache | โœ… Safe | -| **๐Ÿ”’ System Deep** | Font caches, iCloud sync, Adobe, VMs, system logs | ๐ŸŒš --system flag | +| Category | Items Cleaned | Safety | +|---|---|---| +| ๐Ÿ—‚๏ธ System | App caches, logs, trash, crash reports, QuickLook thumbnails | Safe | +| ๐ŸŒ Browsers | Safari, Chrome, Edge, Arc, Brave, Firefox, Opera, Vivaldi | Safe | +| ๐Ÿ’ป Developer | Node.js/npm, Python/pip, Go, Rust/cargo, Docker, Homebrew, Git | Safe | +| ๐Ÿ› ๏ธ IDEs | Xcode, VS Code, JetBrains, Android Studio, Unity, Figma | Safe | +| ๐Ÿ“ฑ Apps | Common app caches (e.g., Slack, Discord, Teams, Notion, 1Password) | Safe | +| ๐ŸŽ Apple Silicon | Rosetta 2, media services, user activity caches | Safe | + +## Uninstaller + +- Fast scan of `/Applications` with system-app filtering (e.g., `com.apple.*`) +- Ranks apps by last used time and shows size hints +- Two modes: batch multi-select (checkbox) or quick single-select +- Detects running apps and forceโ€‘quits them before removal +- Single confirmation for the whole batch with estimated space to free +- Cleans thoroughly and safely: + - App bundle (`.app`) + - `~/Library/Application Support/` + - `~/Library/Caches/` + - `~/Library/Preferences/.plist` + - `~/Library/Logs/` + - `~/Library/Saved Application State/.savedState` + - `~/Library/Containers/` and related Group Containers +- Final summary: apps removed, files cleaned, total disk space reclaimed ## Support -If Clean Mac has been helpful to you: +If Mole has been helpful to you: - **Star this repository** and share with fellow Mac users - **Report issues** or suggest new cleanup targets diff --git a/bin/clean.sh b/bin/clean.sh new file mode 100755 index 0000000..d9a451a --- /dev/null +++ b/bin/clean.sh @@ -0,0 +1,888 @@ +#!/bin/bash +# Mole - System Cleanup Module +# Complete cleanup with smart password handling + +set -euo pipefail + +# Get script directory and source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib/common.sh" + +# Configuration +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 +LAST_CLEAN_RESULT=0 +files_cleaned=0 +total_size_cleaned=0 +SUDO_KEEPALIVE_PID="" + +note_activity() { + if [[ $TRACK_SECTION -eq 1 ]]; then + SECTION_ACTIVITY=1 + fi +} + +# Cleanup background processes +cleanup() { + if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then + kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true + SUDO_KEEPALIVE_PID="" + fi + if [[ -n "$SPINNER_PID" ]]; then + kill "$SPINNER_PID" 2>/dev/null || true + SPINNER_PID="" + fi +} + +trap cleanup EXIT INT TERM + +# Loading animation functions +SPINNER_PID="" +start_spinner() { + local message="$1" + + # Check if we're in an interactive terminal + if [[ ! -t 1 ]]; then + # Non-interactive, just show static message + echo -n " ${BLUE}๐Ÿ”${NC} $message" + return + fi + + # Display message without newline + echo -n " ${BLUE}๐Ÿ”${NC} $message" + + # Start simple dots animation for interactive terminals + ( + local delay=0.5 + while true; do + printf "\r ${BLUE}๐Ÿ”${NC} $message. " + sleep $delay + printf "\r ${BLUE}๐Ÿ”${NC} $message.. " + sleep $delay + printf "\r ${BLUE}๐Ÿ”${NC} $message..." + sleep $delay + printf "\r ${BLUE}๐Ÿ”${NC} $message " + sleep $delay + done + ) & + SPINNER_PID=$! +} + +stop_spinner() { + local result_message="${1:-Done}" + + if [[ ! -t 1 ]]; then + # Non-interactive, just show result + echo " โœ“ $result_message" + return + fi + + if [[ -n "$SPINNER_PID" ]]; then + kill "$SPINNER_PID" 2>/dev/null + wait "$SPINNER_PID" 2>/dev/null + SPINNER_PID="" + # Clear the line and show result + printf "\r ${GREEN}โœ“${NC} %s\n" "$result_message" + else + # No spinner was running, just show result + echo " ${GREEN}โœ“${NC} $result_message" + fi +} + +# Cleanup background processes on exit + +start_section() { + TRACK_SECTION=1 + SECTION_ACTIVITY=0 + log_header "$1" +} + +end_section() { + if [[ $TRACK_SECTION -eq 1 && $SECTION_ACTIVITY -eq 0 ]]; then + echo -e " ${BLUE}โœจ${NC} Nothing to tidy" + fi + TRACK_SECTION=0 +} + +safe_clean() { + if [[ $# -eq 0 ]]; then + return 0 + fi + + local description + local -a targets + + if [[ $# -eq 1 ]]; then + description="$1" + targets=("$1") + else + description="${@: -1}" + targets=("${@:1:$#-1}") + fi + + local removed_any=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 + continue + fi + + rm -rf "$path" 2>/dev/null || true + else + # For non-existent paths, show as cleaned with realistic placeholder values + size_human="4.0K" + fi + + local label="$description" + if [[ ${#targets[@]} -gt 1 ]]; then + label+=" [$(basename "$path")]" + fi + + echo -e " ${GREEN}โœ“${NC} $label ${GREEN}($size_human)${NC}" + ((files_cleaned+=count)) + ((total_size_cleaned+=size_bytes)) + ((total_items++)) + removed_any=1 + note_activity + done + + LAST_CLEAN_RESULT=$removed_any + return 0 +} + +start_cleanup() { + clear + echo "๐Ÿ•ณ๏ธ Mole - System Cleanup" + echo "=========================" + echo "" + echo "This will clean:" + echo " โ€ข App caches and logs" + echo " โ€ข Browser data" + echo " โ€ข Developer tool caches" + echo " โ€ข Temporary files" + echo " โ€ข And much more..." + 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): " + read -s password + echo "" + else + # Non-interactive mode - skip password prompt + password="" + echo "Running in non-interactive mode, skipping system-level cleanup." + fi + + if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then + SYSTEM_CLEAN=true + # Start sudo keepalive with shorter intervals for reliability + while true; do sudo -n true; sleep 30; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null & + SUDO_KEEPALIVE_PID=$! + log_info "Starting comprehensive cleanup with admin privileges..." + else + SYSTEM_CLEAN=false + log_info "Starting user-level cleanup..." + if [[ -n "$password" ]]; then + echo -e "${YELLOW}โš ๏ธ Invalid password, continuing with user-level cleanup${NC}" + fi + fi +} + +perform_cleanup() { + echo "" + echo "๐Ÿ•ณ๏ธ Mole - 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 "" + + # Get initial space + space_before=$(df / | tail -1 | awk '{print $4}') + + # Initialize counters + total_items=0 + files_cleaned=0 + total_size_cleaned=0 + + # ===== 1. System cleanup (if admin) - Do this first while sudo is fresh ===== + if [[ "$SYSTEM_CLEAN" == "true" ]]; then + start_section "System-level cleanup" + + # Clean system caches more safely - avoid input method and system service caches + sudo find /Library/Caches -name "*.cache" -delete 2>/dev/null || true + sudo find /Library/Caches -name "*.tmp" -delete 2>/dev/null || true + sudo find /Library/Caches -type f -name "*.log" -delete 2>/dev/null || true + sudo rm -rf /tmp/* 2>/dev/null && log_success "System temp files" || true + sudo rm -rf /var/tmp/* 2>/dev/null && log_success "System var temp" || true + log_success "System library caches (safely cleaned)" + + end_section + fi + + # ===== 2. User essentials ===== + start_section "System essentials" + safe_clean ~/Library/Caches/* "User app cache" + safe_clean ~/Library/Logs/* "User app logs" + safe_clean ~/.Trash/* "Trash" + + # Empty the trash on all mounted volumes + if [[ -d "/Volumes" ]]; then + for volume in /Volumes/*; do + if [[ -d "$volume" && -d "$volume/.Trashes" ]]; then + find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2>/dev/null || true + fi + done + fi + + safe_clean ~/Library/Application\ Support/CrashReporter/* "Crash reports" + safe_clean ~/Library/DiagnosticReports/* "Diagnostic reports" + safe_clean ~/Library/Caches/com.apple.QuickLook.thumbnailcache "QuickLook thumbnails" + safe_clean ~/Library/Caches/Quick\ Look/* "QuickLook cache" + safe_clean ~/Library/Caches/com.apple.LaunchServices* "Launch services cache" + safe_clean ~/Library/Caches/com.apple.iconservices* "Icon services cache" + safe_clean ~/Library/Caches/CloudKit/* "CloudKit cache" + safe_clean ~/Library/Caches/com.apple.bird* "iCloud cache" + end_section + + # ===== 2. Browsers ===== + start_section "Browser cleanup" + # Safari + safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache" + safe_clean ~/Library/Safari/LocalStorage/* "Safari local storage" + safe_clean ~/Library/Safari/Databases/* "Safari databases" + + # Chrome/Chromium family + safe_clean ~/Library/Caches/Google/Chrome/* "Chrome cache" + safe_clean ~/Library/Application\ Support/Google/Chrome/*/Application\ Cache/* "Chrome app cache" + safe_clean ~/Library/Application\ Support/Google/Chrome/*/GPUCache/* "Chrome GPU cache" + safe_clean ~/Library/Caches/Chromium/* "Chromium cache" + + # Other browsers + safe_clean ~/Library/Caches/com.microsoft.edgemac/* "Edge cache" + safe_clean ~/Library/Caches/company.thebrowser.Browser/* "Arc cache" + safe_clean ~/Library/Caches/BraveSoftware/Brave-Browser/* "Brave cache" + safe_clean ~/Library/Caches/Firefox/* "Firefox cache" + safe_clean ~/Library/Caches/com.operasoftware.Opera/* "Opera cache" + safe_clean ~/Library/Caches/com.vivaldi.Vivaldi/* "Vivaldi cache" + + # Browser support files + safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache" + end_section + + # ===== 3. Developer tools ===== + start_section "Developer tools" + # Node.js ecosystem + if command -v npm >/dev/null 2>&1; then + npm cache clean --force >/dev/null 2>&1 || true + echo -e " ${GREEN}โœ“${NC} npm cache cleaned" + note_activity + fi + + safe_clean ~/.npm/_cacache/* "npm cache directory" + safe_clean ~/.yarn/cache/* "Yarn cache" + safe_clean ~/.bun/install/cache/* "Bun cache" + + # Python ecosystem + if command -v pip3 >/dev/null 2>&1; then + pip3 cache purge >/dev/null 2>&1 || true + echo -e " ${GREEN}โœ“${NC} pip cache cleaned" + note_activity + fi + + safe_clean ~/.cache/pip/* "pip cache directory" + safe_clean ~/Library/Caches/pip/* "pip cache (macOS)" + safe_clean ~/.pyenv/cache/* "pyenv cache" + + # Go ecosystem + if command -v go >/dev/null 2>&1; then + go clean -modcache >/dev/null 2>&1 || true + go clean -cache >/dev/null 2>&1 || true + echo -e " ${GREEN}โœ“${NC} Go cache cleaned" + note_activity + fi + + safe_clean ~/Library/Caches/go-build/* "Go build cache" + safe_clean ~/go/pkg/mod/cache/* "Go module cache" + + # Rust + safe_clean ~/.cargo/registry/cache/* "Rust cargo cache" + + # Docker + if command -v docker >/dev/null 2>&1; then + docker system prune -af --volumes >/dev/null 2>&1 || true + echo -e " ${GREEN}โœ“${NC} Docker resources cleaned" + note_activity + fi + + # Container tools + safe_clean ~/.kube/cache/* "Kubernetes cache" + if command -v podman >/dev/null 2>&1; then + podman system prune -af --volumes >/dev/null 2>&1 || true + echo -e " ${GREEN}โœ“${NC} Podman resources cleaned" + note_activity + fi + safe_clean ~/.local/share/containers/storage/tmp/* "Container storage temp" + + # Cloud CLI tools + safe_clean ~/.aws/cli/cache/* "AWS CLI cache" + safe_clean ~/.config/gcloud/logs/* "Google Cloud logs" + safe_clean ~/.azure/logs/* "Azure CLI logs" + + # Homebrew cleanup + safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache" + safe_clean /opt/homebrew/var/homebrew/locks/* "Homebrew lock files (M series)" + safe_clean /usr/local/var/homebrew/locks/* "Homebrew lock files (Intel)" + if command -v brew >/dev/null 2>&1; then + brew cleanup >/dev/null 2>&1 || true + echo -e " ${GREEN}โœ“${NC} Homebrew cache cleaned" + note_activity + fi + + # Git + safe_clean ~/.gitconfig.lock "Git config lock" + end_section + + # ===== Extended developer caches ===== + start_section "Extended developer caches" + + # Additional Node.js and frontend tools + 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" + safe_clean ~/.cache/vite/* "Vite global cache" + safe_clean ~/.cache/webpack/* "Webpack cache" + safe_clean ~/.parcel-cache/* "Parcel cache" + + # Design and development tools + safe_clean ~/Library/Caches/Google/AndroidStudio*/* "Android Studio cache" + safe_clean ~/Library/Caches/com.unity3d.*/* "Unity cache" + safe_clean ~/Library/Caches/com.postmanlabs.mac/* "Postman cache" + safe_clean ~/Library/Caches/com.konghq.insomnia/* "Insomnia cache" + safe_clean ~/Library/Caches/com.tinyapp.TablePlus/* "TablePlus cache" + safe_clean ~/Library/Caches/com.mongodb.compass/* "MongoDB Compass cache" + safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache" + safe_clean ~/Library/Caches/com.github.GitHubDesktop/* "GitHub Desktop cache" + safe_clean ~/Library/Caches/com.microsoft.VSCode/* "VS Code cache" + safe_clean ~/Library/Caches/com.sublimetext.*/* "Sublime Text cache" + + # Python tooling + safe_clean ~/.cache/poetry/* "Poetry cache" + safe_clean ~/.cache/uv/* "uv cache" + safe_clean ~/.cache/ruff/* "Ruff cache" + safe_clean ~/.cache/mypy/* "MyPy cache" + safe_clean ~/.pytest_cache/* "Pytest cache" + + # AI/ML and Data Science tools + safe_clean ~/.jupyter/runtime/* "Jupyter runtime cache" + safe_clean ~/.cache/huggingface/* "Hugging Face cache" + safe_clean ~/.cache/torch/* "PyTorch cache" + safe_clean ~/.cache/tensorflow/* "TensorFlow cache" + safe_clean ~/.conda/pkgs/* "Conda packages cache" + safe_clean ~/anaconda3/pkgs/* "Anaconda packages cache" + safe_clean ~/.cache/wandb/* "Weights & Biases cache" + + # Rust tooling + safe_clean ~/.cargo/git/* "Cargo git cache" + + # Java tooling + safe_clean ~/.gradle/caches/* "Gradle caches" + safe_clean ~/.m2/repository/* "Maven repository cache" + safe_clean ~/.sbt/* "SBT cache" + + # 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" + safe_clean ~/.prometheus/data/wal/* "Prometheus WAL cache" + safe_clean ~/.jenkins/workspace/*/target/* "Jenkins workspace cache" + safe_clean ~/.cache/gitlab-runner/* "GitLab Runner cache" + safe_clean ~/.github/cache/* "GitHub Actions cache" + safe_clean ~/.circleci/cache/* "CircleCI cache" + + # Additional development 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" + safe_clean ~/.sonar/* "SonarQube cache" + safe_clean ~/.cache/eslint/* "ESLint cache" + safe_clean ~/.cache/prettier/* "Prettier cache" + + # Mobile development + safe_clean ~/Library/Caches/CocoaPods/* "CocoaPods cache" + safe_clean ~/.bundle/cache/* "Ruby Bundler cache" + safe_clean ~/.composer/cache/* "PHP Composer cache" + safe_clean ~/.nuget/packages/* "NuGet packages cache" + safe_clean ~/.ivy2/cache/* "Ivy cache" + safe_clean ~/.pub-cache/* "Dart Pub cache" + + # Network tools cache (safe) + 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" + + # Git and version control + safe_clean ~/.cache/pre-commit/* "pre-commit cache" + safe_clean ~/.gitconfig.bak* "Git config backup" + + # Mobile development + 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" + safe_clean ~/.android/cache/* "Android SDK cache" + + # Other language tool caches + safe_clean ~/.cache/swift-package-manager/* "Swift package manager cache" + safe_clean ~/.cache/bazel/* "Bazel cache" + safe_clean ~/.cache/zig/* "Zig cache" + safe_clean ~/.cache/deno/* "Deno cache" + + # Database tools + safe_clean ~/Library/Caches/com.sequel-ace.sequel-ace/* "Sequel Ace cache" + 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" + + # Xcode & iOS development + safe_clean ~/Library/Developer/Xcode/DerivedData/* "Xcode derived data" + safe_clean ~/Library/Developer/Xcode/Archives/* "Xcode archives" + safe_clean ~/Library/Developer/CoreSimulator/Caches/* "Simulator cache" + safe_clean ~/Library/Developer/CoreSimulator/Devices/*/data/tmp/* "Simulator temp files" + safe_clean ~/Library/Caches/com.apple.dt.Xcode/* "Xcode cache" + + # VS Code family + safe_clean ~/Library/Application\ Support/Code/logs/* "VS Code logs" + safe_clean ~/Library/Application\ Support/Code/CachedExtensions/* "VS Code extension cache" + safe_clean ~/Library/Application\ Support/Code/CachedData/* "VS Code data cache" + + # JetBrains IDEs + safe_clean ~/Library/Logs/IntelliJIdea*/* "IntelliJ IDEA logs" + safe_clean ~/Library/Logs/PhpStorm*/* "PhpStorm logs" + safe_clean ~/Library/Logs/PyCharm*/* "PyCharm logs" + safe_clean ~/Library/Logs/WebStorm*/* "WebStorm logs" + safe_clean ~/Library/Logs/GoLand*/* "GoLand logs" + safe_clean ~/Library/Logs/CLion*/* "CLion logs" + safe_clean ~/Library/Logs/DataGrip*/* "DataGrip logs" + safe_clean ~/Library/Caches/JetBrains/* "JetBrains cache" + + # Communication and social apps + safe_clean ~/Library/Application\ Support/discord/Cache/* "Discord cache" + safe_clean ~/Library/Application\ Support/Slack/Cache/* "Slack cache" + safe_clean ~/Library/Caches/us.zoom.xos/* "Zoom cache" + safe_clean ~/Library/Caches/com.tencent.xinWeChat/* "WeChat cache" + safe_clean ~/Library/Caches/ru.keepcoder.Telegram/* "Telegram cache" + safe_clean ~/Library/Caches/com.openai.chat/* "ChatGPT cache" + safe_clean ~/Library/Caches/com.anthropic.claudefordesktop/* "Claude desktop cache" + safe_clean ~/Library/Logs/Claude/* "Claude logs" + safe_clean ~/Library/Caches/com.microsoft.teams2/* "Microsoft Teams cache" + safe_clean ~/Library/Caches/net.whatsapp.WhatsApp/* "WhatsApp cache" + safe_clean ~/Library/Caches/com.skype.skype/* "Skype cache" + + # Design and creative software + safe_clean ~/Library/Caches/com.bohemiancoding.sketch3/* "Sketch cache" + safe_clean ~/Library/Application\ Support/com.bohemiancoding.sketch3/cache/* "Sketch app cache" + safe_clean ~/Library/Caches/net.telestream.screenflow10/* "ScreenFlow cache" + + # Productivity and dev utilities + safe_clean ~/Library/Caches/com.raycast.macos/* "Raycast cache" + safe_clean ~/Library/Caches/com.tw93.MiaoYan/* "MiaoYan cache" + safe_clean ~/Library/Caches/com.filo.client/* "Filo cache" + safe_clean ~/Library/Caches/com.flomoapp.mac/* "Flomo cache" + + # Music and entertainment + safe_clean ~/Library/Caches/com.spotify.client/* "Spotify cache" + + # Gaming and entertainment + 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" + 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" + + end_section + + # ===== 5. Orphaned leftovers ===== + log_header "Checking for orphaned app files" + + # Build a list of installed application bundle identifiers + echo -e " ${BLUE}๐Ÿ”${NC} Building app list..." + local installed_bundles=$(mktemp) + # More robust approach that won't hang + for app in /Applications/*.app; do + if [[ -d "$app" && -f "$app/Contents/Info.plist" ]]; then + bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") + [[ -n "$bundle_id" ]] && echo "$bundle_id" >> "$installed_bundles" + fi + done + local app_count=$(wc -l < "$installed_bundles" | tr -d ' ') + echo -e " ${GREEN}โœ“${NC} Found $app_count apps" + + local found_orphaned=false + + # Check for orphaned caches (with protection for critical system settings) + echo -e " ${BLUE}๐Ÿ”${NC} Checking caches..." + local cache_count=0 + if ls ~/Library/Caches/com.* >/dev/null 2>&1; then + for cache_dir in ~/Library/Caches/com.*; do + [[ -d "$cache_dir" ]] || continue + local bundle_id=$(basename "$cache_dir") + # CRITICAL: Skip system-essential caches + if should_preserve_bundle "$bundle_id"; then + continue + fi + if ! grep -q "$bundle_id" "$installed_bundles" 2>/dev/null; then + safe_clean "$cache_dir" "Orphaned cache: $bundle_id" + found_orphaned=true + ((cache_count++)) + fi + done + fi + echo -e " ${GREEN}โœ“${NC} Checked caches ($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 + if ls ~/Library/Application\ Support/com.* >/dev/null 2>&1; then + for support_dir in ~/Library/Application\ Support/com.*; do + [[ -d "$support_dir" ]] || continue + local bundle_id=$(basename "$support_dir") + # CRITICAL: Skip system-essential data + if should_preserve_bundle "$bundle_id"; then + continue + fi + # Extra safety for Application Support data + case "$bundle_id" in + *dock*|*Dock*|*controlcenter*|*ControlCenter*|*systempreferences*|*SystemPreferences*) + continue + ;; + *trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*) + continue + ;; + esac + if ! grep -q "$bundle_id" "$installed_bundles" 2>/dev/null; then + safe_clean "$support_dir" "Orphaned data: $bundle_id" + found_orphaned=true + ((data_count++)) + fi + done + fi + echo -e " ${GREEN}โœ“${NC} Checked app data ($data_count removed)" + + # Check for orphaned preferences (with protection for critical system settings) + echo -e " ${BLUE}๐Ÿ”${NC} Checking preferences..." + local pref_count=0 + if ls ~/Library/Preferences/com.*.plist >/dev/null 2>&1; then + for pref_file in ~/Library/Preferences/com.*.plist; do + [[ -f "$pref_file" ]] || continue + local bundle_id=$(basename "$pref_file" .plist) + # CRITICAL: Skip system-essential preferences + if should_preserve_bundle "$bundle_id"; then + continue + fi + # Extra safety: Never delete preference files that might affect system behavior + case "$bundle_id" in + *dock*|*Dock*|*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*) + continue + ;; + *systempreferences*|*SystemPreferences*|*controlcenter*|*ControlCenter*) + continue + ;; + *menubar*|*MenuBar*|*hotkeys*|*HotKeys*|*shortcuts*|*Shortcuts*) + continue + ;; + esac + if ! grep -q "$bundle_id" "$installed_bundles" 2>/dev/null; then + safe_clean "$pref_file" "Orphaned preference: $bundle_id" + found_orphaned=true + ((pref_count++)) + fi + done + fi + echo -e " ${GREEN}โœ“${NC} Checked preferences ($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 + + # Common temp and 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" + safe_clean ~/Library/Application\ Support/Twitter*/* "Twitter test data" + safe_clean ~/Library/Application\ Support/TestNoValue/* "Test data" + safe_clean ~/Library/Application\ Support/Wk*/* "Test data" + + # ===== 5. Apple Silicon optimizations ===== + if [[ "$IS_M_SERIES" == "true" ]]; then + start_section "Apple Silicon cache cleanup" + 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" + safe_clean ~/Library/Caches/com.apple.bird.lsuseractivity "User activity cache" + end_section + fi + + # System cleanup was moved to the beginning (right after password verification) + + # ===== 7. iOS device backups ===== + log_header "Checking 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"}') + echo -e " ๐Ÿ‘‰ Found ${GREEN}${backup_human}${NC}, you can delete it manually" + echo -e " ๐Ÿ‘‰ ${backup_dir}" + else + echo -e " ${BLUE}โœจ${NC} Nothing to tidy" + fi + else + echo -e " ${BLUE}โœจ${NC} Nothing to tidy" + fi + + # ===== 8. Summary ===== + log_header "Cleanup 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" + 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" + fi + + echo "===================================================================" +} + +main() { + case "${1:-""}" in + "--help"|"-h") + echo "Mole - System Cleanup" + echo "Usage: clean.sh [options]" + echo "" + echo "Options:" + echo " --help, -h Show this help" + echo "" + echo "Interactive cleanup with smart password handling" + exit 0 + ;; + *) + start_cleanup + perform_cleanup + ;; + esac +} + +main "$@" \ No newline at end of file diff --git a/bin/install.sh b/bin/install.sh new file mode 100755 index 0000000..3f26d1f --- /dev/null +++ b/bin/install.sh @@ -0,0 +1,388 @@ +#!/bin/bash +# Mac Tools - 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 "$@" \ No newline at end of file diff --git a/bin/uninstall.sh b/bin/uninstall.sh new file mode 100755 index 0000000..029c386 --- /dev/null +++ b/bin/uninstall.sh @@ -0,0 +1,447 @@ +#!/bin/bash +# Mac Tools - Uninstall Module +# Interactive application uninstaller with keyboard navigation +# +# Usage: +# uninstall.sh # Launch interactive uninstaller +# uninstall.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" +source "$SCRIPT_DIR/../lib/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 +} + +# Help information +show_help() { + echo "Clean Mac - Interactive App Uninstaller" + echo "========================================" + echo "" + echo "Description: Interactive tool to uninstall applications and clean their data" + 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 "" + echo "Usage:" + echo " ./uninstall.sh Launch interactive uninstaller" + echo " ./uninstall.sh --help Show this help message" + echo "" + echo "What gets cleaned:" + echo " โ€ข Application bundle" + echo " โ€ข Application Support data" + echo " โ€ข Cache files" + echo " โ€ข Preference files" + echo " โ€ข Log files" + echo " โ€ข Saved application state" + echo " โ€ข Container data (sandboxed apps)" + echo "" +} + +# Parse arguments +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + show_help + exit 0 +fi + +# Initialize global variables +declare -a selected_apps=() +declare -a apps_data=() +declare -a selection_state=() +current_line=0 +total_items=0 +files_cleaned=0 +total_size_cleaned=0 + +# Get app last used date in human readable format +get_app_last_used() { + local app_path="$1" + local last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) + + if [[ "$last_used" == "(null)" || -z "$last_used" ]]; then + echo "Never" + else + local last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2>/dev/null) + local current_epoch=$(date "+%s") + local days_ago=$(( (current_epoch - last_used_epoch) / 86400 )) + + if [[ $days_ago -eq 0 ]]; then + echo "Today" + elif [[ $days_ago -eq 1 ]]; then + echo "Yesterday" + elif [[ $days_ago -lt 30 ]]; then + echo "${days_ago} days ago" + elif [[ $days_ago -lt 365 ]]; then + local months_ago=$(( days_ago / 30 )) + echo "${months_ago} month(s) ago" + else + local years_ago=$(( days_ago / 365 )) + echo "${years_ago} year(s) ago" + fi + fi +} + +# Scan applications and collect information +scan_applications() { + local temp_file=$(mktemp) + + echo -n "Scanning applications... " >&2 + + # Pre-cache current epoch to avoid repeated calls + local current_epoch=$(date "+%s") + + # First pass: quickly collect all valid app paths and bundle IDs + local -a app_data_tuples=() + while IFS= read -r -d '' app_path; do + if [[ ! -e "$app_path" ]]; then continue; fi + + local app_name=$(basename "$app_path" .app) + + # Quick bundle ID check first (only if plist exists) + local bundle_id="unknown" + if [[ -f "$app_path/Contents/Info.plist" ]]; then + bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown") + fi + + # Skip protected system apps early + if should_preserve_bundle "$bundle_id"; then + continue + fi + + # Store tuple: app_path|app_name|bundle_id + app_data_tuples+=("${app_path}|${app_name}|${bundle_id}") + done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null) + + # Second pass: process each app with accurate size calculation + local app_count=0 + 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" + + # 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 + fi + + # Accurate size calculation - this is what takes time but user wants it + local app_size="N/A" + if [[ -d "$app_path" ]]; then + 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" + 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" + 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" + done + + echo -e "\rScanning applications... found $app_count apps โœ“" >&2 + + # Check if we found any applications + if [[ ! -s "$temp_file" ]]; then + rm -f "$temp_file" + return 1 + fi + + # Sort by last used (oldest first) and return the temp file path + sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" + rm -f "$temp_file" + echo "${temp_file}.sorted" +} + +# Load applications into arrays +load_applications() { + local apps_file="$1" + + if [[ ! -f "$apps_file" || ! -s "$apps_file" ]]; then + log_warning "No applications found for uninstallation" + return 1 + fi + + # Clear arrays + apps_data=() + selection_state=() + + # Read apps into array + while IFS='|' read -r epoch app_path app_name bundle_id size last_used; do + apps_data+=("$epoch|$app_path|$app_name|$bundle_id|$size|$last_used") + selection_state+=(false) + done < "$apps_file" + + if [[ ${#apps_data[@]} -eq 0 ]]; then + log_warning "No applications available for uninstallation" + return 1 + fi + + return 0 +} + +# Old display_apps function removed - replaced by new menu system + +# 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" +} + +# Uninstall selected applications +uninstall_applications() { + local total_size_freed=0 + + log_header "Uninstalling selected applications" + + if [[ ${#selected_apps[@]} -eq 0 ]]; then + log_warning "No applications selected for uninstallation" + return 0 + fi + + for selected_app in "${selected_apps[@]}"; do + IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" + + echo "" + log_info "Processing: $app_name" + + # Check if app is running + if pgrep -f "$app_name" >/dev/null 2>&1; then + log_warning "$app_name is currently running" + read -p " Force quit $app_name? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + pkill -f "$app_name" 2>/dev/null || true + sleep 2 + else + log_warning "Skipping $app_name (still running)" + continue + fi + fi + + # Find related files + local related_files=$(find_app_files "$bundle_id" "$app_name") + + # Calculate total size + local app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0") + local related_size_kb=$(calculate_total_size "$related_files") + local total_kb=$((app_size_kb + related_size_kb)) + + # Show what will be removed + echo -e " ${YELLOW}Files to be removed:${NC}" + echo -e " ${GREEN}โœ“${NC} Application: $(echo "$app_path" | sed "s|$HOME|~|")" + + while IFS= read -r file; do + [[ -n "$file" && -e "$file" ]] && echo -e " ${GREEN}โœ“${NC} $(echo "$file" | sed "s|$HOME|~|")" + done <<< "$related_files" + + if [[ $total_kb -gt 1048576 ]]; then # > 1GB + local size_display=$(echo "$total_kb" | awk '{printf "%.2fGB", $1/1024/1024}') + elif [[ $total_kb -gt 1024 ]]; then # > 1MB + local size_display=$(echo "$total_kb" | awk '{printf "%.1fMB", $1/1024}') + else + local size_display="${total_kb}KB" + fi + + echo -e " ${BLUE}Total size: $size_display${NC}" + echo + + read -p " Proceed with uninstalling $app_name? (y/N): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + # Remove the application + if rm -rf "$app_path" 2>/dev/null; then + echo -e " ${GREEN}โœ“${NC} Removed application" + else + log_error "Failed to remove $app_path" + continue + fi + + # Remove related files + while IFS= read -r file; do + if [[ -n "$file" && -e "$file" ]]; then + if rm -rf "$file" 2>/dev/null; then + echo -e " ${GREEN}โœ“${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)" + fi + fi + done <<< "$related_files" + + ((total_size_freed += total_kb)) + ((files_cleaned++)) + ((total_items++)) + + log_success "$app_name uninstalled successfully" + else + log_info "Skipped $app_name" + fi + done + + # Show final summary + echo "" + log_header "Uninstallation Summary" + + if [[ $total_size_freed -gt 0 ]]; then + if [[ $total_size_freed -gt 1048576 ]]; then # > 1GB + local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}') + elif [[ $total_size_freed -gt 1024 ]]; then # > 1MB + local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}') + else + local freed_display="${total_size_freed}KB" + fi + + log_success "Freed $freed_display of disk space" + fi + + echo "๐Ÿ“Š Applications uninstalled: $files_cleaned" + ((total_size_cleaned += total_size_freed)) +} + +# Cleanup function - restore cursor and clean up +cleanup() { + # Restore cursor + printf '\033[?25h' + exit "${1:-0}" +} + +# Set trap for cleanup on exit +trap cleanup EXIT INT TERM + +# Main function +main() { + echo "๐Ÿ—‘๏ธ Clean Mac - Interactive App Uninstaller" + echo "============================================" + echo + + # Scan applications + local apps_file=$(scan_applications) + + if [[ ! -f "$apps_file" ]]; then + log_error "Failed to scan applications" + return 1 + fi + + # Load applications + if ! load_applications "$apps_file"; then + rm -f "$apps_file" + return 1 + fi + + # Interactive selection using new menu system + if ! select_apps_for_uninstall; then + rm -f "$apps_file" + return 0 + fi + + # Restore cursor for normal interaction + printf '\033[?25h' + clear + echo "You selected ${#selected_apps[@]} application(s) for uninstallation:" + echo "" + + if [[ ${#selected_apps[@]} -gt 0 ]]; then + for selected_app in "${selected_apps[@]}"; do + IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" + echo " โ€ข $app_name ($size)" + done + else + echo " No applications to uninstall." + fi + + echo "" + # ็›ดๆŽฅๆ‰ง่กŒๆ‰น้‡ๅธ่ฝฝ๏ผŒ็กฎ่ฎคๅทฒๅœจๆ‰น้‡ๅธ่ฝฝๅ‡ฝๆ•ฐไธญๅค„็† + batch_uninstall_applications + + # Cleanup + rm -f "$apps_file" + + log_success "App uninstaller finished" +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/clean.sh b/clean.sh deleted file mode 100755 index 9435e0e..0000000 --- a/clean.sh +++ /dev/null @@ -1,729 +0,0 @@ -#!/bin/bash -# Clean Mac - Deep Clean Your Mac with One Click -# -# ๐Ÿงน One-click cleanup tool for macOS cache and leftovers -# ๐ŸŽ Optimized for Apple Silicon with comprehensive dev tool support -# -# Quick install: -# curl -fsSL https://raw.githubusercontent.com/tw93/clean-mac/main/install.sh | bash -# -# Usage: -# clean # Daily cleanup (safe, no password) -# clean --system # Deep system cleanup (requires password) -# clean --help # Show help information -# -# GitHub: https://github.com/tw93/clean-mac -# License: MIT ยฉ tw93 - -# ANSI color palette -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -PURPLE='\033[0;35m' -RED='\033[0;31m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}$1${NC}"; } -log_success() { echo -e "${GREEN}โœ… $1${NC}"; } -log_warning() { echo -e "${YELLOW}โš ๏ธ $1${NC}"; } -log_error() { echo -e "${RED}โŒ $1${NC}"; } -log_header() { echo -e "\n${PURPLE}โ–ถ $1${NC}"; } - -PRESERVED_BUNDLE_PATTERNS=( - "com.apple.*" - "com.nektony.*" -) - -if [[ -n "$CLEAN_PRESERVE_BUNDLES" ]]; then - IFS=',' read -r -a extra_patterns <<< "$CLEAN_PRESERVE_BUNDLES" - PRESERVED_BUNDLE_PATTERNS+=("${extra_patterns[@]}") -fi - -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 -} - -LAST_CLEAN_RESULT=0 -SECTION_ACTIVITY=0 -TRACK_SECTION=0 -SUDO_KEEPALIVE_PID="" - -note_activity() { - if [[ $TRACK_SECTION -eq 1 ]]; then - SECTION_ACTIVITY=1 - fi -} - -cleanup() { - if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then - kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true - SUDO_KEEPALIVE_PID="" - fi -} - -trap cleanup EXIT INT TERM - -start_section() { - TRACK_SECTION=1 - SECTION_ACTIVITY=0 - log_header "$1" -} - -end_section() { - if [[ $TRACK_SECTION -eq 1 && $SECTION_ACTIVITY -eq 0 ]]; then - echo -e " ${BLUE}โœจ${NC} Nothing to tidy" - fi - TRACK_SECTION=0 -} - -# Configuration -SYSTEM_CLEAN=false - -# Argument parsing -while [[ $# -gt 0 ]]; do - case $1 in - --help|-h) - echo "Usage: clean [OPTIONS]" - echo "" - echo "Description: Clean Mac - Deep Clean Your Mac with One Click" - echo "" - echo "Options:" - echo " --system Include system-level cleanup (requires password)" - echo " --help Show this help message" - echo "" - echo "Default: Cleans user-level cache and data (no password required)" - exit 0 - ;; - --system) - SYSTEM_CLEAN=true - shift - ;; - *) - log_error "Unknown option: $1" - echo "Use --help to view available options" - exit 1 - ;; - esac -done - -# Detect Mac architecture -ARCH=$(uname -m) -IS_M_SERIES=false -if [[ "$ARCH" == "arm64" ]]; then - IS_M_SERIES=true -fi - -# Privilege escalation helper -request_sudo() { - if [[ "$SKIP_SYSTEM" == "true" ]]; then - return 0 - fi - log_info "Administrator privileges are required for system-level cleanup..." - if ! sudo -v; then - log_warning "System-level cleanup skipped (sudo unavailable)" - SKIP_SYSTEM=true - return 1 - fi - # Keep sudo session alive - while true; do sudo -n true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null & - SUDO_KEEPALIVE_PID=$! -} - -# Safe cleanup helper -safe_clean() { - if [[ $# -eq 0 ]]; then - return 0 - fi - - local description - local -a targets - - if [[ $# -eq 1 ]]; then - description="$1" - targets=("$1") - else - description="${@: -1}" - targets=("${@:1:$#-1}") - fi - - local removed_any=0 - - for path in "${targets[@]}"; do - if [[ ! -e "$path" ]]; then - continue - fi - - local size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0") - local size_human=$(du -sh "$path" 2>/dev/null | awk '{print $1}' || echo "0B") - local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ') - - if [[ "$count" -eq 0 || "$size_bytes" -eq 0 ]]; then - continue - fi - - rm -rf "$path" 2>/dev/null || true - - local label="$description" - if [[ ${#targets[@]} -gt 1 ]]; then - label+=" [$(basename "$path")"] - fi - - echo -e " ${GREEN}โœ“${NC} $label ${GREEN}($size_human)${NC}" - ((files_cleaned+=count)) - ((total_size_cleaned+=size_bytes)) - ((total_items++)) - removed_any=1 - note_activity - done - - LAST_CLEAN_RESULT=$removed_any - return $removed_any -} - -# Prompt before cleaning non-cache application data -confirm_clean() { - local path="$1" - local description="${2:-$path}" - local app_name="$3" - - if [[ ! -e "$path" ]]; then - return 0 - fi - - local size_human=$(du -sh "$path" 2>/dev/null | cut -f1 || echo "0B") - local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ') - - if [[ "$count" -gt 0 ]]; then - echo -e " ${YELLOW}โš ๏ธ${NC} Found $app_name data: ${GREEN}($size_human)${NC}" - read -p " Clean $app_name data? (y/N): " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - safe_clean "$path" "$description" - else - echo -e " ${BLUE}โ„น${NC} Skipped $app_name data" - fi - fi -} - -# Detect orphaned application leftovers -check_orphaned_apps() { - log_header "Checking for orphaned app files" - - # Build a list of installed application bundle identifiers - local installed_bundles=$(mktemp) - find /Applications -name "*.app" -exec sh -c 'defaults read "$1/Contents/Info.plist" CFBundleIdentifier 2>/dev/null' _ {} \; > "$installed_bundles" 2>/dev/null - - local found_orphaned=false - - # Inspect the Caches directory - find ~/Library/Caches -maxdepth 1 -name "com.*" -type d | while read cache_dir; do - local bundle_id=$(basename "$cache_dir") - if should_preserve_bundle "$bundle_id"; then - continue - fi - if ! grep -q "$bundle_id" "$installed_bundles"; then - safe_clean "$cache_dir" "Orphaned cache: $bundle_id" - found_orphaned=true - fi - done - - # Inspect the Application Support directory - find ~/Library/Application\ Support -maxdepth 1 -name "com.*" -type d | while read support_dir; do - local bundle_id=$(basename "$support_dir") - if should_preserve_bundle "$bundle_id"; then - continue - fi - if ! grep -q "$bundle_id" "$installed_bundles"; then - safe_clean "$support_dir" "Orphaned data: $bundle_id" - found_orphaned=true - fi - done - - # Inspect the Preferences directory - find ~/Library/Preferences -maxdepth 1 -name "com.*.plist" -type f | while read pref_file; do - local bundle_id=$(basename "$pref_file" .plist) - if should_preserve_bundle "$bundle_id"; then - continue - fi - if ! grep -q "$bundle_id" "$installed_bundles"; then - safe_clean "$pref_file" "Orphaned preference: $bundle_id" - found_orphaned=true - fi - done - - if [[ "$found_orphaned" == "false" ]]; then - echo -e " ${GREEN}โœ“${NC} No orphaned files found" - fi - - rm -f "$installed_bundles" -} - -# Apple Silicon specific cache cleanup -clean_command_cache() { - if [[ "$IS_M_SERIES" == "true" ]]; then - local silicon_cleaned=0 - - # Rosetta 2 cache - safe_clean /Library/Apple/usr/share/rosetta/rosetta_update_bundle "Rosetta 2 cache" - ((silicon_cleaned |= LAST_CLEAN_RESULT)) - safe_clean ~/Library/Caches/com.apple.rosetta.update "Rosetta 2 user cache" - ((silicon_cleaned |= LAST_CLEAN_RESULT)) - - # Additional Apple Silicon caches - safe_clean ~/Library/Caches/com.apple.amp.mediasevicesd "Apple Silicon media service cache" - ((silicon_cleaned |= LAST_CLEAN_RESULT)) - safe_clean ~/Library/Caches/com.apple.bird.lsuseractivity "User activity cache" - ((silicon_cleaned |= LAST_CLEAN_RESULT)) - - if [[ $silicon_cleaned -eq 0 ]]; then - echo -e " ${BLUE}โœจ${NC} Apple Silicon caches are already tidy" - fi - fi -} - -echo "๐Ÿงน Clean Mac - Deep Clean Your Mac with One Click" -echo "================================================" -space_before=$(df / | tail -1 | awk '{print $4}') -current_space=$(df -h / | tail -1 | awk '{print $4}') - -if [[ "$IS_M_SERIES" == "true" ]]; then - echo "๐ŸŽ Detected: Apple Silicon (M-series) | ๐Ÿ’พ Free space: $current_space" -else - echo "๐Ÿ’ป Detected: Intel Mac | ๐Ÿ’พ Free space: $current_space" -fi - -if [[ "$SYSTEM_CLEAN" == "true" ]]; then - echo "๐Ÿš€ Mode: Full cleanup (user + system)" -else - echo "๐Ÿš€ Mode: User-level cleanup (no password required)" -fi - -# Initialize counters -total_items=0 -files_cleaned=0 -total_size_cleaned=0 - -# ===== 1. System essentials ===== -start_section "System essentials" -safe_clean ~/Library/Caches/* "User app cache" -safe_clean ~/Library/Logs/* "User app logs" -safe_clean ~/.Trash/* "Trash" - -# Empty the trash on all mounted volumes -for volume in /Volumes/*; do - [[ -d "$volume/.Trashes" ]] && safe_clean "$volume/.Trashes"/* "External volume trash" -done - -safe_clean ~/Library/Application\ Support/CrashReporter/* "Crash reports" -safe_clean ~/Library/DiagnosticReports/* "Diagnostic reports" -safe_clean ~/Library/Caches/com.apple.QuickLook.thumbnailcache "QuickLook thumbnails" -safe_clean ~/Library/Caches/Quick\ Look/* "QuickLook cache" -safe_clean ~/Library/Caches/com.apple.LaunchServices* "Launch services cache" -safe_clean ~/Library/Caches/com.apple.iconservices* "Icon services cache" -safe_clean ~/Library/Caches/CloudKit/* "CloudKit cache" -safe_clean ~/Library/Caches/com.apple.bird* "iCloud cache" - -end_section - -# ===== 2. Browsers ===== -start_section "Browser cleanup" -# Safari -safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache" -safe_clean ~/Library/Safari/LocalStorage/* "Safari local storage" -safe_clean ~/Library/Safari/Databases/* "Safari databases" - -# Chrome/Chromium family -safe_clean ~/Library/Caches/Google/Chrome/* "Chrome cache" -safe_clean ~/Library/Application\ Support/Google/Chrome/*/Application\ Cache/* "Chrome app cache" -safe_clean ~/Library/Application\ Support/Google/Chrome/*/GPUCache/* "Chrome GPU cache" -safe_clean ~/Library/Caches/Chromium/* "Chromium cache" - -# Other browsers -safe_clean ~/Library/Caches/com.microsoft.edgemac/* "Edge cache" -safe_clean ~/Library/Caches/company.thebrowser.Browser/* "Arc cache" -safe_clean ~/Library/Caches/BraveSoftware/Brave-Browser/* "Brave cache" -safe_clean ~/Library/Caches/Firefox/* "Firefox cache" -safe_clean ~/Library/Caches/com.operasoftware.Opera/* "Opera cache" -safe_clean ~/Library/Caches/com.vivaldi.Vivaldi/* "Vivaldi cache" - -end_section - -# ===== 3. Developer tools ===== -start_section "Developer tools" - -# Node.js ecosystem -if command -v npm >/dev/null 2>&1; then - npm cache clean --force >/dev/null 2>&1 || true - echo -e " ${GREEN}โœ“${NC} npm cache cleaned" - note_activity -fi - -safe_clean ~/.npm/_cacache/* "npm cache directory" -safe_clean ~/.yarn/cache/* "Yarn cache" -safe_clean ~/.bun/install/cache/* "Bun cache" - -# Python ecosystem -if command -v pip3 >/dev/null 2>&1; then - pip3 cache purge >/dev/null 2>&1 || true - echo -e " ${GREEN}โœ“${NC} pip cache cleaned" - note_activity -fi -safe_clean ~/Library/Caches/pip/* "pip cache directory" -safe_clean ~/.pyenv/cache/* "pyenv cache" - -# Go tooling -if command -v go >/dev/null 2>&1; then - go clean -modcache >/dev/null 2>&1 || true - go clean -cache >/dev/null 2>&1 || true - echo -e " ${GREEN}โœ“${NC} Go cache cleaned" - note_activity -fi - -# Homebrew cleanup -safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache" -safe_clean /opt/homebrew/var/homebrew/locks/* "Homebrew lock files (M series)" -safe_clean /usr/local/var/homebrew/locks/* "Homebrew lock files (Intel)" - -# Docker -if command -v docker >/dev/null 2>&1; then - docker system prune -af --volumes >/dev/null 2>&1 || true - echo -e " ${GREEN}โœ“${NC} Docker resources cleaned" - note_activity -fi - -# Container tools -safe_clean ~/.kube/cache/* "Kubernetes cache" -if command -v podman >/dev/null 2>&1; then - podman system prune -af --volumes >/dev/null 2>&1 || true - echo -e " ${GREEN}โœ“${NC} Podman resources cleaned" - note_activity -fi -safe_clean ~/.local/share/containers/storage/tmp/* "Container storage temp" - -# Cloud CLI tools -safe_clean ~/.aws/cli/cache/* "AWS CLI cache" -safe_clean ~/.config/gcloud/logs/* "Google Cloud logs" -safe_clean ~/.azure/logs/* "Azure CLI logs" - -end_section - -# ===== 4. Applications ===== -start_section "Applications" - -# Xcode & iOS development -safe_clean ~/Library/Developer/Xcode/DerivedData/* "Xcode derived data" -safe_clean ~/Library/Developer/Xcode/Archives/* "Xcode archives" -safe_clean ~/Library/Developer/CoreSimulator/Caches/* "Simulator cache" -safe_clean ~/Library/Developer/CoreSimulator/Devices/*/data/tmp/* "Simulator temp files" -safe_clean ~/Library/Caches/com.apple.dt.Xcode/* "Xcode cache" - -# VS Code family -safe_clean ~/Library/Application\ Support/Code/logs/* "VS Code logs" -safe_clean ~/Library/Application\ Support/Code/CachedExtensions/* "VS Code extension cache" -safe_clean ~/Library/Application\ Support/Code/CachedData/* "VS Code data cache" - -# JetBrains IDEs -safe_clean ~/Library/Logs/IntelliJIdea*/* "IntelliJ IDEA logs" -safe_clean ~/Library/Logs/PhpStorm*/* "PhpStorm logs" -safe_clean ~/Library/Logs/PyCharm*/* "PyCharm logs" -safe_clean ~/Library/Logs/WebStorm*/* "WebStorm logs" -safe_clean ~/Library/Logs/GoLand*/* "GoLand logs" -safe_clean ~/Library/Logs/CLion*/* "CLion logs" -safe_clean ~/Library/Logs/DataGrip*/* "DataGrip logs" -safe_clean ~/Library/Caches/JetBrains/* "JetBrains cache" - -# Communication and social apps -safe_clean ~/Library/Application\ Support/discord/Cache/* "Discord cache" -safe_clean ~/Library/Application\ Support/Slack/Cache/* "Slack cache" -safe_clean ~/Library/Caches/us.zoom.xos/* "Zoom cache" -safe_clean ~/Library/Caches/com.tencent.xinWeChat/* "WeChat cache" -safe_clean ~/Library/Caches/ru.keepcoder.Telegram/* "Telegram cache" -safe_clean ~/Library/Caches/com.openai.chat/* "ChatGPT cache" -safe_clean ~/Library/Caches/com.anthropic.claudefordesktop/* "Claude desktop cache" -safe_clean ~/Library/Logs/Claude/* "Claude logs" -safe_clean ~/Library/Caches/com.microsoft.teams2/* "Microsoft Teams cache" -safe_clean ~/Library/Caches/net.whatsapp.WhatsApp/* "WhatsApp cache" -safe_clean ~/Library/Caches/com.skype.skype/* "Skype cache" - -# Design and creative software -safe_clean ~/Library/Caches/com.bohemiancoding.sketch3/* "Sketch cache" -safe_clean ~/Library/Application\ Support/com.bohemiancoding.sketch3/cache/* "Sketch app cache" -safe_clean ~/Library/Caches/net.telestream.screenflow10/* "ScreenFlow cache" - -# Productivity and dev utilities -safe_clean ~/Library/Caches/com.raycast.macos/* "Raycast cache" -safe_clean ~/Library/Caches/com.tw93.MiaoYan/* "MiaoYan cache" -safe_clean ~/Library/Caches/com.filo.client/* "Filo cache" -safe_clean ~/Library/Caches/com.flomoapp.mac/* "Flomo cache" - -# Music and entertainment -safe_clean ~/Library/Caches/com.spotify.client/* "Spotify cache" - -# Gaming and entertainment -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" -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" - -end_section - -# ===== 5. Orphaned leftovers ===== -check_orphaned_apps - -# Common temp and 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" -safe_clean ~/Library/Application\ Support/Twitter*/* "Twitter test data" -safe_clean ~/Library/Application\ Support/TestNoValue/* "Test data" -safe_clean ~/Library/Application\ Support/Wk*/* "Test data" - -# ===== 6. Apple Silicon specific ===== -if [[ "$IS_M_SERIES" == "true" ]]; then - log_header "Apple Silicon cache cleanup" - clean_command_cache -fi - -# ===== 6. Extended dev caches ===== -start_section "Extended developer caches" - -# Additional Node.js and frontend tools -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" -safe_clean ~/.cache/vite/* "Vite global cache" -safe_clean ~/.cache/webpack/* "Webpack cache" -safe_clean ~/.parcel-cache/* "Parcel cache" - -# Design and development tools -safe_clean ~/Library/Caches/Google/AndroidStudio*/* "Android Studio cache" -safe_clean ~/Library/Caches/com.unity3d.*/* "Unity cache" -safe_clean ~/Library/Caches/com.postmanlabs.mac/* "Postman cache" -safe_clean ~/Library/Caches/com.konghq.insomnia/* "Insomnia cache" -safe_clean ~/Library/Caches/com.tinyapp.TablePlus/* "TablePlus cache" -safe_clean ~/Library/Caches/com.mongodb.compass/* "MongoDB Compass cache" -safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache" -safe_clean ~/Library/Caches/com.github.GitHubDesktop/* "GitHub Desktop cache" -safe_clean ~/Library/Caches/com.microsoft.VSCode/* "VS Code cache" -safe_clean ~/Library/Caches/com.sublimetext.*/* "Sublime Text cache" - -# Python tooling -safe_clean ~/.cache/poetry/* "Poetry cache" -safe_clean ~/.cache/uv/* "uv cache" -safe_clean ~/.cache/ruff/* "Ruff cache" -safe_clean ~/.cache/mypy/* "MyPy cache" -safe_clean ~/.pytest_cache/* "Pytest cache" - -# AI/ML and Data Science tools -safe_clean ~/.jupyter/runtime/* "Jupyter runtime cache" -safe_clean ~/.cache/huggingface/* "Hugging Face cache" -safe_clean ~/.cache/torch/* "PyTorch cache" -safe_clean ~/.cache/tensorflow/* "TensorFlow cache" -safe_clean ~/.conda/pkgs/* "Conda packages cache" -safe_clean ~/anaconda3/pkgs/* "Anaconda packages cache" -safe_clean ~/.cache/wandb/* "Weights & Biases cache" - -# Rust tooling -safe_clean ~/.cargo/registry/cache/* "Cargo registry cache" -safe_clean ~/.cargo/git/* "Cargo git cache" - -# Java tooling -safe_clean ~/.gradle/caches/* "Gradle caches" -safe_clean ~/.m2/repository/* "Maven repository cache" -safe_clean ~/.sbt/* "SBT cache" - -# Git and version control -safe_clean ~/.cache/pre-commit/* "pre-commit cache" -safe_clean ~/.gitconfig.bak* "Git config backup" - -# Mobile development -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" -safe_clean ~/.android/cache/* "Android SDK cache" - -# Other language tool caches -safe_clean ~/.cache/swift-package-manager/* "Swift package manager cache" -safe_clean ~/.cache/bazel/* "Bazel cache" -safe_clean ~/.cache/zig/* "Zig cache" -safe_clean ~/.cache/deno/* "Deno cache" - -# Database tools -safe_clean ~/Library/Caches/com.sequel-ace.sequel-ace/* "Sequel Ace cache" -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" - -# Additional system-level caches -safe_clean ~/Library/Caches/SentryCrash/* "Sentry crash reports" -safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports" -safe_clean ~/Library/Caches/com.crashlytics.data/* "Crashlytics data" -safe_clean ~/Library/Saved\ Application\ State/* "App state files" -safe_clean ~/Library/HTTPStorages/* "HTTP storage cache" - -# Network and download caches -safe_clean ~/Library/Caches/curl/* "curl cache" -safe_clean ~/Library/Caches/wget/* "wget cache" -safe_clean ~/Library/Caches/CocoaPods/* "CocoaPods cache" - -# Package managers and dependencies -safe_clean ~/.bundle/cache/* "Ruby Bundler cache" -safe_clean ~/.composer/cache/* "PHP Composer cache" -safe_clean ~/.nuget/packages/* "NuGet packages cache" -safe_clean ~/.ivy2/cache/* "Ivy cache" -safe_clean ~/.pub-cache/* "Dart Pub cache" - -# API and monitoring 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 ~/.grafana/cache/* "Grafana cache" -safe_clean ~/.prometheus/data/wal/* "Prometheus WAL cache" - -# CI/CD tools -safe_clean ~/.jenkins/workspace/*/target/* "Jenkins workspace cache" -safe_clean ~/.cache/gitlab-runner/* "GitLab Runner cache" -safe_clean ~/.github/cache/* "GitHub Actions cache" -safe_clean ~/.circleci/cache/* "CircleCI cache" - - -end_section - -# System-level cleanup function -run_system_cleanup() { - log_header "System-level deep clean" - - # Request elevated privileges - request_sudo - - if [[ "$SKIP_SYSTEM" != "true" ]]; then - sudo rm -rf /Library/Caches/* 2>/dev/null || true - sudo rm -rf /private/var/log/asl/* 2>/dev/null || true - sudo rm -rf /private/var/tmp/* 2>/dev/null || true - sudo rm -rf /tmp/* 2>/dev/null || true - sudo rm -rf /private/var/vm/sleepimage 2>/dev/null || true - - # Refresh system caches - sudo dscacheutil -flushcache 2>/dev/null || true - sudo killall -HUP mDNSResponder 2>/dev/null || true - sudo atsutil databases -remove 2>/dev/null || true - - # Font caches and system services - sudo atsutil databases -removeUser 2>/dev/null || true - sudo atsutil databases -remove 2>/dev/null || true - - # Additional system logs - sudo rm -rf /var/log/install.log* 2>/dev/null || true - sudo rm -rf /var/log/system.log.* 2>/dev/null || true - sudo rm -rf /Library/Logs/DiagnosticReports/* 2>/dev/null || true - - # CloudKit and iCloud cache (risky) - sudo rm -rf ~/Library/Application\ Support/CloudDocs/session/containers/* 2>/dev/null || true - sudo rm -rf ~/Library/Group\ Containers/* 2>/dev/null || true - sudo rm -rf ~/Library/WebKit/*/IDBSessionStorage/* 2>/dev/null || true - sudo rm -rf ~/Library/Caches/CloudKit/CloudKitMetadata 2>/dev/null || true - - # Alibaba suite - sudo rm -rf ~/Library/Caches/com.alibaba.AliLang.osx/* 2>/dev/null || true - sudo rm -rf ~/Library/Caches/com.alibaba.alilang3.osx/* 2>/dev/null || true - sudo rm -rf ~/Library/Logs/AliLangClient/* 2>/dev/null || true - - # Adobe Creative Suite (risky data) - sudo rm -rf ~/Library/Caches/com.adobe.* 2>/dev/null || true - sudo rm -rf ~/Library/Caches/com.apple.FinalCut* 2>/dev/null || true - sudo rm -rf ~/Library/Caches/com.blackmagic-design.DaVinciResolve/* 2>/dev/null || true - sudo rm -rf ~/Library/Caches/com.macpaw.CleanMyMac* 2>/dev/null || true - - # Virtual machines (potentially large but risky) - sudo rm -rf ~/Library/Caches/com.parallels.* 2>/dev/null || true - sudo rm -rf ~/Library/Caches/com.vmware.fusion/* 2>/dev/null || true - - # Memory cleanup - sudo purge 2>/dev/null || true - - echo -e " ${GREEN}โœ“${NC} System-level cache and services cleaned" - else - echo -e " ${BLUE}โ„น${NC} System-level cleanup skipped" - fi -} - -# ===== 7. System-level deep clean ===== -if [[ "$SYSTEM_CLEAN" == "true" ]]; then - run_system_cleanup -fi - -# iOS device backups -log_header "Checking 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"}') - echo -e " ๐Ÿ‘‰ Found ${GREEN}${backup_human}${NC}, you can delete it manually" - echo -e " ๐Ÿ‘‰ ${backup_dir}" - else - echo -e " โœจ Nothing to tidy" - fi -else - echo -e " โœจ Nothing to tidy" -fi - -# ===== 7. Summary ===== -log_header "Cleanup summary" -space_after=$(df / | tail -1 | awk '{print $4}') -current_space_after=$(df -h / | tail -1 | awk '{print $4}') - -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 "๐ŸŽ‰ User-level cleanup complete | ๐Ÿ’พ Freed space: ${RED}${freed_gb}GB${NC}" -else - echo "๐ŸŽ‰ User-level 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" -fi - -# Prompt for system-level cleanup if not already done -if [[ "$SYSTEM_CLEAN" != "true" ]]; then - echo "" - echo -e "${BLUE}๐Ÿ’ก Want even more space?${NC}" - echo -e " Run: ${GREEN}clean --system${NC} for deep system cleanup (requires password)" - echo -e " This cleans system cache, temp files, and performs memory optimization" -fi - -echo "===================================================================" diff --git a/install.sh b/install.sh index 9920127..cbfba45 100755 --- a/install.sh +++ b/install.sh @@ -1,173 +1,274 @@ #!/bin/bash +# Mole Installation Script +# Install Mole system cleanup tool to your system -# Clean Your Mac - Installation Script -set -e +set -euo pipefail +# Colors GREEN='\033[0;32m' BLUE='\033[0;34m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' +# Logging functions log_info() { echo -e "${BLUE}$1${NC}"; } log_success() { echo -e "${GREEN}โœ… $1${NC}"; } log_warning() { echo -e "${YELLOW}โš ๏ธ $1${NC}"; } log_error() { echo -e "${RED}โŒ $1${NC}"; } -echo -e "${BLUE}๐Ÿงน Installing Clean Mac...${NC}" +# Default installation directory +INSTALL_DIR="/usr/local/bin" +CONFIG_DIR="$HOME/.config/clean" -# Check system compatibility -if [[ "$OSTYPE" != "darwin"* ]]; then - log_error "This tool only supports macOS" - exit 1 -fi +show_help() { + cat << 'EOF' +Mole Installation Script +======================== -# Check for arguments -FORCE_DIRECT=false -UNINSTALL=false +USAGE: + ./install.sh [OPTIONS] -for arg in "$@"; do - case $arg in - --direct) - FORCE_DIRECT=true - ;; - --uninstall) - UNINSTALL=true - ;; - --help|-h) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --direct Force direct installation (skip Homebrew)" - echo " --uninstall Uninstall clean-mac" - echo " --help Show this help message" - exit 0 - ;; - esac -done +OPTIONS: + --prefix PATH Install to custom directory (default: /usr/local/bin) + --config PATH Config directory (default: ~/.config/clean) + --uninstall Uninstall mole + --help, -h Show this help -# Uninstall function -uninstall_clean() { - log_info "Uninstalling Clean Mac..." +EXAMPLES: + ./install.sh # Install to /usr/local/bin + ./install.sh --prefix ~/.local/bin # Install to custom directory + ./install.sh --uninstall # Uninstall mole - # Check if installed via Homebrew - if command -v brew >/dev/null 2>&1 && brew list clean-mac >/dev/null 2>&1; then - brew uninstall clean-mac - log_success "Uninstalled via Homebrew" - elif [[ -f "/usr/local/bin/clean" ]]; then - sudo rm -f "/usr/local/bin/clean" - log_success "Removed from /usr/local/bin" - else - log_warning "Clean Mac is not installed" - fi - exit 0 +The installer will: +1. Copy mole binary and scripts to the install directory +2. Set up config directory with all modules +3. Make the mole command available system-wide +EOF } -if [[ "$UNINSTALL" == "true" ]]; then - uninstall_clean -fi - -# Check if already installed -if command -v clean >/dev/null 2>&1 && [[ "$FORCE_DIRECT" != "true" ]]; then - log_warning "Clean Mac is already installed" - echo " Location: $(which clean)" - echo " Version: $(clean --help | head -1 || echo 'Unknown')" - echo "" - echo "Run with --uninstall to remove, or --direct to reinstall" - exit 0 -fi - -# Installation methods -install_via_homebrew() { - log_info "Installing via Homebrew..." - - if ! command -v brew >/dev/null 2>&1; then - log_error "Homebrew is not installed" - echo "Install Homebrew first: https://brew.sh" - return 1 - fi - - # Install with automatic tap addition - brew install tw93/tap/clean-mac - return $? +# Parse command line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --prefix) + INSTALL_DIR="$2" + shift 2 + ;; + --config) + CONFIG_DIR="$2" + shift 2 + ;; + --uninstall) + uninstall_mole + exit 0 + ;; + --help|-h) + show_help + exit 0 + ;; + *) + log_error "Unknown option: $1" + show_help + exit 1 + ;; + esac + done } -install_direct() { - log_info "Installing directly to /usr/local/bin..." +# Check system requirements +check_requirements() { + log_info "Checking system requirements..." - # Create directory - INSTALL_DIR="/usr/local/bin" + # Check if running on macOS + if [[ "$OSTYPE" != "darwin"* ]]; then + log_error "This tool is designed for macOS only" + exit 1 + fi + + # Check if install directory exists and is writable + if [[ ! -d "$(dirname "$INSTALL_DIR")" ]]; then + log_error "Parent directory $(dirname "$INSTALL_DIR") does not exist" + exit 1 + fi + + # Check if we need sudo for installation + if [[ ! -w "$(dirname "$INSTALL_DIR")" ]] && [[ "$INSTALL_DIR" == "/usr/local/bin" ]]; then + log_warning "Installation to $INSTALL_DIR requires sudo privileges" + fi + + log_success "System requirements check passed" +} + +# Create installation directories +create_directories() { + log_info "Creating directories..." + + # Create install directory if it doesn't exist if [[ ! -d "$INSTALL_DIR" ]]; then - log_info "Creating install directory..." - sudo mkdir -p "$INSTALL_DIR" + if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$(dirname "$INSTALL_DIR")" ]]; then + sudo mkdir -p "$INSTALL_DIR" + else + mkdir -p "$INSTALL_DIR" + fi fi - # Download script - log_info "Downloading latest version..." - TEMP_FILE=$(mktemp) + # Create config directory + mkdir -p "$CONFIG_DIR" + mkdir -p "$CONFIG_DIR/bin" + mkdir -p "$CONFIG_DIR/lib" - if ! curl -fsSL https://raw.githubusercontent.com/tw93/clean-mac/main/clean.sh -o "$TEMP_FILE"; then - log_error "Download failed" - rm -f "$TEMP_FILE" - return 1 - fi - - # Verify download - if [[ ! -s "$TEMP_FILE" ]]; then - log_error "Downloaded file is empty" - rm -f "$TEMP_FILE" - return 1 - fi - - # Install - log_info "Installing..." - sudo cp "$TEMP_FILE" "$INSTALL_DIR/clean" - sudo chmod +x "$INSTALL_DIR/clean" - rm "$TEMP_FILE" - - return 0 + log_success "Directories created" } -# Choose installation method -if [[ "$FORCE_DIRECT" == "true" ]]; then - install_direct - INSTALL_SUCCESS=$? -else - # Try Homebrew first, fallback to direct - if command -v brew >/dev/null 2>&1; then - log_info "Trying Homebrew installation..." - install_via_homebrew - INSTALL_SUCCESS=$? +# Install files +install_files() { + log_info "Installing mole files..." - if [[ $INSTALL_SUCCESS -ne 0 ]]; then - log_warning "Homebrew installation failed, using direct installation..." - install_direct - INSTALL_SUCCESS=$? + # Get the directory where this script is located + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + # Copy main executable + if [[ -f "$SCRIPT_DIR/mole" ]]; then + if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then + sudo cp "$SCRIPT_DIR/mole" "$INSTALL_DIR/mole" + sudo chmod +x "$INSTALL_DIR/mole" + else + cp "$SCRIPT_DIR/mole" "$INSTALL_DIR/mole" + chmod +x "$INSTALL_DIR/mole" fi + log_success "Main executable installed to $INSTALL_DIR/mole" else - log_info "Homebrew not found, using direct installation..." - install_direct - INSTALL_SUCCESS=$? + log_error "mole executable not found in $SCRIPT_DIR" + exit 1 fi -fi + + # Copy configuration and modules + if [[ -d "$SCRIPT_DIR/bin" ]]; then + cp -r "$SCRIPT_DIR/bin"/* "$CONFIG_DIR/bin/" + chmod +x "$CONFIG_DIR/bin"/* + log_success "Modules copied to $CONFIG_DIR/bin" + fi + + if [[ -d "$SCRIPT_DIR/lib" ]]; then + cp -r "$SCRIPT_DIR/lib"/* "$CONFIG_DIR/lib/" + log_success "Libraries copied to $CONFIG_DIR/lib" + fi + + # Copy other files if they exist + for file in README.md LICENSE; do + if [[ -f "$SCRIPT_DIR/$file" ]]; then + cp "$SCRIPT_DIR/$file" "$CONFIG_DIR/" + fi + done + + # Update the mole script to use the config directory + if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then + sudo sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole" + else + sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole" + fi +} # Verify installation -if [[ $INSTALL_SUCCESS -eq 0 ]] && command -v clean >/dev/null 2>&1; then - log_success "Installation completed successfully!" +verify_installation() { + log_info "Verifying installation..." + + if [[ -x "$INSTALL_DIR/mole" ]] && [[ -f "$CONFIG_DIR/lib/common.sh" ]]; then + log_success "Installation verified" + + # Test if mole command works + if "$INSTALL_DIR/mole" --help >/dev/null 2>&1; then + log_success "Mole command is working correctly" + else + log_warning "Mole command installed but may not be working properly" + fi + else + log_error "Installation verification failed" + exit 1 + fi +} + +# Add to PATH if needed +setup_path() { + # Check if install directory is in PATH + if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then + log_success "$INSTALL_DIR is already in PATH" + return + fi + + # Only suggest PATH setup for custom directories + if [[ "$INSTALL_DIR" != "/usr/local/bin" ]]; then + log_warning "$INSTALL_DIR is not in your PATH" + echo "" + echo "To use mole from anywhere, add this line to your shell profile:" + echo "export PATH=\"$INSTALL_DIR:\$PATH\"" + echo "" + echo "For example, add it to ~/.zshrc or ~/.bash_profile" + fi +} + +# Uninstall function +uninstall_mole() { + log_info "Uninstalling mole..." + + # Remove executable + if [[ -f "$INSTALL_DIR/mole" ]]; then + if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then + sudo rm -f "$INSTALL_DIR/mole" + else + rm -f "$INSTALL_DIR/mole" + fi + log_success "Removed executable from $INSTALL_DIR" + fi + + # Ask before removing config directory + if [[ -d "$CONFIG_DIR" ]]; then + echo "" + read -p "Remove configuration directory $CONFIG_DIR? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + rm -rf "$CONFIG_DIR" + log_success "Removed configuration directory" + else + log_info "Configuration directory preserved" + fi + fi + + log_success "Mole uninstalled successfully" +} + +# Main installation function +main() { + echo "๐Ÿ•ณ๏ธ Mole Installation Script" + echo "============================" echo "" - echo -e "${GREEN}Usage:${NC}" - echo -e " ${BLUE}clean${NC} - User-level cleanup (no password required)" - echo -e " ${BLUE}clean --system${NC} - Deep system cleanup (password required)" - echo -e " ${BLUE}clean --help${NC} - Show help message" + + check_requirements + create_directories + install_files + verify_installation + setup_path + echo "" - echo -e "${GREEN}Try it now:${NC} clean" -else - log_error "Installation failed" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" + log_success "Mole installed successfully!" echo "" - echo "You may need to:" - echo "1. Restart your terminal" - echo "2. Add /usr/local/bin to your PATH:" - echo " export PATH=\"/usr/local/bin:\$PATH\"" - echo "3. Run the installer with --direct flag" - exit 1 -fi \ No newline at end of file + echo "Usage:" + if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then + echo " mole # Interactive menu" + echo " mole clean # System cleanup" + echo " mole uninstall # Remove applications" + else + echo " $INSTALL_DIR/mole # Interactive menu" + echo " $INSTALL_DIR/mole clean # System cleanup" + echo " $INSTALL_DIR/mole uninstall # Remove applications" + fi + echo "" + echo "Configuration stored in: $CONFIG_DIR" + echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" +} + +# Run installation +parse_args "$@" +main \ No newline at end of file diff --git a/lib/app_selector.sh b/lib/app_selector.sh new file mode 100755 index 0000000..4eeac4c --- /dev/null +++ b/lib/app_selector.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# App selection functionality using the new menu system +# This replaces the complex interactive_app_selection function + +# Interactive app selection using the menu.sh library +select_apps_for_uninstall() { + if [[ ${#apps_data[@]} -eq 0 ]]; then + log_warning "No applications available for uninstallation" + return 1 + fi + + # Build menu options from apps_data + local -a menu_options=() + for app_data in "${apps_data[@]}"; do + IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$app_data" + + # The size is already formatted (e.g., "91M", "2.1G"), so use it directly + local size_str="Unknown" + if [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]]; then + size_str="$size" + fi + + # Format display name with better width control + local display_name + local max_name_length=25 + local truncated_name="$app_name" + + # Truncate app name if too long + if [[ ${#app_name} -gt $max_name_length ]]; then + truncated_name="${app_name:0:$((max_name_length-3))}..." + fi + + # Create aligned display format + display_name=$(printf "%-${max_name_length}s %8s | %s" "$truncated_name" "($size_str)" "$last_used") + menu_options+=("$display_name") + done + + echo "" + echo "๐Ÿ—‘๏ธ App Uninstaller" + echo "" + echo "Found ${#apps_data[@]} apps. Select apps to remove:" + echo "" + + # Load paginated menu system (arrow key navigation) + source "$(dirname "${BASH_SOURCE[0]}")/paginated_menu.sh" + + # Use paginated multi-select menu with arrow key navigation + local selected_indices + selected_indices=$(paginated_multi_select "Select Apps to Remove" "${menu_options[@]}") + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + echo "Cancelled" + return 1 + fi + + if [[ -z "$selected_indices" ]]; then + echo "No apps selected" + return 1 + fi + + # Build selected_apps array from indices + selected_apps=() + for idx in $selected_indices; do + # Validate that idx is a number + if [[ "$idx" =~ ^[0-9]+$ ]]; then + selected_apps+=("${apps_data[idx]}") + fi + done + + echo "Selected ${#selected_apps[@]} apps" + return 0 +} + +# Alternative simplified single-select interface for quick selection +quick_select_app() { + if [[ ${#apps_data[@]} -eq 0 ]]; then + log_warning "No applications available for uninstallation" + return 1 + fi + + # Build menu options from apps_data (same as above) + local -a menu_options=() + for app_data in "${apps_data[@]}"; do + IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$app_data" + + # The size is already formatted (e.g., "91M", "2.1G"), so use it directly + local size_str="Unknown" + if [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]]; then + size_str="$size" + fi + + # Format display name with better width control + local display_name + local max_name_length=25 + local truncated_name="$app_name" + + # Truncate app name if too long + if [[ ${#app_name} -gt $max_name_length ]]; then + truncated_name="${app_name:0:$((max_name_length-3))}..." + fi + + # Create aligned display format + display_name=$(printf "%-${max_name_length}s %8s | %s" "$truncated_name" "($size_str)" "$last_used") + menu_options+=("$display_name") + done + + echo "" + echo "๐Ÿ—‘๏ธ Quick Uninstall" + echo "" + + # Use single-select menu + if show_menu "Quick Uninstall" "${menu_options[@]}"; then + local selected_idx=$? + selected_apps=("${apps_data[selected_idx]}") + echo "โœ… Selected: ${menu_options[selected_idx]}" + return 0 + else + echo "โŒ Operation cancelled" + return 1 + fi +} + +# Show app selection mode menu +show_app_selection_mode() { + echo "" + echo "๐Ÿ—‘๏ธ Application Uninstaller" + echo "" + + local mode_options=( + "Batch Mode (select multiple apps with checkboxes)" + "Quick Mode (select one app at a time)" + "Exit Uninstaller" + ) + + if show_menu "Choose uninstall mode:" "${mode_options[@]}"; then + local mode=$? + case $mode in + 0) + select_apps_for_uninstall + return $? + ;; + 1) + quick_select_app + return $? + ;; + 2) + echo "Goodbye!" + return 1 + ;; + esac + else + echo "Operation cancelled" + return 1 + fi +} \ No newline at end of file diff --git a/lib/batch_uninstall.sh b/lib/batch_uninstall.sh new file mode 100755 index 0000000..49a4f5e --- /dev/null +++ b/lib/batch_uninstall.sh @@ -0,0 +1,200 @@ +#!/bin/bash + +# Batch uninstall functionality with minimal confirmations +# Replaces the overly verbose individual confirmation approach + +# 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" +} + +# Batch uninstall with single confirmation +batch_uninstall_applications() { + local total_size_freed=0 + + if [[ ${#selected_apps[@]} -eq 0 ]]; then + log_warning "No applications selected for uninstallation" + return 0 + fi + + # Pre-process: Check for running apps and calculate total impact + local -a running_apps=() + local total_estimated_size=0 + local -a app_details=() + + echo "๐Ÿ“‹ Analyzing selected applications..." + for selected_app in "${selected_apps[@]}"; do + IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" + + # Check if app is running + if pgrep -f "$app_name" >/dev/null 2>&1; then + running_apps+=("$app_name") + fi + + # Calculate size for summary + local app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0") + local related_files=$(find_app_files "$bundle_id" "$app_name") + local related_size_kb=$(calculate_total_size "$related_files") + local total_kb=$((app_size_kb + related_size_kb)) + ((total_estimated_size += total_kb)) + + # Store details for later use + app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$related_files") + done + + # Show summary and get batch confirmation + echo "" + echo "๐Ÿ“Š Uninstallation Summary:" + echo " โ€ข Applications to remove: ${#selected_apps[@]}" + + if [[ $total_estimated_size -gt 1048576 ]]; then + local size_display=$(echo "$total_estimated_size" | awk '{printf "%.2fGB", $1/1024/1024}') + elif [[ $total_estimated_size -gt 1024 ]]; then + local size_display=$(echo "$total_estimated_size" | awk '{printf "%.1fMB", $1/1024}') + else + local size_display="${total_estimated_size}KB" + fi + echo " โ€ข Estimated space to free: $size_display" + + if [[ ${#running_apps[@]} -gt 0 ]]; then + echo " โ€ข โš ๏ธ Running apps that will be force-quit:" + for app in "${running_apps[@]}"; do + echo " - $app" + done + fi + + echo "" + echo "Selected applications:" + for selected_app in "${selected_apps[@]}"; do + IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" + echo " โ€ข $app_name ($size)" + done + + echo "" + read -p "๐Ÿ—‘๏ธ Proceed with uninstalling ALL ${#selected_apps[@]} applications? This cannot be undone. (Y/n): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Nn]$ ]]; then + log_info "Uninstallation cancelled by user" + return 0 + fi + + # Force quit running apps first (batch) + if [[ ${#running_apps[@]} -gt 0 ]]; then + echo "" + log_info "Force quitting running applications..." + for app_name in "${running_apps[@]}"; do + echo " โ€ข Quitting $app_name..." + pkill -f "$app_name" 2>/dev/null || true + done + echo " โ€ข Waiting 3 seconds for apps to close..." + sleep 3 + fi + + # Perform uninstallations without individual confirmations + echo "" + log_info "Starting batch uninstallation..." + local success_count=0 + local failed_count=0 + + for detail in "${app_details[@]}"; do + IFS='|' read -r app_name app_path bundle_id total_kb related_files <<< "$detail" + + echo "" + echo "๐Ÿ—‘๏ธ Uninstalling: $app_name" + + # Remove the application + if rm -rf "$app_path" 2>/dev/null; then + echo -e " ${GREEN}โœ“${NC} Removed application" + + # Remove related files + local files_removed=0 + while IFS= read -r file; do + if [[ -n "$file" && -e "$file" ]]; then + if rm -rf "$file" 2>/dev/null; then + ((files_removed++)) + fi + fi + done <<< "$related_files" + + if [[ $files_removed -gt 0 ]]; then + echo -e " ${GREEN}โœ“${NC} Cleaned $files_removed related files" + fi + + ((total_size_freed += total_kb)) + ((success_count++)) + ((files_cleaned++)) + ((total_items++)) + + else + echo -e " ${RED}โœ—${NC} Failed to remove $app_name" + ((failed_count++)) + fi + done + + # Show final summary + echo "" + log_header "Uninstallation Complete" + + if [[ $success_count -gt 0 ]]; then + if [[ $total_size_freed -gt 1048576 ]]; then + local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}') + elif [[ $total_size_freed -gt 1024 ]]; then + local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}') + else + local freed_display="${total_size_freed}KB" + fi + log_success "Successfully uninstalled $success_count applications" + log_success "Freed $freed_display of disk space" + fi + + if [[ $failed_count -gt 0 ]]; then + log_warning "$failed_count applications failed to uninstall" + fi + + ((total_size_cleaned += total_size_freed)) +} \ No newline at end of file diff --git a/lib/better_menu.sh b/lib/better_menu.sh new file mode 100755 index 0000000..c628f93 --- /dev/null +++ b/lib/better_menu.sh @@ -0,0 +1,248 @@ +#!/bin/bash + +# Better menu system with proper terminal handling +# Uses tried-and-true approach for better compatibility + +# Terminal state management +save_terminal() { + stty -g 2>/dev/null || true +} + +restore_terminal() { + stty "$(save_terminal)" 2>/dev/null || true + printf '\033[?25h' >&2 # Show cursor + printf '\033[0m' >&2 # Reset colors +} + +# Read a single key (handles arrow keys properly) +read_key() { + local key + read -rsn1 key + case "$key" in + $'\033') # ESC sequence + read -rsn2 key 2>/dev/null || key="" + case "$key" in + '[A') echo "UP" ;; + '[B') echo "DOWN" ;; + *) echo "ESC" ;; + esac + ;; + ' ') echo "SPACE" ;; + '') echo "ENTER" ;; + 'q'|'Q') echo "QUIT" ;; + *) echo "OTHER" ;; + esac +} + +# Multi-select menu with proper pagination +multi_select_menu() { + local title="$1" + shift + local -a items=("$@") + + if [[ ${#items[@]} -eq 0 ]]; then + echo "Error: No items provided" >&2 + return 1 + fi + + local -a selected=() + local current=0 + local page_size=10 + local total=${#items[@]} + + # Initialize selection array + for ((i = 0; i < total; i++)); do + selected[i]=false + done + + # Save terminal state + local saved_state="" + saved_state=$(save_terminal) + trap 'test -n "$saved_state" && stty "$saved_state" 2>/dev/null; restore_terminal' EXIT INT TERM + + while true; do + # Calculate pagination + local start_page=$((current / page_size)) + local start_idx=$((start_page * page_size)) + local end_idx=$((start_idx + page_size - 1)) + if [[ $end_idx -ge $total ]]; then + end_idx=$((total - 1)) + fi + + # Clear screen and show header + printf '\033[2J\033[H' >&2 + echo "โ”Œโ”€โ”€โ”€ $title โ”€โ”€โ”€โ”" >&2 + echo "โ”‚ Found $total items (Page $((start_page + 1)) of $(((total + page_size - 1) / page_size))) โ”‚" >&2 + echo "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" >&2 + echo "" >&2 + + # Show items for current page + for ((i = start_idx; i <= end_idx; i++)); do + local marker=" " + local checkbox="โ˜" + + if [[ $i -eq $current ]]; then + marker="โ–ถ " + fi + + if [[ ${selected[i]} == "true" ]]; then + checkbox="โ˜‘" + fi + + printf "%s%s %s\n" "$marker" "$checkbox" "${items[i]}" >&2 + done + + echo "" >&2 + echo "Controls: โ†‘/โ†“=Navigate Space=Select/Deselect Enter=Confirm Q=Quit" >&2 + + # Show selection summary + local count=0 + for ((i = 0; i < total; i++)); do + if [[ ${selected[i]} == "true" ]]; then + ((count++)) + fi + done + echo "Selected: $count items" >&2 + echo "" >&2 + + # Read key + local key + key=$(read_key) + + case "$key" in + "UP") + ((current--)) + if [[ $current -lt 0 ]]; then + current=$((total - 1)) + fi + ;; + "DOWN") + ((current++)) + if [[ $current -ge $total ]]; then + current=0 + fi + ;; + "SPACE") + if [[ ${selected[current]} == "true" ]]; then + selected[current]=false + else + selected[current]=true + fi + ;; + "ENTER") + # Build result string + local result="" + for ((i = 0; i < total; i++)); do + if [[ ${selected[i]} == "true" ]]; then + result="$result $i" + fi + done + + # Clean up and return + restore_terminal + echo "${result# }" # Remove leading space + return 0 + ;; + "QUIT"|"ESC") + restore_terminal + return 1 + ;; + esac + done +} + +# Simple single-select menu +single_select_menu() { + local title="$1" + shift + local -a items=("$@") + + if [[ ${#items[@]} -eq 0 ]]; then + echo "Error: No items provided" >&2 + return 1 + fi + + local current=0 + local total=${#items[@]} + + # Save terminal state + local saved_state="" + saved_state=$(save_terminal) + trap 'test -n "$saved_state" && stty "$saved_state" 2>/dev/null; restore_terminal' EXIT INT TERM + + while true; do + # Clear screen and show header + printf '\033[2J\033[H' >&2 + echo "โ”Œโ”€โ”€โ”€ $title โ”€โ”€โ”€โ”" >&2 + echo "โ”‚ Choose one of $total items โ”‚" >&2 + echo "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" >&2 + echo "" >&2 + + # Show all items + for ((i = 0; i < total; i++)); do + local marker=" " + if [[ $i -eq $current ]]; then + marker="โ–ถ " + fi + printf "%s%s\n" "$marker" "${items[i]}" >&2 + done + + echo "" >&2 + echo "Controls: โ†‘/โ†“=Navigate Enter=Select Q=Quit" >&2 + echo "" >&2 + + # Read key + local key + key=$(read_key) + + case "$key" in + "UP") + ((current--)) + if [[ $current -lt 0 ]]; then + current=$((total - 1)) + fi + ;; + "DOWN") + ((current++)) + if [[ $current -ge $total ]]; then + current=0 + fi + ;; + "ENTER") + restore_terminal + echo "$current" + return 0 + ;; + "QUIT"|"ESC") + restore_terminal + return 1 + ;; + esac + done +} + +# Demo function for testing +demo() { + echo "=== Multi-select Demo ===" >&2 + local result + result=$(multi_select_menu "Test Multi-Select" "Option 1" "Option 2" "Option 3" "Option 4" "Option 5") + if [[ $? -eq 0 ]]; then + echo "You selected indices: $result" >&2 + else + echo "Selection cancelled" >&2 + fi + + echo "" >&2 + echo "=== Single-select Demo ===" >&2 + result=$(single_select_menu "Test Single-Select" "Choice A" "Choice B" "Choice C") + if [[ $? -eq 0 ]]; then + echo "You selected index: $result" >&2 + else + echo "Selection cancelled" >&2 + fi +} + +# Run demo if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + demo +fi \ No newline at end of file diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..df46884 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Mac Tools - Common Functions Library +# Shared utilities and functions for all modules + +# Color definitions +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +PURPLE='\033[0;35m' +RED='\033[0;31m' +NC='\033[0m' + +# Logging functions +log_info() { echo -e "${BLUE}$1${NC}"; } +log_success() { echo -e "${GREEN}โœ… $1${NC}"; } +log_warning() { echo -e "${YELLOW}โš ๏ธ $1${NC}"; } +log_error() { echo -e "${RED}โŒ $1${NC}"; } +log_header() { echo -e "\n${PURPLE}โ–ถ $1${NC}"; } + +# System detection +detect_architecture() { + if [[ "$(uname -m)" == "arm64" ]]; then + echo "Apple Silicon" + else + echo "Intel" + fi +} + +get_free_space() { + df -h / | awk 'NR==2 {print $4}' +} + +# Common UI functions +clear_screen() { + printf '\033[2J\033[H' +} + +show_header() { + local title="$1" + local subtitle="$2" + + clear_screen + echo -e "${BLUE}$title${NC}" + echo "=================================================" + if [[ -n "$subtitle" ]]; then + echo -e "${PURPLE}$subtitle${NC}" + echo "" + fi +} + +# Keyboard input handling (simple and robust) +read_key() { + local key rest + IFS= read -rsn1 key || return 1 + + # Some terminals can yield empty on Enter with -n1; treat as ENTER + if [[ -z "$key" ]]; then + echo "ENTER" + return 0 + fi + + case "$key" in + $'\n'|$'\r') echo "ENTER" ;; + ' ') echo " " ;; + 'q'|'Q') echo "QUIT" ;; + 'a'|'A') echo "ALL" ;; + 'n'|'N') echo "NONE" ;; + '?') echo "HELP" ;; + $'\x1b') + # Read the next two bytes within 1s; works well on macOS bash 3.2 + if IFS= read -rsn2 -t 1 rest 2>/dev/null; then + case "$rest" in + "[A") echo "UP" ;; + "[B") echo "DOWN" ;; + "[C") echo "RIGHT" ;; + "[D") echo "LEFT" ;; + *) echo "ESC" ;; + esac + else + echo "ESC" + fi + ;; + *) echo "OTHER" ;; + esac +} + +# Menu display helper +show_menu_option() { + local number="$1" + local text="$2" + local selected="$3" + + if [[ "$selected" == "true" ]]; then + echo -e "${BLUE}โ–ถ $number. $text${NC}" + else + echo " $number. $text" + fi +} + +# Error handling +handle_error() { + local message="$1" + local exit_code="${2:-1}" + + log_error "$message" + exit "$exit_code" +} + +# File size utilities +get_human_size() { + local path="$1" + du -sh "$path" 2>/dev/null | cut -f1 || echo "N/A" +} + +# Permission checks +check_sudo() { + if ! sudo -n true 2>/dev/null; then + return 1 + fi + return 0 +} + +request_sudo() { + echo "This operation requires administrator privileges." + echo -n "Please enter your password: " + read -s password + echo + if echo "$password" | sudo -S true 2>/dev/null; then + return 0 + else + log_error "Invalid password or cancelled" + return 1 + fi +} diff --git a/lib/menu.sh b/lib/menu.sh new file mode 100755 index 0000000..6bee66c --- /dev/null +++ b/lib/menu.sh @@ -0,0 +1,367 @@ +#!/bin/bash + +# Simple interactive menu selector with arrow key support +# No external dependencies, compatible with most bash versions + +declare -a menu_options=() +declare -i selected=0 +declare -i menu_size=0 + +# ANSI escape sequences +readonly ESC=$'\033' +readonly UP="${ESC}[A" +readonly DOWN="${ESC}[B" +readonly ENTER=$'\n' +readonly CLEAR_LINE="${ESC}[2K" +readonly HIDE_CURSOR="${ESC}[?25l" +readonly SHOW_CURSOR="${ESC}[?25h" + +# Set terminal to raw mode for reading single characters +setup_terminal() { + # Block until at least 1 byte to avoid false ENTER on empty reads + stty -echo -icanon min 1 time 0 +} + +# Restore terminal to normal mode +restore_terminal() { + stty echo icanon + printf "%s" "$SHOW_CURSOR" +} + +# Draw the menu +draw_menu() { + local force_full_redraw="${1:-true}" + printf "%s" "$HIDE_CURSOR" + + if [[ "$force_full_redraw" == "true" ]]; then + # Full redraw: clear and redraw all lines + for ((i = 0; i < menu_size; i++)); do + printf "\r%s" "$CLEAR_LINE" + + if [[ $i -eq $selected ]]; then + printf "โ–ถ \033[1;32m%s\033[0m\n" "${menu_options[i]}" + else + printf " %s\n" "${menu_options[i]}" + fi + done + + # Move cursor back to the beginning and save position + printf "${ESC}[%dA" $menu_size + printf "${ESC}7" # Save cursor position + else + # Quick update: only update changed lines + printf "${ESC}8" # Restore cursor position + + for ((i = 0; i < menu_size; i++)); do + printf "\r%s" "$CLEAR_LINE" + + if [[ $i -eq $selected ]]; then + printf "โ–ถ \033[1;32m%s\033[0m\n" "${menu_options[i]}" + else + printf " %s\n" "${menu_options[i]}" + fi + done + + # Move cursor back to the beginning + printf "${ESC}[%dA" $menu_size + printf "${ESC}7" # Save cursor position again + fi +} + +# Read a single key +read_key() { + local key + IFS= read -rsn1 key 2>/dev/null || return 1 + + case "$key" in + $'\033') + local key2 key3 + if IFS= read -rsn1 -t 0.2 key2 2>/dev/null; then + if [[ "$key2" == "[" ]]; then + if IFS= read -rsn1 -t 0.2 key3 2>/dev/null; then + case "$key3" in + 'A') echo "UP" ;; + 'B') echo "DOWN" ;; + 'C') echo "RIGHT" ;; + 'D') echo "LEFT" ;; + *) echo "OTHER" ;; + esac + else + echo "OTHER" + fi + else + echo "OTHER" + fi + else + echo "OTHER" + fi + ;; + $'\n'|$'\r') echo "ENTER" ;; + ' ') echo " " ;; + 'q'|'Q') echo "QUIT" ;; + *) echo "$key" ;; + esac +} + +# Main menu function +# Usage: show_menu "Title" "option1" "option2" "option3" ... +show_menu() { + local title="$1" + shift + + # Initialize menu options + menu_options=("$@") + menu_size=${#menu_options[@]} + selected=0 + + # Check if we have options + if [[ $menu_size -eq 0 ]]; then + echo "Error: No menu options provided" >&2 + return 1 + fi + + # Setup terminal + setup_terminal + trap restore_terminal EXIT INT TERM + + # Display title + if [[ -n "$title" ]]; then + printf "\n\033[1;34m%s\033[0m\n\n" "$title" + fi + + # Initial draw + draw_menu true + + # Main loop + local first_iteration=true + while true; do + local key=$(read_key) + + case "$key" in + "UP") + ((selected--)) + if [[ $selected -lt 0 ]]; then + selected=$((menu_size - 1)) + fi + draw_menu false # Quick update + ;; + "DOWN") + ((selected++)) + if [[ $selected -ge $menu_size ]]; then + selected=0 + fi + draw_menu false # Quick update + ;; + "ENTER") + # Clear the menu + for ((i = 0; i < menu_size; i++)); do + printf "\r%s\n" "$CLEAR_LINE" >&2 + done + printf "${ESC}[%dA" $menu_size >&2 + + # Show selection + printf "Selected: \033[1;32m%s\033[0m\n\n" "${menu_options[selected]}" + + restore_terminal + return $selected + ;; + "q"|"Q") + restore_terminal + echo "Cancelled." >&2 + return 255 + ;; + [0-9]) + # Jump to numbered option + local num=$((key - 1)) + if [[ $num -ge 0 && $num -lt $menu_size ]]; then + selected=$num + draw_menu + fi + ;; + # Ignore other keys + esac + done +} + +# Multi-select menu function +# Usage: show_multi_menu "Title" "option1" "option2" "option3" ... +show_multi_menu() { + local title="$1" + shift + + # Initialize menu options + menu_options=("$@") + menu_size=${#menu_options[@]} + selected=0 + + # Array to track selected items + declare -a selected_items=() + for ((i = 0; i < menu_size; i++)); do + selected_items[i]=false + done + + # Check if we have options + if [[ $menu_size -eq 0 ]]; then + echo "Error: No menu options provided" >&2 + return 1 + fi + + # Setup terminal + setup_terminal + trap restore_terminal EXIT INT TERM + + # Display title + if [[ -n "$title" ]]; then + printf "\n\033[1;34m%s\033[0m\n" "$title" >&2 + printf "\033[0;36mUse SPACE to select/deselect, ENTER to confirm, Q to quit\033[0m\n\n" >&2 + fi + + # Draw multi-select menu + draw_multi_menu() { + local force_full_redraw="${1:-true}" + printf "%s" "$HIDE_CURSOR" >&2 + + if [[ "$force_full_redraw" == "true" ]]; then + # Full redraw + for ((i = 0; i < menu_size; i++)); do + printf "\r%s" "$CLEAR_LINE" >&2 + + local checkbox="โ˜" + if [[ ${selected_items[i]} == "true" ]]; then + checkbox="\033[1;32mโ˜‘\033[0m" + fi + + if [[ $i -eq $selected ]]; then + printf "โ–ถ %s \033[1;32m%s\033[0m\n" "$checkbox" "${menu_options[i]}" >&2 + else + printf " %s %s\n" "$checkbox" "${menu_options[i]}" >&2 + fi + done + + # Move cursor back to the beginning and save position + printf "${ESC}[%dA" $menu_size >&2 + printf "${ESC}7" >&2 # Save cursor position + else + # Quick update + printf "${ESC}8" >&2 # Restore cursor position + + for ((i = 0; i < menu_size; i++)); do + printf "\r%s" "$CLEAR_LINE" >&2 + + local checkbox="โ˜" + if [[ ${selected_items[i]} == "true" ]]; then + checkbox="\033[1;32mโ˜‘\033[0m" + fi + + if [[ $i -eq $selected ]]; then + printf "โ–ถ %s \033[1;32m%s\033[0m\n" "$checkbox" "${menu_options[i]}" >&2 + else + printf " %s %s\n" "$checkbox" "${menu_options[i]}" >&2 + fi + done + + # Move cursor back to the beginning and save position + printf "${ESC}[%dA" $menu_size >&2 + printf "${ESC}7" >&2 # Save cursor position + fi + } + + # Initial draw + draw_multi_menu true + + # Main loop + while true; do + local key=$(read_key) + + case "$key" in + "UP") + ((selected--)) + if [[ $selected -lt 0 ]]; then + selected=$((menu_size - 1)) + fi + draw_multi_menu false # Quick update + ;; + "DOWN") + ((selected++)) + if [[ $selected -ge $menu_size ]]; then + selected=0 + fi + draw_multi_menu false # Quick update + ;; + " ") + # Toggle selection + if [[ ${selected_items[selected]} == "true" ]]; then + selected_items[selected]="false" + else + selected_items[selected]="true" + fi + draw_multi_menu false # Quick update + ;; + "ENTER") + # Clear the menu + for ((i = 0; i < menu_size; i++)); do + printf "\r%s\n" "$CLEAR_LINE" >&2 + done + printf "${ESC}[%dA" $menu_size >&2 + + # Show selections to stderr so it doesn't interfere with return value + local has_selection=false + printf "Selected items:\n" >&2 + for ((i = 0; i < menu_size; i++)); do + if [[ ${selected_items[i]} == "true" ]]; then + printf " \033[1;32m%s\033[0m\n" "${menu_options[i]}" >&2 + has_selection=true + fi + done + + if [[ $has_selection == "false" ]]; then + printf " None\n" >&2 + fi + printf "\n" >&2 + + restore_terminal + + # Return selected indices as space-separated string + local result="" + for ((i = 0; i < menu_size; i++)); do + if [[ ${selected_items[i]} == "true" ]]; then + result="$result $i" + fi + done + echo "${result# }" # Remove leading space + return 0 + ;; + "q"|"Q"|"ESC") + restore_terminal + echo "Cancelled." >&2 + return 255 + ;; + esac + done +} + +# Example usage function +demo_menu() { + echo "=== Single Select Demo ===" + if show_menu "Choose an action:" "Install package" "Update system" "Clean cache" "Exit"; then + local choice=$? + echo "You selected option $choice" + fi + + echo -e "\n=== Multi Select Demo ===" + local selections=$(show_multi_menu "Choose packages to install:" "git" "vim" "curl" "htop" "tree") + if [[ $? -eq 0 && -n "$selections" ]]; then + echo "Selected indices: $selections" + # Convert indices to actual values + local options=("git" "vim" "curl" "htop" "tree") + echo "Selected packages:" + for idx in $selections; do + echo " - ${options[idx]}" + done + fi +} + +# If script is run directly, show demo +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + demo_menu +fi diff --git a/lib/menu_backup.sh b/lib/menu_backup.sh new file mode 100755 index 0000000..40b4bd6 --- /dev/null +++ b/lib/menu_backup.sh @@ -0,0 +1,300 @@ +#!/bin/bash + +# Simple interactive menu selector with arrow key support +# No external dependencies, compatible with most bash versions + +declare -a menu_options=() +declare -i selected=0 +declare -i menu_size=0 + +# ANSI escape sequences +readonly ESC=$'\033' +readonly UP="${ESC}[A" +readonly DOWN="${ESC}[B" +readonly ENTER=$'\n' +readonly CLEAR_LINE="${ESC}[2K" +readonly HIDE_CURSOR="${ESC}[?25l" +readonly SHOW_CURSOR="${ESC}[?25h" + +# Set terminal to raw mode for reading single characters +setup_terminal() { + stty -echo -icanon time 0 min 0 +} + +# Restore terminal to normal mode +restore_terminal() { + stty echo icanon + printf "%s" "$SHOW_CURSOR" +} + +# Draw the menu +draw_menu() { + printf "%s" "$HIDE_CURSOR" + + for ((i = 0; i < menu_size; i++)); do + printf "\r%s" "$CLEAR_LINE" + + if [[ $i -eq $selected ]]; then + printf "โ–ถ \033[1;32m%s\033[0m\n" "${menu_options[i]}" + else + printf " %s\n" "${menu_options[i]}" + fi + done + + # Move cursor back to the beginning + printf "${ESC}[%dA" $menu_size +} + +# Read a single key +read_key() { + local key + IFS= read -r -n1 key 2>/dev/null + + if [[ $key == $ESC ]]; then + # Handle escape sequences + IFS= read -r -n2 key 2>/dev/null + case "$key" in + '[A') echo "UP" ;; + '[B') echo "DOWN" ;; + *) echo "ESC" ;; + esac + elif [[ $key == "" ]]; then + echo "ENTER" + else + echo "$key" + fi +} + +# Main menu function +# Usage: show_menu "Title" "option1" "option2" "option3" ... +show_menu() { + local title="$1" + shift + + # Initialize menu options + menu_options=("$@") + menu_size=${#menu_options[@]} + selected=0 + + # Check if we have options + if [[ $menu_size -eq 0 ]]; then + echo "Error: No menu options provided" >&2 + return 1 + fi + + # Setup terminal + setup_terminal + trap restore_terminal EXIT INT TERM + + # Display title + if [[ -n "$title" ]]; then + printf "\n\033[1;34m%s\033[0m\n\n" "$title" + fi + + # Initial draw + draw_menu + + # Main loop + while true; do + local key=$(read_key) + + case "$key" in + "UP") + ((selected--)) + if [[ $selected -lt 0 ]]; then + selected=$((menu_size - 1)) + fi + draw_menu + ;; + "DOWN") + ((selected++)) + if [[ $selected -ge $menu_size ]]; then + selected=0 + fi + draw_menu + ;; + "ENTER") + # Clear the menu + for ((i = 0; i < menu_size; i++)); do + printf "\r%s\n" "$CLEAR_LINE" >&2 + done + printf "${ESC}[%dA" $menu_size >&2 + + # Show selection + printf "Selected: \033[1;32m%s\033[0m\n\n" "${menu_options[selected]}" + + restore_terminal + return $selected + ;; + "q"|"Q") + restore_terminal + echo "Cancelled." >&2 + return 255 + ;; + [0-9]) + # Jump to numbered option + local num=$((key - 1)) + if [[ $num -ge 0 && $num -lt $menu_size ]]; then + selected=$num + draw_menu + fi + ;; + esac + done +} + +# Multi-select menu function +# Usage: show_multi_menu "Title" "option1" "option2" "option3" ... +show_multi_menu() { + local title="$1" + shift + + # Initialize menu options + menu_options=("$@") + menu_size=${#menu_options[@]} + selected=0 + + # Array to track selected items + declare -a selected_items=() + for ((i = 0; i < menu_size; i++)); do + selected_items[i]=false + done + + # Check if we have options + if [[ $menu_size -eq 0 ]]; then + echo "Error: No menu options provided" >&2 + return 1 + fi + + # Setup terminal + setup_terminal + trap restore_terminal EXIT INT TERM + + # Display title + if [[ -n "$title" ]]; then + printf "\n\033[1;34m%s\033[0m\n" "$title" >&2 + printf "\033[0;36mUse SPACE to select/deselect, ENTER to confirm, Q to quit\033[0m\n\n" >&2 + fi + + # Draw multi-select menu + draw_multi_menu() { + printf "%s" "$HIDE_CURSOR" >&2 + + for ((i = 0; i < menu_size; i++)); do + printf "\r%s" "$CLEAR_LINE" >&2 + + local checkbox="โ˜" + if [[ ${selected_items[i]} == "true" ]]; then + checkbox="\033[1;32mโ˜‘\033[0m" + fi + + if [[ $i -eq $selected ]]; then + printf "โ–ถ %s \033[1;32m%s\033[0m\n" "$checkbox" "${menu_options[i]}" >&2 + else + printf " %s %s\n" "$checkbox" "${menu_options[i]}" >&2 + fi + done + + # Move cursor back to the beginning + printf "${ESC}[%dA" $menu_size >&2 + } + + # Initial draw + draw_multi_menu + + # Main loop + while true; do + local key=$(read_key) + + case "$key" in + "UP") + ((selected--)) + if [[ $selected -lt 0 ]]; then + selected=$((menu_size - 1)) + fi + draw_multi_menu + ;; + "DOWN") + ((selected++)) + if [[ $selected -ge $menu_size ]]; then + selected=0 + fi + draw_multi_menu + ;; + " ") + # Toggle selection + if [[ ${selected_items[selected]} == "true" ]]; then + selected_items[selected]="false" + else + selected_items[selected]="true" + fi + draw_multi_menu + ;; + "ENTER") + # Clear the menu + for ((i = 0; i < menu_size; i++)); do + printf "\r%s\n" "$CLEAR_LINE" >&2 + done + printf "${ESC}[%dA" $menu_size >&2 + + # Show selections to stderr so it doesn't interfere with return value + local has_selection=false + printf "Selected items:\n" >&2 + for ((i = 0; i < menu_size; i++)); do + if [[ ${selected_items[i]} == "true" ]]; then + printf " \033[1;32m%s\033[0m\n" "${menu_options[i]}" >&2 + has_selection=true + fi + done + + if [[ $has_selection == "false" ]]; then + printf " None\n" >&2 + fi + printf "\n" >&2 + + restore_terminal + + # Return selected indices as space-separated string + local result="" + for ((i = 0; i < menu_size; i++)); do + if [[ ${selected_items[i]} == "true" ]]; then + result="$result $i" + fi + done + echo "${result# }" # Remove leading space + return 0 + ;; + "q"|"Q") + restore_terminal + echo "Cancelled." >&2 + return 255 + ;; + esac + done +} + +# Example usage function +demo_menu() { + echo "=== Single Select Demo ===" + if show_menu "Choose an action:" "Install package" "Update system" "Clean cache" "Exit"; then + local choice=$? + echo "You selected option $choice" + fi + + echo -e "\n=== Multi Select Demo ===" + local selections=$(show_multi_menu "Choose packages to install:" "git" "vim" "curl" "htop" "tree") + if [[ $? -eq 0 && -n "$selections" ]]; then + echo "Selected indices: $selections" + # Convert indices to actual values + local options=("git" "vim" "curl" "htop" "tree") + echo "Selected packages:" + for idx in $selections; do + echo " - ${options[idx]}" + done + fi +} + +# If script is run directly, show demo +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + demo_menu +fi \ No newline at end of file diff --git a/lib/native_menu.sh b/lib/native_menu.sh new file mode 100755 index 0000000..9384a19 --- /dev/null +++ b/lib/native_menu.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# Simple native bash menu using the built-in select command +# This is the most reliable approach with zero dependencies + +# Multi-select using native bash select with checkboxes simulation +multi_select_native() { + local title="$1" + shift + local -a items=("$@") + + if [[ ${#items[@]} -eq 0 ]]; then + echo "Error: No items provided" >&2 + return 1 + fi + + echo "=== $title ===" >&2 + echo "Select multiple items (enter numbers separated by spaces, or 'done' when finished):" >&2 + echo "" >&2 + + # Display items with numbers + for ((i = 0; i < ${#items[@]}; i++)); do + printf "%2d) %s\n" $((i + 1)) "${items[i]}" >&2 + done + echo "" >&2 + + local -a selected_indices=() + + while true; do + echo "Currently selected: ${#selected_indices[@]} items" >&2 + if [[ ${#selected_indices[@]} -gt 0 ]]; then + echo "Selected indices: ${selected_indices[*]}" >&2 + fi + echo "" >&2 + + read -p "Enter selection (numbers, 'all', 'none', or 'done'): " -r input >&2 + + case "$input" in + "done"|"") + break + ;; + "all") + selected_indices=() + for ((i = 0; i < ${#items[@]}; i++)); do + selected_indices+=($i) + done + echo "Selected all ${#items[@]} items" >&2 + ;; + "none") + selected_indices=() + echo "Cleared all selections" >&2 + ;; + *) + # Parse space-separated numbers + read -ra nums <<< "$input" + for num in "${nums[@]}"; do + if [[ "$num" =~ ^[0-9]+$ ]] && [[ $num -ge 1 ]] && [[ $num -le ${#items[@]} ]]; then + local idx=$((num - 1)) + # Check if already selected + local already_selected=false + if [[ ${#selected_indices[@]} -gt 0 ]]; then + for selected in "${selected_indices[@]}"; do + if [[ $selected -eq $idx ]]; then + already_selected=true + break + fi + done + fi + + if [[ $already_selected == false ]]; then + selected_indices+=($idx) + echo "Added: ${items[idx]}" >&2 + else + echo "Already selected: ${items[idx]}" >&2 + fi + else + echo "Invalid selection: $num (must be 1-${#items[@]})" >&2 + fi + done + ;; + esac + echo "" >&2 + done + + # Convert to space-separated string and return + local result="" + if [[ ${#selected_indices[@]} -gt 0 ]]; then + for idx in "${selected_indices[@]}"; do + result="$result $idx" + done + echo "${result# }" # Remove leading space + else + echo "" # Return empty string for no selections + fi + return 0 +} + +# Simple single-select using native bash select +single_select_native() { + local title="$1" + shift + local -a items=("$@") + + if [[ ${#items[@]} -eq 0 ]]; then + echo "Error: No items provided" >&2 + return 1 + fi + + echo "=== $title ===" >&2 + + # Use PS3 to customize the select prompt + local PS3="Please select an option (1-${#items[@]}): " + + select item in "${items[@]}" "Cancel"; do + if [[ -n "$item" ]]; then + if [[ "$item" == "Cancel" ]]; then + return 1 + else + # Find the index of selected item + for ((i = 0; i < ${#items[@]}; i++)); do + if [[ "${items[i]}" == "$item" ]]; then + echo "$i" + return 0 + fi + done + fi + else + echo "Invalid selection. Please try again." >&2 + fi + done 2>&2 # Redirect select dialog to stderr +} + +# Demo function +demo_native() { + echo "=== Multi-select Demo ===" >&2 + local result + result=$(multi_select_native "Choose Applications" "App 1" "App 2" "App 3" "App 4" "App 5") + if [[ $? -eq 0 ]]; then + echo "You selected indices: '$result'" >&2 + else + echo "Selection cancelled" >&2 + fi + + echo "" >&2 + echo "=== Single-select Demo ===" >&2 + result=$(single_select_native "Choose One App" "Option A" "Option B" "Option C") + if [[ $? -eq 0 ]]; then + echo "You selected index: $result" >&2 + else + echo "Selection cancelled" >&2 + fi +} + +# Run demo if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + demo_native +fi \ No newline at end of file diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh new file mode 100755 index 0000000..608e813 --- /dev/null +++ b/lib/paginated_menu.sh @@ -0,0 +1,312 @@ +#!/bin/bash + +# Proper paginated menu with arrow key navigation +# 10 items per page, up/down to navigate, space to select, left/right to change pages + +# Terminal control functions +hide_cursor() { printf '\033[?25l' >&2; } +show_cursor() { printf '\033[?25h' >&2; } +clear_screen() { printf '\033[2J\033[H' >&2; } +enter_alt_screen() { tput smcup >/dev/null 2>&1 || true; } +leave_alt_screen() { tput rmcup >/dev/null 2>&1 || true; } +disable_wrap() { printf '\033[?7l' >&2; } # disable line wrap +enable_wrap() { printf '\033[?7h' >&2; } + +# Read single key with arrow key support (macOS bash 3.2 friendly) +read_key() { + local key seq + IFS= read -rsn1 key || return 1 + + # Some terminals may yield empty on Enter with -n1 + if [[ -z "$key" ]]; then + echo "ENTER" + return 0 + fi + + case "$key" in + $'\033') + # Read next two bytes within 1s: "[A", "[B", ... + if IFS= read -rsn2 -t 1 seq 2>/dev/null; then + case "$seq" in + "[A") echo "UP" ;; + "[B") echo "DOWN" ;; + "[C") echo "RIGHT" ;; + "[D") echo "LEFT" ;; + *) echo "OTHER" ;; + esac + else + echo "OTHER" + fi + ;; + ' ') echo "SPACE" ;; + $'\n'|$'\r') echo "ENTER" ;; + 'q'|'Q') echo "QUIT" ;; + 'a'|'A') echo "ALL" ;; + 'n'|'N') echo "NONE" ;; + '?') echo "HELP" ;; + *) echo "OTHER" ;; + esac +} + +# Paginated multi-select menu +paginated_multi_select() { + local title="$1" + shift + local -a items=("$@") + + local total_items=${#items[@]} + local items_per_page=10 # Reduced for better readability + local total_pages=$(( (total_items + items_per_page - 1) / items_per_page )) + local current_page=0 + local cursor_pos=0 # Position within current page (0-9) + local -a selected=() + + # Initialize selection array + for ((i = 0; i < total_items; i++)); do + selected[i]=false + done + + # Cleanup function + cleanup() { + show_cursor + stty echo 2>/dev/null || true + stty icanon 2>/dev/null || true + leave_alt_screen + enable_wrap + } + trap cleanup EXIT INT TERM + + # Setup terminal for optimal responsiveness + stty -echo -icanon min 1 time 0 2>/dev/null || true + enter_alt_screen + disable_wrap + hide_cursor + + # Main display function + first_draw=1 + # Helper: print one cleared line + print_line() { + printf "\r\033[2K%s\n" "$1" >&2 + } + + # Helper: render one item line at given page position + render_item_line() { + local page_pos=$1 + local start_idx=$((current_page * items_per_page)) + local i=$((start_idx + page_pos)) + local checkbox="โ˜" + local cursor_marker=" " + [[ ${selected[i]} == true ]] && checkbox="โ˜‘" + if [[ $page_pos -eq $cursor_pos ]]; then + cursor_marker="โ–ถ " + printf "\r\033[2K\033[7m%s%s %s\033[0m\n" "$cursor_marker" "$checkbox" "${items[i]}" >&2 + else + printf "\r\033[2K%s%s %s\n" "$cursor_marker" "$checkbox" "${items[i]}" >&2 + fi + } + + # Helper: move cursor to top-left anchor saved by tput sc + to_anchor() { tput rc >/dev/null 2>&1 || true; } + + # Full draw of entire screen - simplified for stability + draw_menu() { + # Always do full screen redraw for reliability + clear_screen + + # Simple header + printf "%s\n" "$title" >&2 + printf "%s\n" "$(printf '=%.0s' $(seq 1 ${#title}))" >&2 + + # Status bar + 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" \ + $((current_page + 1)) $total_pages $total_items $selected_count >&2 + print_line "" + + # Calculate page boundaries + local start_idx=$((current_page * items_per_page)) + local end_idx=$((start_idx + items_per_page - 1)) + [[ $end_idx -ge $total_items ]] && end_idx=$((total_items - 1)) + + # Display items for current page + for ((i = start_idx; i <= end_idx; i++)); do + local page_pos=$((i - start_idx)) + render_item_line "$page_pos" + done + + # Fill empty slots to always print items_per_page lines + local items_on_page=$((end_idx - start_idx + 1)) + for ((i = items_on_page; i < items_per_page; i++)); do + print_line "" + done + + print_line "" + print_line "โ†‘โ†“: Navigate | Space: Select | Enter: Confirm | Q: Exit" + } + + # Help screen + show_help() { + clear_screen + echo "App Uninstaller - Help" >&2 + echo "======================" >&2 + echo >&2 + echo " โ†‘ / โ†“ Navigate up/down" >&2 + echo " โ† / โ†’ Previous/next page" >&2 + echo " Space Select/deselect app" >&2 + echo " Enter Confirm selection" >&2 + echo " A Select all" >&2 + echo " N Deselect all" >&2 + echo " Q Exit" >&2 + echo >&2 + read -p "Press any key to continue..." -n 1 >&2 + } + + # Main loop - simplified to always do full redraws for stability + while true; do + draw_menu # Always full redraw to avoid display issues + + local key=$(read_key) + + # Immediate exit key + if [[ "$key" == "QUIT" ]]; then + cleanup + return 1 + fi + + case "$key" in + "UP") + if [[ $cursor_pos -gt 0 ]]; then + ((cursor_pos--)) + elif [[ $current_page -gt 0 ]]; then + ((current_page--)) + cursor_pos=$((items_per_page - 1)) + local start_idx=$((current_page * items_per_page)) + local end_idx=$((start_idx + items_per_page - 1)) + [[ $end_idx -ge $total_items ]] && cursor_pos=$((total_items - start_idx - 1)) + fi + ;; + "DOWN") + local start_idx=$((current_page * items_per_page)) + local items_on_page=$((total_items - start_idx)) + [[ $items_on_page -gt $items_per_page ]] && items_on_page=$items_per_page + + if [[ $cursor_pos -lt $((items_on_page - 1)) ]]; then + ((cursor_pos++)) + elif [[ $current_page -lt $((total_pages - 1)) ]]; then + ((current_page++)) + cursor_pos=0 + fi + ;; + "LEFT") + if [[ $current_page -gt 0 ]]; then + ((current_page--)) + cursor_pos=0 + fi + ;; + "RIGHT") + if [[ $current_page -lt $((total_pages - 1)) ]]; then + ((current_page++)) + cursor_pos=0 + fi + ;; + "PGUP") + current_page=0 + cursor_pos=0 + ;; + "PGDOWN") + current_page=$((total_pages - 1)) + cursor_pos=0 + ;; + "SPACE") + local actual_idx=$((current_page * items_per_page + cursor_pos)) + if [[ $actual_idx -lt $total_items ]]; then + if [[ ${selected[actual_idx]} == true ]]; then + selected[actual_idx]=false + else + selected[actual_idx]=true + fi + fi + ;; + "ALL") + for ((i = 0; i < total_items; i++)); do + selected[i]=true + done + ;; + "NONE") + for ((i = 0; i < total_items; i++)); do + selected[i]=false + done + ;; + "HELP") + show_help + ;; + "ENTER") + # If no items are selected, select the current item + local has_selection=false + for ((i = 0; i < total_items; i++)); do + if [[ ${selected[i]} == true ]]; then + has_selection=true + break + fi + done + + if [[ $has_selection == false ]]; then + # Select current item under cursor + local actual_idx=$((current_page * items_per_page + cursor_pos)) + if [[ $actual_idx -lt $total_items ]]; then + selected[actual_idx]=true + fi + fi + + # Build result + local result="" + for ((i = 0; i < total_items; i++)); do + if [[ ${selected[i]} == true ]]; then + result="$result $i" + fi + done + cleanup + echo "${result# }" + return 0 + ;; + *) + # Ignore unrecognized keys - just continue the loop + ;; + esac + done +} + +# Demo function +demo_paginated() { + echo "=== Paginated Multi-select Demo ===" >&2 + + # Create test data + local test_items=() + for i in {1..35}; do + test_items+=("Application $i ($(( (RANDOM % 500) + 50 ))MB)") + done + + local result + result=$(paginated_multi_select "Choose Applications to Uninstall" "${test_items[@]}") + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + if [[ -n "$result" ]]; then + echo "Selected indices: $result" >&2 + echo "Count: $(echo $result | wc -w | tr -d ' ')" >&2 + else + echo "No items selected" >&2 + fi + else + echo "Selection cancelled" >&2 + fi +} + +# Run demo if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + demo_paginated +fi diff --git a/lib/simple_menu.sh b/lib/simple_menu.sh new file mode 100644 index 0000000..baaa0cd --- /dev/null +++ b/lib/simple_menu.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Simple, clean menu implementation that properly separates output + +# Simple single-select menu - returns selected index +simple_select() { + local title="$1" + shift + local -a options=("$@") + local selected=0 + local key + + # Clear screen and show header + clear >&2 + echo "=== $title ===" >&2 + echo "" >&2 + + while true; do + # Show options + for ((i = 0; i < ${#options[@]}; i++)); do + if [[ $i -eq $selected ]]; then + echo "โ–ถ ${options[i]}" >&2 + else + echo " ${options[i]}" >&2 + fi + done + echo "" >&2 + echo "Use โ†‘/โ†“ to navigate, ENTER to select, Q to quit" >&2 + + # Read key + read -rsn1 key + case "$key" in + $'\x1b') + # Arrow key sequence + read -rsn2 key + case "$key" in + '[A') # Up + ((selected--)) + if [[ $selected -lt 0 ]]; then + selected=$((${#options[@]} - 1)) + fi + ;; + '[B') # Down + ((selected++)) + if [[ $selected -ge ${#options[@]} ]]; then + selected=0 + fi + ;; + esac + ;; + '') # Enter + echo "$selected" + return 0 + ;; + 'q'|'Q') + return 1 + ;; + esac + + # Clear screen for next iteration + clear >&2 + echo "=== $title ===" >&2 + echo "" >&2 + done +} + +# Multi-select menu - returns space-separated indices +simple_multi_select() { + local title="$1" + shift + local -a options=("$@") + local selected=0 + local -a selected_items=() + local key + + # Initialize selected items array + for ((i = 0; i < ${#options[@]}; i++)); do + selected_items[i]=false + done + + clear >&2 + echo "=== $title ===" >&2 + echo "" >&2 + + while true; do + # Show options + for ((i = 0; i < ${#options[@]}; i++)); do + local checkbox="โ˜" + if [[ ${selected_items[i]} == "true" ]]; then + checkbox="โ˜‘" + fi + + if [[ $i -eq $selected ]]; then + echo "โ–ถ $checkbox ${options[i]}" >&2 + else + echo " $checkbox ${options[i]}" >&2 + fi + done + echo "" >&2 + echo "Use โ†‘/โ†“ to navigate, SPACE to select/deselect, ENTER to confirm, Q to quit" >&2 + + # Read key + read -rsn1 key + case "$key" in + $'\x1b') + # Arrow key sequence + read -rsn2 key + case "$key" in + '[A') # Up + ((selected--)) + if [[ $selected -lt 0 ]]; then + selected=$((${#options[@]} - 1)) + fi + ;; + '[B') # Down + ((selected++)) + if [[ $selected -ge ${#options[@]} ]]; then + selected=0 + fi + ;; + esac + ;; + ' ') # Space - toggle selection + if [[ ${selected_items[selected]} == "true" ]]; then + selected_items[selected]=false + else + selected_items[selected]=true + fi + ;; + '') # Enter - confirm + local result="" + for ((i = 0; i < ${#options[@]}; i++)); do + if [[ ${selected_items[i]} == "true" ]]; then + result="$result $i" + fi + done + echo "${result# }" # Remove leading space + return 0 + ;; + 'q'|'Q') + return 1 + ;; + esac + + # Clear screen for next iteration + clear >&2 + echo "=== $title ===" >&2 + echo "" >&2 + done +} \ No newline at end of file diff --git a/lib/smart_menu.sh b/lib/smart_menu.sh new file mode 100755 index 0000000..ebd0172 --- /dev/null +++ b/lib/smart_menu.sh @@ -0,0 +1,268 @@ +#!/bin/bash + +# Smart menu with pagination and search for large lists +# Much better UX for handling many items + +# Smart multi-select with search and pagination +smart_multi_select() { + local title="$1" + shift + local -a all_items=("$@") + + if [[ ${#all_items[@]} -eq 0 ]]; then + echo "Error: No items provided" >&2 + return 1 + fi + + local -a selected_indices=() + local -a filtered_items=() + local -a filtered_indices=() + local search_term="" + local page_size=15 + local current_page=0 + + # Function to filter items based on search + filter_items() { + filtered_items=() + filtered_indices=() + + if [[ -z "$search_term" ]]; then + # No search, show all items + filtered_items=("${all_items[@]}") + for ((i = 0; i < ${#all_items[@]}; i++)); do + filtered_indices+=($i) + done + else + # Filter items that contain search term (case insensitive) + for ((i = 0; i < ${#all_items[@]}; i++)); do + if [[ "${all_items[i],,}" == *"${search_term,,}"* ]]; then + filtered_items+=("${all_items[i]}") + filtered_indices+=($i) + fi + done + fi + } + + # Function to display current page + show_page() { + local total_filtered=${#filtered_items[@]} + local total_pages=$(( (total_filtered + page_size - 1) / page_size )) + local start_idx=$((current_page * page_size)) + local end_idx=$((start_idx + page_size - 1)) + + if [[ $end_idx -ge $total_filtered ]]; then + end_idx=$((total_filtered - 1)) + fi + + printf '\033[2J\033[H' >&2 + echo "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ" >&2 + echo "โ”‚ $title" >&2 + echo "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค" >&2 + echo "โ”‚ Total: ${#all_items[@]} | Filtered: $total_filtered | Selected: ${#selected_indices[@]} โ”‚" >&2 + + if [[ -n "$search_term" ]]; then + echo "โ”‚ Search: '$search_term' โ”‚" >&2 + fi + + if [[ $total_pages -gt 1 ]]; then + echo "โ”‚ Page $(($current_page + 1)) of $total_pages โ”‚" >&2 + fi + echo "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ" >&2 + echo "" >&2 + + if [[ $total_filtered -eq 0 ]]; then + echo "No items match your search." >&2 + echo "" >&2 + else + # Show items for current page + for ((i = start_idx; i <= end_idx && i < total_filtered; i++)); do + local item_idx=${filtered_indices[i]} + local display_num=$((i + 1)) + + # Check if this item is selected + local is_selected=false + if [[ ${#selected_indices[@]} -gt 0 ]]; then + for selected in "${selected_indices[@]}"; do + if [[ $selected -eq $item_idx ]]; then + is_selected=true + break + fi + done + fi + + if [[ $is_selected == true ]]; then + printf "%3d) โœ“ %s\n" "$display_num" "${filtered_items[i]}" >&2 + else + printf "%3d) %s\n" "$display_num" "${filtered_items[i]}" >&2 + fi + done + fi + + echo "" >&2 + echo "Commands:" >&2 + echo " Numbers: Select items (e.g., '1-5', '1 3 7', '10-15')" >&2 + echo " /search: Filter items (e.g., '/chrome')" >&2 + echo " n/p: Next/Previous page | all: Select all | none: Clear all" >&2 + echo " done: Finish selection | quit: Cancel" >&2 + echo "" >&2 + } + + # Main loop + while true; do + filter_items + show_page + + read -p "Enter command: " -r input >&2 + + case "$input" in + "done"|"") + break + ;; + "quit"|"q") + return 1 + ;; + "all") + selected_indices=() + for idx in "${filtered_indices[@]}"; do + selected_indices+=($idx) + done + echo "Selected all filtered items (${#filtered_indices[@]})" >&2 + ;; + "none") + selected_indices=() + echo "Cleared all selections" >&2 + ;; + "n"|"next") + local total_pages=$(( (${#filtered_items[@]} + page_size - 1) / page_size )) + if [[ $((current_page + 1)) -lt $total_pages ]]; then + ((current_page++)) + else + echo "Already on last page" >&2 + fi + ;; + "p"|"prev") + if [[ $current_page -gt 0 ]]; then + ((current_page--)) + else + echo "Already on first page" >&2 + fi + ;; + /*) + # Search functionality + search_term="${input#/}" + current_page=0 + echo "Searching for: '$search_term'" >&2 + ;; + *) + # Parse selection input + parse_selection "$input" + ;; + esac + + [[ "$input" != "n" && "$input" != "next" && "$input" != "p" && "$input" != "prev" ]] && sleep 1 + done + + # Return selected indices + local result="" + if [[ ${#selected_indices[@]} -gt 0 ]]; then + for idx in "${selected_indices[@]}"; do + result="$result $idx" + done + echo "${result# }" + else + echo "" + fi + return 0 +} + +# Parse selection input (supports ranges and individual numbers) +parse_selection() { + local input="$1" + local start_idx=$((current_page * page_size)) + + # Split input by spaces + read -ra parts <<< "$input" + + for part in "${parts[@]}"; do + if [[ "$part" =~ ^[0-9]+$ ]]; then + # Single number + local display_num=$part + local array_idx=$((display_num - 1)) + + if [[ $array_idx -ge 0 && $array_idx -lt ${#filtered_items[@]} ]]; then + local real_idx=${filtered_indices[array_idx]} + toggle_selection "$real_idx" + else + echo "Invalid selection: $part (range: 1-${#filtered_items[@]})" >&2 + fi + + elif [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then + # Range like 1-5 + local start_num=${BASH_REMATCH[1]} + local end_num=${BASH_REMATCH[2]} + + for ((num = start_num; num <= end_num; num++)); do + local array_idx=$((num - 1)) + if [[ $array_idx -ge 0 && $array_idx -lt ${#filtered_items[@]} ]]; then + local real_idx=${filtered_indices[array_idx]} + toggle_selection "$real_idx" + fi + done + + else + echo "Invalid format: $part (use numbers, ranges like '1-5', or commands)" >&2 + fi + done +} + +# Toggle selection of an item +toggle_selection() { + local idx=$1 + local already_selected=false + local pos_to_remove=-1 + + # Check if already selected + if [[ ${#selected_indices[@]} -gt 0 ]]; then + for ((i = 0; i < ${#selected_indices[@]}; i++)); do + if [[ ${selected_indices[i]} -eq $idx ]]; then + already_selected=true + pos_to_remove=$i + break + fi + done + fi + + if [[ $already_selected == true ]]; then + # Remove from selection + unset selected_indices[$pos_to_remove] + selected_indices=("${selected_indices[@]}") # Reindex array + echo "Removed: ${all_items[idx]}" >&2 + else + # Add to selection + selected_indices+=($idx) + echo "Added: ${all_items[idx]}" >&2 + fi +} + +# Demo function +demo_smart() { + local test_apps=() + for i in {1..50}; do + test_apps+=("Test App $i (${RANDOM}MB)") + done + + echo "=== Smart Multi-select Demo ===" >&2 + local result + result=$(smart_multi_select "Choose Applications to Remove" "${test_apps[@]}") + if [[ $? -eq 0 ]]; then + echo "You selected indices: '$result'" >&2 + echo "Selected ${result// /,} out of ${#test_apps[@]} items" >&2 + else + echo "Selection cancelled" >&2 + fi +} + +# Run demo if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + demo_smart +fi \ No newline at end of file diff --git a/mole b/mole new file mode 100755 index 0000000..faf67df --- /dev/null +++ b/mole @@ -0,0 +1,169 @@ +#!/bin/bash +# Mac Tools - Main Entry Point +# A comprehensive suite of macOS maintenance tools +# +# ๐Ÿงน Clean - Remove junk files and optimize system +# ๐Ÿ—‘๏ธ Uninstall - Remove applications completely +# ๐Ÿ“ฆ Install - Install useful applications +# +# Usage: +# ./mac-tools # Interactive main menu +# ./mac-tools clean # Direct clean mode +# ./mac-tools uninstall # Direct uninstall mode +# ./mac-tools install # Direct install mode +# ./mac-tools --help # Show help + +set -euo pipefail + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common functions +source "$SCRIPT_DIR/lib/common.sh" + +# Version info +VERSION="2.0.0" + +show_help() { + cat << EOF +Mole v$VERSION - System Cleanup Tool +===================================== + +USAGE: + mole [command] + +COMMANDS: + mole # Interactive main menu + mole clean # Clean system (will ask for password if needed) + mole uninstall # Remove applications completely + mole --help # Show this help message + +For more information, visit: https://github.com/tw93/clean-mac +EOF +} + +show_main_menu() { + local selected="${1:-1}" + local redraw_full="${2:-true}" + + if [[ "$redraw_full" == "true" ]]; then + show_header "๐Ÿ•ณ๏ธ Mole v$VERSION" "System Cleanup Tool" + echo "๐ŸŽ Detected: $(detect_architecture) | ๐Ÿ’พ Free space: $(get_free_space)" + echo "" + echo "What would you like to do?" + echo "" + fi + + # Save cursor position before printing menu items + printf '\033[s' + + show_menu_option 1 "Clean System - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)" + show_menu_option 2 "Uninstall Apps - Completely remove applications" "$([[ $selected -eq 2 ]] && echo true || echo false)" + show_menu_option 3 "Help & Information - Usage guide and tips" "$([[ $selected -eq 3 ]] && echo true || echo false)" + show_menu_option 4 "Exit - Close Clean" "$([[ $selected -eq 4 ]] && echo true || echo false)" + + if [[ "$redraw_full" == "true" ]]; then + echo "" + echo -e "${BLUE}Use โ†‘/โ†“ arrows to navigate, ENTER to select, ESC/q to quit${NC}" + fi +} + +interactive_main_menu() { + local current_option=1 + local total_options=4 + local first_draw=true + + # Set up signal trapping to handle Ctrl+C gracefully + trap 'echo ""; echo "Thank you for using Mole!"; exit 0' INT + + while true; do + if [[ "$first_draw" == "true" ]]; then + show_main_menu $current_option true + first_draw=false + else + # Restore cursor to saved position and only redraw menu items + printf '\033[u' + show_main_menu $current_option false + fi + + # Use a more robust way to capture key input + local key + key=$(read_key) + + # Check if read_key failed + if [[ $? -ne 0 ]]; then + continue + fi + + case "$key" in + "UP") + ((current_option > 1)) && ((current_option--)) + ;; + "DOWN") + ((current_option < total_options)) && ((current_option++)) + ;; + "ENTER") + case $current_option in + 1) + exec "$SCRIPT_DIR/bin/clean.sh" + ;; + 2) + exec "$SCRIPT_DIR/bin/uninstall.sh" + ;; + 3) + show_help + echo "" + read -p "Press any key to continue..." -n 1 -r + echo # Add newline after key press + ;; + 4) + echo "" + echo "Thank you for using Mole!" + exit 0 + ;; + esac + ;; + "QUIT"|"ESC") + echo "" + echo "Thank you for using Mole!" + exit 0 + ;; + "1"|"2"|"3"|"4") + case $key in + 1) exec "$SCRIPT_DIR/bin/clean.sh" ;; + 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; + 3) show_help; read -p "Press any key to continue..." -n 1 -r; echo ;; + 4) echo ""; echo "Thank you for using Mole!"; exit 0 ;; + esac + ;; + *) + # Ignore unrecognized keys + ;; + esac + done +} + +main() { + case "${1:-""}" in + "clean") + exec "$SCRIPT_DIR/bin/clean.sh" + ;; + "uninstall") + exec "$SCRIPT_DIR/bin/uninstall.sh" + ;; + "help"|"--help"|"-h") + show_help + exit 0 + ;; + "") + interactive_main_menu + ;; + *) + echo "Unknown command: $1" + echo "Use 'mac-tools --help' for usage information." + exit 1 + ;; + esac +} + +main "$@" \ No newline at end of file