diff --git a/README.md b/README.md
index 39c0df9..32666e5 100644
--- a/README.md
+++ b/README.md
@@ -1,91 +1,88 @@
-

-
-# Clean Mac
-
-**๐งน Deep Clean Your Mac with One Click**
-
-[](https://github.com/tw93/clean-mac/releases) [](https://formulae.brew.sh/formula/clean-mac) [](https://github.com/tw93/clean-mac/blob/main/LICENSE) [](https://github.com/tw93/clean-mac)
+

+
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