mirror of
https://github.com/tw93/Mole.git
synced 2026-02-10 23:34:16 +00:00
🎨 Continue to upgrade to easy to use
This commit is contained in:
126
README.md
126
README.md
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- 🦡 Deep-clean hidden caches, logs, and temp files in one sweep
|
- 🦡 **Deep System Cleanup** - Remove hidden caches, logs, and temp files in one sweep
|
||||||
- 🛡 Guardrails built in: skip vital macOS and input method data
|
- 📦 **Smart Uninstall** - Complete app removal with all related files and folders
|
||||||
- 📦 Smart uninstall removes apps together with every leftover directory
|
- ⚡️ **Fast Interactive UI** - Arrow-key navigation with pagination for large lists
|
||||||
- ⚡️ Fast arrow-key TUI with pagination for big app lists
|
- 🧹 **Massive Space Recovery** - Reclaim 100GB+ of wasted disk space
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -21,77 +21,97 @@ curl -fsSL https://raw.githubusercontent.com/tw93/mole/main/install.sh | bash
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mole # Interactive main menu
|
mole # Interactive main menu
|
||||||
mole clean # Deeper system cleanup
|
mole clean # Deep system cleanup
|
||||||
mole uninstall # Interactive app uninstaller
|
mole uninstall # Interactive app uninstaller
|
||||||
mole --help # Show help
|
mole --help # Show help
|
||||||
```
|
```
|
||||||
|
|
||||||
### Quick Peek
|
## Examples
|
||||||
|
|
||||||
|
### Deep System Cleanup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ mole clean
|
$ mole clean
|
||||||
🕳️ Mole - Deeper system cleanup
|
|
||||||
==================================================
|
|
||||||
|
|
||||||
🍎 Detected: Apple Silicon M3 | 💾 Free space: 245GB
|
Starting user-level cleanup...
|
||||||
|
|
||||||
------------------------------ System essentials ------------------------------
|
▶ System essentials
|
||||||
✓ User app cache (20.8GB)
|
✓ User app cache (28 items) (45.2GB)
|
||||||
✓ User app logs (190MB)
|
✓ User app logs (15 items) (2.1GB)
|
||||||
✓ Trash (5.4GB)
|
✓ Trash (12.3GB)
|
||||||
|
|
||||||
------------------------------ Browser cleanup --------------------------------
|
▶ Browser cleanup
|
||||||
✓ Safari cache (320MB)
|
✓ Chrome cache (8 items) (8.4GB)
|
||||||
✓ Chrome cache (1.2GB)
|
✓ Safari cache (2.1GB)
|
||||||
✓ Arc cache (460MB)
|
✓ Arc cache (3.2GB)
|
||||||
|
|
||||||
------------------------------ Developer tools --------------------------------
|
▶ Extended developer caches
|
||||||
✓ npm cache cleaned
|
✓ Xcode derived data (9.1GB)
|
||||||
✓ Docker resources cleaned
|
✓ Node.js cache (4 items) (14.2GB)
|
||||||
✓ Homebrew cache (940MB)
|
✓ VS Code cache (1.4GB)
|
||||||
|
|
||||||
------------------------------ Cleanup summary --------------------------------
|
▶ Applications
|
||||||
🎉 Cleanup complete | 💾 Freed space: 38.6GB
|
✓ JetBrains cache (3.8GB)
|
||||||
📊 Items processed: 356 | 💾 Free space now: 253GB
|
✓ Slack cache (2.2GB)
|
||||||
===================================================================
|
✓ Discord cache (1.8GB)
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
🎉 CLEANUP COMPLETE!
|
||||||
|
💾 Space freed: 95.50GB | Free space now: 223.5GB
|
||||||
|
📊 Files cleaned: 6420 | Categories processed: 6
|
||||||
|
====================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Smart App Uninstaller
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mole uninstall
|
||||||
|
|
||||||
|
Select Apps to Remove
|
||||||
|
|
||||||
|
▶ ☑ Adobe Creative Cloud (12.4G) | Old
|
||||||
|
☐ WeChat (2.1G) | Recent
|
||||||
|
☐ Final Cut Pro (3.8G) | Recent
|
||||||
|
|
||||||
|
🗑️ Uninstalling: Adobe Creative Cloud
|
||||||
|
✓ Removed application
|
||||||
|
✓ Cleaned 45 related files
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
🎉 UNINSTALLATION COMPLETE!
|
||||||
|
🗑️ Apps uninstalled: 1 | Space freed: 12.4GB
|
||||||
|
====================================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
## What Mole Cleans
|
## What Mole Cleans
|
||||||
|
|
||||||
| Category | Items Cleaned | Safety |
|
| Category | Targets | Recovery |
|
||||||
|---|---|---|
|
|----------|---------|----------|
|
||||||
| 🗂️ System | App caches, logs, trash, crash reports, QuickLook thumbnails | Safe |
|
| 🗂️ **System** | App caches, logs, trash, crash reports | 20-50GB |
|
||||||
| 🌐 Browsers | Safari, Chrome, Edge, Arc, Brave, Firefox, Opera, Vivaldi | Safe |
|
| 🌐 **Browsers** | Safari, Chrome, Edge, Arc, Firefox cache | 5-15GB |
|
||||||
| 💻 Developer | Node.js/npm, Python/pip, Go, Rust/cargo, Docker, Homebrew, Git | Safe |
|
| 💻 **Developer** | npm, pip, Docker, Homebrew, Xcode | 15-40GB |
|
||||||
| 🛠️ IDEs | Xcode, VS Code, JetBrains, Android Studio, Unity, Figma | Safe |
|
| 📱 **Apps** | Slack, Discord, Teams, Notion cache | 3-10GB |
|
||||||
| 📱 Apps | Common app caches (e.g., Slack, Discord, Teams, Notion, 1Password) | Safe |
|
|
||||||
| 🍎 Apple Silicon | Rosetta 2, media services, user activity caches | Safe |
|
|
||||||
|
|
||||||
## Smart Uninstall
|
## What Mole Uninstalls
|
||||||
|
|
||||||
- Fast scan of `/Applications` with system-app filtering (e.g., `com.apple.*`)
|
| Component | Files Removed | Examples |
|
||||||
- Ranks apps by last used time and shows size hints
|
|-----------|--------------|----------|
|
||||||
- Two modes: batch multi-select (checkbox) or quick single-select
|
| 🎯 **App Bundle** | Main .app executable | `/Applications/App.app` |
|
||||||
- Detects running apps and force‑quits them before removal
|
| 📁 **Support Data** | App-specific user data | `~/Library/Application Support/AppName` |
|
||||||
- Single confirmation for the whole batch with estimated space to free
|
| 💾 **Cache Files** | Temporary & cache data | `~/Library/Caches/com.company.app` |
|
||||||
- Cleans thoroughly and safely:
|
| ⚙️ **Preferences** | Settings & config files | `~/Library/Preferences/com.app.plist` |
|
||||||
- App bundle (`.app`)
|
| 📝 **Logs & Reports** | Crash reports & logs | `~/Library/Logs/AppName` |
|
||||||
- `~/Library/Application Support/<App|BundleID>`
|
| 📦 **Containers** | Sandboxed app data | `~/Library/Containers/com.app.id` |
|
||||||
- `~/Library/Caches/<BundleID>`
|
|
||||||
- `~/Library/Preferences/<BundleID>.plist`
|
|
||||||
- `~/Library/Logs/<App|BundleID>`
|
|
||||||
- `~/Library/Saved Application State/<BundleID>.savedState`
|
|
||||||
- `~/Library/Containers/<BundleID>` and related Group Containers
|
|
||||||
- Final summary: apps removed, files cleaned, total disk space reclaimed
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If Mole has been helpful to you:
|
If Mole helps you recover disk space:
|
||||||
|
|
||||||
- **Star this repository** and share with fellow Mac users
|
- ⭐ **Star this repository** and share with fellow Mac users
|
||||||
- **Report issues** or suggest new cleanup targets
|
- 🐛 **Report issues** via GitHub Issues
|
||||||
- I have two cats. If Mole helps you, you can <a href="https://miaoyan.app/cats.html?name=Mole" target="_blank">feed them canned food 🥩🍤</a>
|
- 🎁 I have two cats. You can <a href="https://miaoyan.app/cats.html?name=Mole" target="_blank">feed them canned food 🥩🍤</a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License © [tw93](https://github.com/tw93) - Feel free to enjoy and contribute to open source.
|
- Follow the MIT License.
|
||||||
|
- Please feel free to enjoy and participate in open source.
|
||||||
|
|||||||
360
bin/clean.sh
360
bin/clean.sh
@@ -13,135 +13,6 @@ SYSTEM_CLEAN=false
|
|||||||
IS_M_SERIES=$([ "$(uname -m)" = "arm64" ] && echo "true" || echo "false")
|
IS_M_SERIES=$([ "$(uname -m)" = "arm64" ] && echo "true" || echo "false")
|
||||||
total_items=0
|
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
|
# Tracking variables
|
||||||
TRACK_SECTION=0
|
TRACK_SECTION=0
|
||||||
SECTION_ACTIVITY=0
|
SECTION_ACTIVITY=0
|
||||||
@@ -228,7 +99,8 @@ stop_spinner() {
|
|||||||
start_section() {
|
start_section() {
|
||||||
TRACK_SECTION=1
|
TRACK_SECTION=1
|
||||||
SECTION_ACTIVITY=0
|
SECTION_ACTIVITY=0
|
||||||
log_header "$1"
|
echo ""
|
||||||
|
echo -e "${PURPLE}▶ $1${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
end_section() {
|
end_section() {
|
||||||
@@ -255,15 +127,15 @@ safe_clean() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local removed_any=0
|
local removed_any=0
|
||||||
|
local total_size_bytes=0
|
||||||
|
local total_count=0
|
||||||
|
|
||||||
for path in "${targets[@]}"; do
|
for path in "${targets[@]}"; do
|
||||||
local size_bytes=0
|
local size_bytes=0
|
||||||
local size_human="0B"
|
|
||||||
local count=0
|
local count=0
|
||||||
|
|
||||||
if [[ -e "$path" ]]; then
|
if [[ -e "$path" ]]; then
|
||||||
size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
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 ' ')
|
count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
if [[ "$count" -eq 0 || "$size_bytes" -eq 0 ]]; then
|
if [[ "$count" -eq 0 || "$size_bytes" -eq 0 ]]; then
|
||||||
@@ -271,47 +143,55 @@ safe_clean() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf "$path" 2>/dev/null || true
|
rm -rf "$path" 2>/dev/null || true
|
||||||
|
|
||||||
|
((total_size_bytes += size_bytes))
|
||||||
|
((total_count += count))
|
||||||
|
removed_any=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Only show output if something was actually cleaned
|
||||||
|
if [[ $removed_any -eq 1 ]]; then
|
||||||
|
local size_human
|
||||||
|
if [[ $total_size_bytes -gt 1048576 ]]; then # > 1GB
|
||||||
|
size_human=$(echo "$total_size_bytes" | awk '{printf "%.1fGB", $1/1024/1024}')
|
||||||
|
elif [[ $total_size_bytes -gt 1024 ]]; then # > 1MB
|
||||||
|
size_human=$(echo "$total_size_bytes" | awk '{printf "%.1fMB", $1/1024}')
|
||||||
else
|
else
|
||||||
# For non-existent paths, show as cleaned with realistic placeholder values
|
size_human="${total_size_bytes}KB"
|
||||||
size_human="4.0K"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local label="$description"
|
local label="$description"
|
||||||
if [[ ${#targets[@]} -gt 1 ]]; then
|
if [[ ${#targets[@]} -gt 1 ]]; then
|
||||||
label+=" [$(basename "$path")]"
|
label+=" (${#targets[@]} items)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e " ${GREEN}✓${NC} $label ${GREEN}($size_human)${NC}"
|
echo -e " ${GREEN}✓${NC} $label ${GREEN}($size_human)${NC}"
|
||||||
((files_cleaned+=count))
|
((files_cleaned+=total_count))
|
||||||
((total_size_cleaned+=size_bytes))
|
((total_size_cleaned+=total_size_bytes))
|
||||||
((total_items++))
|
((total_items++))
|
||||||
removed_any=1
|
|
||||||
note_activity
|
note_activity
|
||||||
done
|
fi
|
||||||
|
|
||||||
LAST_CLEAN_RESULT=$removed_any
|
LAST_CLEAN_RESULT=$removed_any
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
start_cleanup() {
|
start_cleanup() {
|
||||||
clear
|
echo "Removing app caches, browser data, developer tools, and temporary files..."
|
||||||
echo "🕳️ Mole - Deeper system cleanup"
|
|
||||||
echo "=================================================="
|
|
||||||
echo ""
|
|
||||||
echo "This will clean: App caches & logs, Browser data, Developer tools, Temporary files & more..."
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if we're in an interactive terminal
|
# Check if we're in an interactive terminal
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
# Interactive mode - ask for password
|
# Interactive mode - ask for password
|
||||||
echo "For deeper system cleanup, administrator password is needed."
|
echo "Enter admin password for system-level cleanup (or press Enter to skip):"
|
||||||
echo -n "Enter password (or press Enter to skip): "
|
echo -n "> "
|
||||||
read -s password
|
read -s password
|
||||||
echo ""
|
echo ""
|
||||||
else
|
else
|
||||||
# Non-interactive mode - skip password prompt
|
# Non-interactive mode - skip password prompt
|
||||||
password=""
|
password=""
|
||||||
echo "Running in non-interactive mode, skipping system-level cleanup."
|
log_info "Running in non-interactive mode, skipping system-level cleanup."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
|
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
|
||||||
@@ -331,16 +211,7 @@ start_cleanup() {
|
|||||||
|
|
||||||
perform_cleanup() {
|
perform_cleanup() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "🕳️ Mole - Deeper system cleanup"
|
echo "🍎 $(detect_architecture) | 💾 Free space: $(get_free_space)"
|
||||||
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
|
# Get initial space
|
||||||
space_before=$(df / | tail -1 | awk '{print $4}')
|
space_before=$(df / | tail -1 | awk '{print $4}')
|
||||||
@@ -365,6 +236,7 @@ perform_cleanup() {
|
|||||||
end_section
|
end_section
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# ===== 2. User essentials =====
|
# ===== 2. User essentials =====
|
||||||
start_section "System essentials"
|
start_section "System essentials"
|
||||||
safe_clean ~/Library/Caches/* "User app cache"
|
safe_clean ~/Library/Caches/* "User app cache"
|
||||||
@@ -390,6 +262,7 @@ perform_cleanup() {
|
|||||||
safe_clean ~/Library/Caches/com.apple.bird* "iCloud cache"
|
safe_clean ~/Library/Caches/com.apple.bird* "iCloud cache"
|
||||||
end_section
|
end_section
|
||||||
|
|
||||||
|
|
||||||
# ===== 2. Browsers =====
|
# ===== 2. Browsers =====
|
||||||
start_section "Browser cleanup"
|
start_section "Browser cleanup"
|
||||||
# Safari
|
# Safari
|
||||||
@@ -415,6 +288,7 @@ perform_cleanup() {
|
|||||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||||
end_section
|
end_section
|
||||||
|
|
||||||
|
|
||||||
# ===== 3. Developer tools =====
|
# ===== 3. Developer tools =====
|
||||||
start_section "Developer tools"
|
start_section "Developer tools"
|
||||||
# Node.js ecosystem
|
# Node.js ecosystem
|
||||||
@@ -495,7 +369,6 @@ perform_cleanup() {
|
|||||||
safe_clean ~/.pnpm-store/* "pnpm store cache"
|
safe_clean ~/.pnpm-store/* "pnpm store cache"
|
||||||
safe_clean ~/.cache/typescript/* "TypeScript cache"
|
safe_clean ~/.cache/typescript/* "TypeScript cache"
|
||||||
safe_clean ~/.cache/electron/* "Electron cache"
|
safe_clean ~/.cache/electron/* "Electron cache"
|
||||||
safe_clean ~/.cache/yarn/* "Yarn cache"
|
|
||||||
safe_clean ~/.turbo/* "Turbo cache"
|
safe_clean ~/.turbo/* "Turbo cache"
|
||||||
safe_clean ~/.next/* "Next.js cache"
|
safe_clean ~/.next/* "Next.js cache"
|
||||||
safe_clean ~/.vite/* "Vite cache"
|
safe_clean ~/.vite/* "Vite cache"
|
||||||
@@ -542,13 +415,11 @@ perform_cleanup() {
|
|||||||
# Cloud and container tools
|
# Cloud and container tools
|
||||||
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
|
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
|
||||||
safe_clean ~/.cache/terraform/* "Terraform cache"
|
safe_clean ~/.cache/terraform/* "Terraform cache"
|
||||||
safe_clean ~/.kube/cache/* "Kubernetes cache"
|
|
||||||
|
|
||||||
# API and network development tools
|
# API and network development tools
|
||||||
safe_clean ~/Library/Caches/com.getpaw.Paw/* "Paw API cache"
|
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.charlesproxy.charles/* "Charles Proxy cache"
|
||||||
safe_clean ~/Library/Caches/com.proxyman.NSProxy/* "Proxyman cache"
|
safe_clean ~/Library/Caches/com.proxyman.NSProxy/* "Proxyman cache"
|
||||||
safe_clean ~/Library/Caches/redis-desktop-manager/* "Redis Desktop Manager cache"
|
|
||||||
|
|
||||||
# CI/CD tools
|
# CI/CD tools
|
||||||
safe_clean ~/.grafana/cache/* "Grafana cache"
|
safe_clean ~/.grafana/cache/* "Grafana cache"
|
||||||
@@ -575,17 +446,17 @@ perform_cleanup() {
|
|||||||
safe_clean ~/.ivy2/cache/* "Ivy cache"
|
safe_clean ~/.ivy2/cache/* "Ivy cache"
|
||||||
safe_clean ~/.pub-cache/* "Dart Pub cache"
|
safe_clean ~/.pub-cache/* "Dart Pub cache"
|
||||||
|
|
||||||
# Network tools cache (safe)
|
# Network tools cache
|
||||||
safe_clean ~/.cache/curl/* "curl cache"
|
safe_clean ~/.cache/curl/* "curl cache"
|
||||||
safe_clean ~/.cache/wget/* "wget cache"
|
safe_clean ~/.cache/wget/* "wget cache"
|
||||||
safe_clean ~/Library/Caches/curl/* "curl cache"
|
safe_clean ~/Library/Caches/curl/* "curl cache (macOS)"
|
||||||
safe_clean ~/Library/Caches/wget/* "wget cache"
|
safe_clean ~/Library/Caches/wget/* "wget cache (macOS)"
|
||||||
|
|
||||||
# Git and version control
|
# Git and version control
|
||||||
safe_clean ~/.cache/pre-commit/* "pre-commit cache"
|
safe_clean ~/.cache/pre-commit/* "pre-commit cache"
|
||||||
safe_clean ~/.gitconfig.bak* "Git config backup"
|
safe_clean ~/.gitconfig.bak* "Git config backup"
|
||||||
|
|
||||||
# Mobile development
|
# Mobile development additional
|
||||||
safe_clean ~/.cache/flutter/* "Flutter cache"
|
safe_clean ~/.cache/flutter/* "Flutter cache"
|
||||||
safe_clean ~/.gradle/daemon/* "Gradle daemon logs"
|
safe_clean ~/.gradle/daemon/* "Gradle daemon logs"
|
||||||
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "iOS device cache"
|
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "iOS device cache"
|
||||||
@@ -602,26 +473,15 @@ perform_cleanup() {
|
|||||||
safe_clean ~/Library/Caches/com.eggerapps.Sequel-Pro/* "Sequel Pro cache"
|
safe_clean ~/Library/Caches/com.eggerapps.Sequel-Pro/* "Sequel Pro cache"
|
||||||
safe_clean ~/Library/Caches/redis-desktop-manager/* "Redis Desktop Manager 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
|
# Crash reports and debugging
|
||||||
safe_clean ~/Library/Caches/SentryCrash/* "Sentry crash reports"
|
safe_clean ~/Library/Caches/SentryCrash/* "Sentry crash reports"
|
||||||
safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports"
|
safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports"
|
||||||
safe_clean ~/Library/Caches/com.crashlytics.data/* "Crashlytics data"
|
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"
|
safe_clean ~/Library/HTTPStorages/* "HTTP storage cache"
|
||||||
|
|
||||||
end_section
|
end_section
|
||||||
|
|
||||||
|
|
||||||
# ===== 4. Applications =====
|
# ===== 4. Applications =====
|
||||||
start_section "Applications"
|
start_section "Applications"
|
||||||
|
|
||||||
@@ -678,24 +538,24 @@ perform_cleanup() {
|
|||||||
safe_clean ~/Library/Caches/com.valvesoftware.steam/* "Steam cache"
|
safe_clean ~/Library/Caches/com.valvesoftware.steam/* "Steam cache"
|
||||||
safe_clean ~/Library/Caches/com.epicgames.EpicGamesLauncher/* "Epic Games cache"
|
safe_clean ~/Library/Caches/com.epicgames.EpicGamesLauncher/* "Epic Games cache"
|
||||||
|
|
||||||
# Utilities and productivity
|
# Utilities and productivity (only cache, avoid license/settings data)
|
||||||
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/com.runjuu.Input-Source-Pro/* "Input Source Pro cache"
|
||||||
safe_clean ~/Library/Caches/macos-wakatime.WakaTime/* "WakaTime cache"
|
safe_clean ~/Library/Caches/macos-wakatime.WakaTime/* "WakaTime cache"
|
||||||
safe_clean ~/Library/Caches/notion.id/* "Notion cache"
|
safe_clean ~/Library/Caches/notion.id/* "Notion cache"
|
||||||
safe_clean ~/Library/Caches/md.obsidian/* "Obsidian 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/com.runningwithcrayons.Alfred/* "Alfred cache"
|
||||||
safe_clean ~/Library/Caches/cx.c3.theunarchiver/* "The Unarchiver cache"
|
safe_clean ~/Library/Caches/cx.c3.theunarchiver/* "The Unarchiver cache"
|
||||||
safe_clean ~/Library/Caches/com.freemacsoft.AppCleaner/* "AppCleaner cache"
|
|
||||||
|
# Note: Skipping App Cleaner, 1Password and similar apps to preserve licenses
|
||||||
|
|
||||||
end_section
|
end_section
|
||||||
|
|
||||||
# ===== 5. Orphaned leftovers =====
|
|
||||||
|
# ===== Orphaned leftovers =====
|
||||||
start_section "Orphaned app files"
|
start_section "Orphaned app files"
|
||||||
|
|
||||||
# Build a list of installed application bundle identifiers
|
# Build a list of installed application bundle identifiers
|
||||||
echo -e " ${BLUE}🔍${NC} Building app list..."
|
echo -n " ${BLUE}🔍${NC} Scanning installed applications..."
|
||||||
local installed_bundles=$(mktemp)
|
local installed_bundles=$(mktemp)
|
||||||
# More robust approach that won't hang
|
# More robust approach that won't hang
|
||||||
for app in /Applications/*.app; do
|
for app in /Applications/*.app; do
|
||||||
@@ -705,13 +565,15 @@ perform_cleanup() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
local app_count=$(wc -l < "$installed_bundles" | tr -d ' ')
|
local app_count=$(wc -l < "$installed_bundles" | tr -d ' ')
|
||||||
echo -e " ${GREEN}✓${NC} Found $app_count apps"
|
echo " ${GREEN}✓${NC} Found $app_count apps"
|
||||||
|
|
||||||
local found_orphaned=false
|
local found_orphaned=false
|
||||||
|
local cache_count=0
|
||||||
|
local data_count=0
|
||||||
|
local pref_count=0
|
||||||
|
|
||||||
# Check for orphaned caches (with protection for critical system settings)
|
# Check for orphaned caches (with protection for critical system settings)
|
||||||
echo -e " ${BLUE}🔍${NC} Checking caches..."
|
echo -n " ${BLUE}🔍${NC} Scanning cache directories..."
|
||||||
local cache_count=0
|
|
||||||
if ls ~/Library/Caches/com.* >/dev/null 2>&1; then
|
if ls ~/Library/Caches/com.* >/dev/null 2>&1; then
|
||||||
for cache_dir in ~/Library/Caches/com.*; do
|
for cache_dir in ~/Library/Caches/com.*; do
|
||||||
[[ -d "$cache_dir" ]] || continue
|
[[ -d "$cache_dir" ]] || continue
|
||||||
@@ -727,11 +589,10 @@ perform_cleanup() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
echo -e " ${GREEN}✓${NC} Checked caches ($cache_count removed)"
|
echo " ${GREEN}✓${NC} Complete ($cache_count removed)"
|
||||||
|
|
||||||
# Check for orphaned application support data (with protection for critical system settings)
|
# Check for orphaned application support data (with protection for critical system settings)
|
||||||
echo -e " ${BLUE}🔍${NC} Checking app data..."
|
echo -n " ${BLUE}🔍${NC} Scanning application data..."
|
||||||
local data_count=0
|
|
||||||
if ls ~/Library/Application\ Support/com.* >/dev/null 2>&1; then
|
if ls ~/Library/Application\ Support/com.* >/dev/null 2>&1; then
|
||||||
for support_dir in ~/Library/Application\ Support/com.*; do
|
for support_dir in ~/Library/Application\ Support/com.*; do
|
||||||
[[ -d "$support_dir" ]] || continue
|
[[ -d "$support_dir" ]] || continue
|
||||||
@@ -740,11 +601,20 @@ perform_cleanup() {
|
|||||||
if should_preserve_bundle "$bundle_id"; then
|
if should_preserve_bundle "$bundle_id"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# Extra safety for Application Support data
|
# Extra safety for Application Support data (preserve licenses and critical settings)
|
||||||
case "$bundle_id" in
|
case "$bundle_id" in
|
||||||
|
# System components
|
||||||
*dock*|*Dock*|*controlcenter*|*ControlCenter*|*systempreferences*|*SystemPreferences*)
|
*dock*|*Dock*|*controlcenter*|*ControlCenter*|*systempreferences*|*SystemPreferences*)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
|
# Paid software and license-critical apps
|
||||||
|
*nektony*|*macpaw*|*jetbrains*|*sublimetext*|*adobe*|*1password*|*agilebits*|*omnigroup*|*culturedcode*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
# Security and password managers
|
||||||
|
*lastpass*|*dashlane*|*bitwarden*|*keepass*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*)
|
*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
@@ -756,11 +626,10 @@ perform_cleanup() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
echo -e " ${GREEN}✓${NC} Checked app data ($data_count removed)"
|
echo " ${GREEN}✓${NC} Complete ($data_count removed)"
|
||||||
|
|
||||||
# Check for orphaned preferences (with protection for critical system settings)
|
# Check for orphaned preferences (with protection for critical system settings)
|
||||||
echo -e " ${BLUE}🔍${NC} Checking preferences..."
|
echo -n " ${BLUE}🔍${NC} Scanning preference files..."
|
||||||
local pref_count=0
|
|
||||||
if ls ~/Library/Preferences/com.*.plist >/dev/null 2>&1; then
|
if ls ~/Library/Preferences/com.*.plist >/dev/null 2>&1; then
|
||||||
for pref_file in ~/Library/Preferences/com.*.plist; do
|
for pref_file in ~/Library/Preferences/com.*.plist; do
|
||||||
[[ -f "$pref_file" ]] || continue
|
[[ -f "$pref_file" ]] || continue
|
||||||
@@ -769,8 +638,9 @@ perform_cleanup() {
|
|||||||
if should_preserve_bundle "$bundle_id"; then
|
if should_preserve_bundle "$bundle_id"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# Extra safety: Never delete preference files that might affect system behavior
|
# Extra safety: Never delete preference files that might affect system behavior or paid app licenses
|
||||||
case "$bundle_id" in
|
case "$bundle_id" in
|
||||||
|
# System components
|
||||||
*dock*|*Dock*|*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*)
|
*dock*|*Dock*|*trackpad*|*Trackpad*|*mouse*|*Mouse*|*keyboard*|*Keyboard*)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
@@ -780,6 +650,16 @@ perform_cleanup() {
|
|||||||
*menubar*|*MenuBar*|*hotkeys*|*HotKeys*|*shortcuts*|*Shortcuts*)
|
*menubar*|*MenuBar*|*hotkeys*|*HotKeys*|*shortcuts*|*Shortcuts*)
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
|
# Licensed software and critical apps (preserve activation data)
|
||||||
|
*nektony*|*macpaw*|*jetbrains*|*sublimetext*|*adobe*|*1password*|*agilebits*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
*omnigroup*|*culturedcode*|*lastpass*|*dashlane*|*bitwarden*|*keepass*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
*bohemiancoding*|*figma*|*framer*|*panic*|*sequelpro*|*tinyapp*|*pixelmator*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
if ! grep -q "$bundle_id" "$installed_bundles" 2>/dev/null; then
|
if ! grep -q "$bundle_id" "$installed_bundles" 2>/dev/null; then
|
||||||
safe_clean "$pref_file" "Orphaned preference: $bundle_id"
|
safe_clean "$pref_file" "Orphaned preference: $bundle_id"
|
||||||
@@ -788,17 +668,12 @@ perform_cleanup() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
echo -e " ${GREEN}✓${NC} Checked preferences ($pref_count removed)"
|
echo " ${GREEN}✓${NC} Complete ($pref_count removed)"
|
||||||
|
|
||||||
# Clean up temp file
|
# Clean up temp file
|
||||||
rm -f "$installed_bundles"
|
rm -f "$installed_bundles"
|
||||||
|
|
||||||
if [ "$found_orphaned" = false ]; then
|
# Clean test data
|
||||||
echo -e " ${GREEN}✓${NC} No orphaned files found"
|
|
||||||
fi
|
|
||||||
end_section
|
|
||||||
|
|
||||||
# Common temp and test data
|
|
||||||
safe_clean ~/Library/Application\ Support/TestApp* "Test app data"
|
safe_clean ~/Library/Application\ Support/TestApp* "Test app data"
|
||||||
safe_clean ~/Library/Application\ Support/MyApp/* "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/GitHub*/* "GitHub test data"
|
||||||
@@ -806,9 +681,11 @@ perform_cleanup() {
|
|||||||
safe_clean ~/Library/Application\ Support/TestNoValue/* "Test data"
|
safe_clean ~/Library/Application\ Support/TestNoValue/* "Test data"
|
||||||
safe_clean ~/Library/Application\ Support/Wk*/* "Test data"
|
safe_clean ~/Library/Application\ Support/Wk*/* "Test data"
|
||||||
|
|
||||||
# ===== 5. Apple Silicon optimizations =====
|
end_section
|
||||||
|
|
||||||
|
# ===== Apple Silicon optimizations =====
|
||||||
if [[ "$IS_M_SERIES" == "true" ]]; then
|
if [[ "$IS_M_SERIES" == "true" ]]; then
|
||||||
start_section "Apple Silicon cache cleanup"
|
start_section "Apple Silicon optimizations"
|
||||||
safe_clean /Library/Apple/usr/share/rosetta/rosetta_update_bundle "Rosetta 2 cache"
|
safe_clean /Library/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.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.amp.mediasevicesd "Apple Silicon media service cache"
|
||||||
@@ -818,54 +695,76 @@ perform_cleanup() {
|
|||||||
|
|
||||||
# System cleanup was moved to the beginning (right after password verification)
|
# System cleanup was moved to the beginning (right after password verification)
|
||||||
|
|
||||||
# ===== 7. iOS device backups =====
|
# ===== iOS device backups =====
|
||||||
start_section "iOS device backups"
|
start_section "iOS device backups"
|
||||||
backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
||||||
if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then
|
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}')
|
backup_kb=$(du -sk "$backup_dir" 2>/dev/null | awk '{print $1}')
|
||||||
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then # >100MB
|
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then # >100MB
|
||||||
backup_human=$(du -shm "$backup_dir" 2>/dev/null | awk '{print $1"M"}')
|
backup_human=$(du -sh "$backup_dir" 2>/dev/null | awk '{print $1}')
|
||||||
note_activity
|
note_activity
|
||||||
echo -e " 👉 Found ${GREEN}${backup_human}${NC}, you can delete it manually"
|
echo -e " ${BLUE}💾${NC} Found ${GREEN}${backup_human}${NC} iOS backups"
|
||||||
echo -e " 👉 ${backup_dir}"
|
echo -e " ${YELLOW}💡${NC} You can delete them manually: ${backup_dir}"
|
||||||
else
|
|
||||||
echo -e " ${BLUE}✨${NC} Nothing to tidy"
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
echo -e " ${BLUE}✨${NC} Nothing to tidy"
|
|
||||||
fi
|
fi
|
||||||
end_section
|
end_section
|
||||||
|
|
||||||
# ===== 8. Summary =====
|
# ===== Final summary =====
|
||||||
start_section "Cleanup summary"
|
|
||||||
note_activity
|
|
||||||
space_after=$(df / | tail -1 | awk '{print $4}')
|
space_after=$(df / | tail -1 | awk '{print $4}')
|
||||||
current_space_after=$(get_free_space)
|
|
||||||
|
|
||||||
echo "==================================================================="
|
|
||||||
space_freed_kb=$((space_after - space_before))
|
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 ""
|
||||||
echo "✨ Apple Silicon optimizations finished"
|
echo "===================================================================="
|
||||||
|
echo "🎉 CLEANUP COMPLETE!"
|
||||||
|
|
||||||
|
if [[ $total_size_cleaned -gt 0 ]]; then
|
||||||
|
local freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}')
|
||||||
|
echo "💾 Space freed: ${GREEN}${freed_gb}GB${NC} | Free space now: $(get_free_space)"
|
||||||
|
|
||||||
|
# Add some context to make it more impressive
|
||||||
|
if [[ $(echo "$freed_gb" | awk '{print ($1 >= 1) ? 1 : 0}') -eq 1 ]]; then
|
||||||
|
local movies=$(echo "$freed_gb" | awk '{printf "%.0f", $1/4.5}')
|
||||||
|
if [[ $movies -gt 0 ]]; then
|
||||||
|
echo "🎬 That's like ~$movies 4K movies worth of space!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "💾 No significant space was freed (system was already clean) | Free space: $(get_free_space)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
|
||||||
|
echo "📊 Files cleaned: $files_cleaned | Categories processed: $total_items"
|
||||||
|
elif [[ $files_cleaned -gt 0 ]]; then
|
||||||
|
echo "📊 Files cleaned: $files_cleaned"
|
||||||
|
elif [[ $total_items -gt 0 ]]; then
|
||||||
|
echo "🗂️ Categories processed: $total_items"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$SYSTEM_CLEAN" != "true" ]]; then
|
if [[ "$SYSTEM_CLEAN" != "true" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}💡 Want deeper cleanup next time?${NC}"
|
echo -e "${BLUE}💡 For deeper cleanup, run with admin password next time${NC}"
|
||||||
echo -e " Just enter your password when prompted for system-level cleaning"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==================================================================="
|
echo "===================================================================="
|
||||||
end_section
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Cleanup function - restore cursor on exit
|
||||||
|
cleanup() {
|
||||||
|
# Restore cursor
|
||||||
|
show_cursor
|
||||||
|
# Kill any background processes
|
||||||
|
if [[ -n "${SUDO_KEEPALIVE_PID:-}" ]]; then
|
||||||
|
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${SPINNER_PID:-}" ]]; then
|
||||||
|
kill "$SPINNER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
exit "${1:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set trap for cleanup on exit
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
case "${1:-""}" in
|
case "${1:-""}" in
|
||||||
"--help"|"-h")
|
"--help"|"-h")
|
||||||
@@ -876,11 +775,14 @@ main() {
|
|||||||
echo " --help, -h Show this help"
|
echo " --help, -h Show this help"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Interactive cleanup with smart password handling"
|
echo "Interactive cleanup with smart password handling"
|
||||||
|
echo ""
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
hide_cursor
|
||||||
start_cleanup
|
start_cleanup
|
||||||
perform_cleanup
|
perform_cleanup
|
||||||
|
show_cursor
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
388
bin/install.sh
388
bin/install.sh
@@ -1,388 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Mole - Install Module
|
|
||||||
# Interactive application installer using Homebrew
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# install.sh # Launch interactive installer
|
|
||||||
# install.sh --help # Show help information
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Get script directory and source common functions
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
source "$SCRIPT_DIR/../lib/common.sh"
|
|
||||||
|
|
||||||
# Check if Homebrew is available
|
|
||||||
check_homebrew() {
|
|
||||||
if ! command -v brew >/dev/null 2>&1; then
|
|
||||||
log_error "Homebrew is not installed"
|
|
||||||
echo ""
|
|
||||||
echo "To install Homebrew, run:"
|
|
||||||
echo '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Application categories with descriptions
|
|
||||||
declare -A APP_CATEGORIES=(
|
|
||||||
["productivity"]="📝 Productivity Apps"
|
|
||||||
["development"]="💻 Development Tools"
|
|
||||||
["media"]="🎵 Media & Entertainment"
|
|
||||||
["utilities"]="🔧 System Utilities"
|
|
||||||
["communication"]="💬 Communication"
|
|
||||||
["design"]="🎨 Design & Graphics"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define applications by category
|
|
||||||
declare -A APPS=(
|
|
||||||
# Productivity
|
|
||||||
["notion"]="productivity|Notion|All-in-one workspace for notes and docs"
|
|
||||||
["obsidian"]="productivity|Obsidian|Knowledge management and note-taking"
|
|
||||||
["raycast"]="productivity|Raycast|Launcher and productivity tool"
|
|
||||||
["alfred"]="productivity|Alfred|Application launcher and productivity app"
|
|
||||||
["1password"]="productivity|1Password|Password manager"
|
|
||||||
|
|
||||||
# Development
|
|
||||||
["visual-studio-code"]="development|VS Code|Code editor by Microsoft"
|
|
||||||
["docker"]="development|Docker|Containerization platform"
|
|
||||||
["postman"]="development|Postman|API development and testing"
|
|
||||||
["github-desktop"]="development|GitHub Desktop|Git client for GitHub"
|
|
||||||
["figma"]="development|Figma|Design and prototyping tool"
|
|
||||||
["iterm2"]="development|iTerm2|Terminal replacement"
|
|
||||||
|
|
||||||
# Media
|
|
||||||
["vlc"]="media|VLC|Media player"
|
|
||||||
["spotify"]="media|Spotify|Music streaming"
|
|
||||||
["handbrake"]="media|HandBrake|Video transcoder"
|
|
||||||
["obs"]="media|OBS Studio|Live streaming and recording"
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
["the-unarchiver"]="utilities|The Unarchiver|Archive utility"
|
|
||||||
["appcleaner"]="utilities|AppCleaner|Uninstall applications completely"
|
|
||||||
["cleanmymac"]="utilities|CleanMyMac X|System cleaning and optimization"
|
|
||||||
["bartender-4"]="utilities|Bartender 4|Menu bar organization"
|
|
||||||
|
|
||||||
# Communication
|
|
||||||
["discord"]="communication|Discord|Voice and text chat"
|
|
||||||
["slack"]="communication|Slack|Team communication"
|
|
||||||
["telegram"]="communication|Telegram|Messaging app"
|
|
||||||
["zoom"]="communication|Zoom|Video conferencing"
|
|
||||||
|
|
||||||
# Design
|
|
||||||
["sketch"]="design|Sketch|Digital design toolkit"
|
|
||||||
["adobe-creative-cloud"]="design|Adobe CC|Creative suite"
|
|
||||||
["blender"]="design|Blender|3D creation suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize global variables
|
|
||||||
declare -a selected_apps=()
|
|
||||||
declare -a filtered_apps=()
|
|
||||||
current_category="all"
|
|
||||||
current_line=0
|
|
||||||
|
|
||||||
# Help information
|
|
||||||
show_help() {
|
|
||||||
echo "Mole - Interactive App Installer"
|
|
||||||
echo "================================="
|
|
||||||
echo ""
|
|
||||||
echo "Description: Install useful applications using Homebrew Cask"
|
|
||||||
echo ""
|
|
||||||
echo "Features:"
|
|
||||||
echo " • Browse apps by category"
|
|
||||||
echo " • Navigate with ↑/↓ arrow keys"
|
|
||||||
echo " • Select/deselect apps with SPACE"
|
|
||||||
echo " • Filter by category with 1-6 keys"
|
|
||||||
echo " • Install selected apps with ENTER"
|
|
||||||
echo " • Quit anytime with 'q'"
|
|
||||||
echo ""
|
|
||||||
echo "Usage:"
|
|
||||||
echo " ./install.sh Launch interactive installer"
|
|
||||||
echo " ./install.sh --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Requirements:"
|
|
||||||
echo " • Homebrew must be installed"
|
|
||||||
echo " • Internet connection for downloads"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Filter apps by category
|
|
||||||
filter_apps_by_category() {
|
|
||||||
local category="$1"
|
|
||||||
filtered_apps=()
|
|
||||||
|
|
||||||
for app_key in "${!APPS[@]}"; do
|
|
||||||
IFS='|' read -r app_category app_name app_desc <<< "${APPS[$app_key]}"
|
|
||||||
if [[ "$category" == "all" || "$app_category" == "$category" ]]; then
|
|
||||||
filtered_apps+=("$app_key|$app_category|$app_name|$app_desc")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Sort alphabetically by name
|
|
||||||
IFS=$'\n' filtered_apps=($(sort -t'|' -k3 <<<"${filtered_apps[*]}"))
|
|
||||||
unset IFS
|
|
||||||
}
|
|
||||||
|
|
||||||
# Display application list
|
|
||||||
display_apps() {
|
|
||||||
clear
|
|
||||||
echo "📦 Mole - Interactive App Installer"
|
|
||||||
echo "══════════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show category filter
|
|
||||||
local category_name="All Applications"
|
|
||||||
case "$current_category" in
|
|
||||||
"productivity") category_name="${APP_CATEGORIES[productivity]}" ;;
|
|
||||||
"development") category_name="${APP_CATEGORIES[development]}" ;;
|
|
||||||
"media") category_name="${APP_CATEGORIES[media]}" ;;
|
|
||||||
"utilities") category_name="${APP_CATEGORIES[utilities]}" ;;
|
|
||||||
"communication") category_name="${APP_CATEGORIES[communication]}" ;;
|
|
||||||
"design") category_name="${APP_CATEGORIES[design]}" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo -e "${PURPLE}Category: $category_name${NC}"
|
|
||||||
echo -e "${PURPLE}Showing ${#filtered_apps[@]} applications${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Display apps (max 15 per page)
|
|
||||||
local start_idx=0
|
|
||||||
local end_idx=$((${#filtered_apps[@]} - 1))
|
|
||||||
local max_display=15
|
|
||||||
|
|
||||||
if [[ $end_idx -gt $((max_display - 1)) ]]; then
|
|
||||||
end_idx=$((max_display - 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
for ((i=start_idx; i<=end_idx && i<${#filtered_apps[@]}; i++)); do
|
|
||||||
IFS='|' read -r app_key app_category app_name app_desc <<< "${filtered_apps[i]}"
|
|
||||||
|
|
||||||
local prefix=" "
|
|
||||||
local line_color="$NC"
|
|
||||||
local name_color="$NC"
|
|
||||||
|
|
||||||
# Current selection highlighting
|
|
||||||
if [[ $i -eq $current_line ]]; then
|
|
||||||
prefix="▶ "
|
|
||||||
line_color="$BLUE"
|
|
||||||
name_color="$BLUE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if app is selected
|
|
||||||
local checkbox="[ ]"
|
|
||||||
local checkbox_color="$NC"
|
|
||||||
for selected in "${selected_apps[@]}"; do
|
|
||||||
if [[ "$selected" == "$app_key" ]]; then
|
|
||||||
checkbox="[✓]"
|
|
||||||
checkbox_color="$GREEN"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Format display
|
|
||||||
printf "${line_color}${prefix}${checkbox_color}${checkbox}${NC} "
|
|
||||||
printf "${name_color}%-25s${NC} " "$app_name"
|
|
||||||
printf "│ %s\n" "$app_desc"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "──────────────────────────────────────────────────────────────────────"
|
|
||||||
|
|
||||||
# Show selection summary
|
|
||||||
local selected_count=${#selected_apps[@]}
|
|
||||||
if [[ $selected_count -eq 0 ]]; then
|
|
||||||
echo -e "${BLUE}📋 No applications selected${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${GREEN}📋 Selected: $selected_count applications${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show category filters
|
|
||||||
echo -e "${PURPLE}🏷️ Categories:${NC}"
|
|
||||||
echo " 0 All 1 Productivity 2 Development 3 Media 4 Utilities 5 Communication 6 Design"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Controls
|
|
||||||
echo -e "${PURPLE}🎮 Controls:${NC}"
|
|
||||||
echo " ↑/↓ Navigate SPACE Select 0-6 Filter ENTER Install ? Help q Quit"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Interactive app selection
|
|
||||||
interactive_app_selection() {
|
|
||||||
filter_apps_by_category "$current_category"
|
|
||||||
current_line=0
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
display_apps
|
|
||||||
|
|
||||||
# Read key input
|
|
||||||
read -rsn1 key
|
|
||||||
|
|
||||||
case "$key" in
|
|
||||||
$'\x1b') # ESC sequences
|
|
||||||
read -rsn2 key
|
|
||||||
case "$key" in
|
|
||||||
'[A') # Up arrow
|
|
||||||
((current_line > 0)) && ((current_line--))
|
|
||||||
;;
|
|
||||||
'[B') # Down arrow
|
|
||||||
((current_line < ${#filtered_apps[@]} - 1)) && ((current_line++))
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
' ') # Space - toggle selection
|
|
||||||
if [[ ${#filtered_apps[@]} -gt 0 && $current_line -lt ${#filtered_apps[@]} ]]; then
|
|
||||||
IFS='|' read -r app_key app_category app_name app_desc <<< "${filtered_apps[current_line]}"
|
|
||||||
|
|
||||||
# Check if already selected
|
|
||||||
local found=false
|
|
||||||
for i in "${!selected_apps[@]}"; do
|
|
||||||
if [[ "${selected_apps[i]}" == "$app_key" ]]; then
|
|
||||||
unset 'selected_apps[i]'
|
|
||||||
selected_apps=("${selected_apps[@]}") # Re-index array
|
|
||||||
found=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$found" == "false" ]]; then
|
|
||||||
selected_apps+=("$app_key")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
$'\n'|$'\r') # Enter - proceed to installation
|
|
||||||
if [[ ${#selected_apps[@]} -gt 0 ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
'q'|'Q') # Quit
|
|
||||||
log_info "Installation cancelled"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
[0-6]) # Category filters
|
|
||||||
case "$key" in
|
|
||||||
'0') current_category="all" ;;
|
|
||||||
'1') current_category="productivity" ;;
|
|
||||||
'2') current_category="development" ;;
|
|
||||||
'3') current_category="media" ;;
|
|
||||||
'4') current_category="utilities" ;;
|
|
||||||
'5') current_category="communication" ;;
|
|
||||||
'6') current_category="design" ;;
|
|
||||||
esac
|
|
||||||
filter_apps_by_category "$current_category"
|
|
||||||
current_line=0
|
|
||||||
;;
|
|
||||||
'a'|'A') # Select all visible
|
|
||||||
for app_data in "${filtered_apps[@]}"; do
|
|
||||||
IFS='|' read -r app_key app_category app_name app_desc <<< "$app_data"
|
|
||||||
|
|
||||||
# Check if already selected
|
|
||||||
local found=false
|
|
||||||
for selected in "${selected_apps[@]}"; do
|
|
||||||
if [[ "$selected" == "$app_key" ]]; then
|
|
||||||
found=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$found" == "false" ]]; then
|
|
||||||
selected_apps+=("$app_key")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
'n'|'N') # Select none
|
|
||||||
selected_apps=()
|
|
||||||
;;
|
|
||||||
'?') # Help
|
|
||||||
show_help
|
|
||||||
echo ""
|
|
||||||
read -p "Press any key to continue..." -n 1 -r
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Install selected applications
|
|
||||||
install_applications() {
|
|
||||||
log_header "Installing selected applications"
|
|
||||||
|
|
||||||
echo "You selected ${#selected_apps[@]} application(s) for installation:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for app_key in "${selected_apps[@]}"; do
|
|
||||||
IFS='|' read -r app_category app_name app_desc <<< "${APPS[$app_key]}"
|
|
||||||
echo " • $app_name - $app_desc"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
read -p "Continue with installation? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
echo ""
|
|
||||||
log_info "Starting installation..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
local successful=0
|
|
||||||
local failed=0
|
|
||||||
|
|
||||||
for app_key in "${selected_apps[@]}"; do
|
|
||||||
IFS='|' read -r app_category app_name app_desc <<< "${APPS[$app_key]}"
|
|
||||||
|
|
||||||
echo -e "${BLUE}Installing $app_name...${NC}"
|
|
||||||
|
|
||||||
if brew install --cask "$app_key" 2>/dev/null; then
|
|
||||||
echo -e " ${GREEN}✓${NC} $app_name installed successfully"
|
|
||||||
((successful++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗${NC} Failed to install $app_name"
|
|
||||||
((failed++))
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo "══════════════════════════════════════════════════════════════════════"
|
|
||||||
log_success "Installation complete!"
|
|
||||||
echo "📊 Successfully installed: $successful applications"
|
|
||||||
if [[ $failed -gt 0 ]]; then
|
|
||||||
echo "⚠️ Failed to install: $failed applications"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "Installation cancelled"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main function
|
|
||||||
main() {
|
|
||||||
echo "📦 Mole - Interactive App Installer"
|
|
||||||
echo "===================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check Homebrew
|
|
||||||
check_homebrew
|
|
||||||
|
|
||||||
log_info "Checking Homebrew installation..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Interactive selection
|
|
||||||
if ! interactive_app_selection; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
clear
|
|
||||||
install_applications
|
|
||||||
|
|
||||||
log_success "App installer finished"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run main function
|
|
||||||
main "$@"
|
|
||||||
261
bin/uninstall.sh
261
bin/uninstall.sh
@@ -11,41 +11,24 @@ set -euo pipefail
|
|||||||
# Get script directory and source common functions
|
# Get script directory and source common functions
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$SCRIPT_DIR/../lib/common.sh"
|
source "$SCRIPT_DIR/../lib/common.sh"
|
||||||
source "$SCRIPT_DIR/../lib/menu.sh"
|
source "$SCRIPT_DIR/../lib/paginated_menu.sh"
|
||||||
source "$SCRIPT_DIR/../lib/app_selector.sh"
|
source "$SCRIPT_DIR/../lib/app_selector.sh"
|
||||||
source "$SCRIPT_DIR/../lib/batch_uninstall.sh"
|
source "$SCRIPT_DIR/../lib/batch_uninstall.sh"
|
||||||
|
|
||||||
# Basic preserved bundle patterns
|
# Note: Bundle preservation logic is now in lib/common.sh
|
||||||
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
|
# Help information
|
||||||
show_help() {
|
show_help() {
|
||||||
echo "Mole - Interactive App Uninstaller"
|
echo "App Uninstaller"
|
||||||
echo "========================================"
|
echo "==============="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Description: Interactive tool to uninstall applications and clean their data"
|
echo "Uninstall applications and clean their data completely."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Features:"
|
echo "Controls:"
|
||||||
echo " • Navigate with ↑/↓ arrow keys"
|
echo " ↑/↓ Navigate"
|
||||||
echo " • Select/deselect apps with SPACE"
|
echo " SPACE Select/deselect"
|
||||||
echo " • Confirm selection with ENTER"
|
echo " ENTER Confirm"
|
||||||
echo " • Quit anytime with 'q'"
|
echo " Q Quit"
|
||||||
echo " • Apps sorted by last usage time"
|
|
||||||
echo " • Comprehensive cleanup of app data"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Usage:"
|
echo "Usage:"
|
||||||
echo " ./uninstall.sh Launch interactive uninstaller"
|
echo " ./uninstall.sh Launch interactive uninstaller"
|
||||||
@@ -69,7 +52,7 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Initialize global variables
|
# Initialize global variables
|
||||||
declare -a selected_apps=()
|
selected_apps=() # Global array for app selection
|
||||||
declare -a apps_data=()
|
declare -a apps_data=()
|
||||||
declare -a selection_state=()
|
declare -a selection_state=()
|
||||||
current_line=0
|
current_line=0
|
||||||
@@ -109,7 +92,7 @@ get_app_last_used() {
|
|||||||
scan_applications() {
|
scan_applications() {
|
||||||
local temp_file=$(mktemp)
|
local temp_file=$(mktemp)
|
||||||
|
|
||||||
echo -n "Scanning applications... " >&2
|
echo -n "Scanning... " >&2
|
||||||
|
|
||||||
# Pre-cache current epoch to avoid repeated calls
|
# Pre-cache current epoch to avoid repeated calls
|
||||||
local current_epoch=$(date "+%s")
|
local current_epoch=$(date "+%s")
|
||||||
@@ -121,10 +104,80 @@ scan_applications() {
|
|||||||
|
|
||||||
local app_name=$(basename "$app_path" .app)
|
local app_name=$(basename "$app_path" .app)
|
||||||
|
|
||||||
# Quick bundle ID check first (only if plist exists)
|
# Try to get English name from bundle info, fallback to folder name
|
||||||
local bundle_id="unknown"
|
local bundle_id="unknown"
|
||||||
|
local display_name="$app_name"
|
||||||
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
||||||
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown")
|
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# Try to get English name from bundle info
|
||||||
|
local bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null)
|
||||||
|
|
||||||
|
# Smart display name selection - prefer descriptive names over generic ones
|
||||||
|
local candidates=()
|
||||||
|
|
||||||
|
# Get all potential names
|
||||||
|
local bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||||
|
local bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||||
|
|
||||||
|
# Check if executable name is generic/technical (should be avoided)
|
||||||
|
local is_generic_executable=false
|
||||||
|
if [[ -n "$bundle_executable" ]]; then
|
||||||
|
case "$bundle_executable" in
|
||||||
|
"pake"|"Electron"|"electron"|"nwjs"|"node"|"helper"|"main"|"app"|"binary")
|
||||||
|
is_generic_executable=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Priority order for name selection:
|
||||||
|
# 1. App folder name (if ASCII and descriptive) - often the most complete name
|
||||||
|
if [[ "$app_name" =~ ^[A-Za-z0-9\ ._-]+$ && ${#app_name} -gt 3 ]]; then
|
||||||
|
candidates+=("$app_name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. CFBundleDisplayName (if meaningful and ASCII)
|
||||||
|
if [[ -n "$bundle_display_name" && "$bundle_display_name" =~ ^[A-Za-z0-9\ ._-]+$ ]]; then
|
||||||
|
candidates+=("$bundle_display_name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. CFBundleName (if meaningful and ASCII)
|
||||||
|
if [[ -n "$bundle_name" && "$bundle_name" =~ ^[A-Za-z0-9\ ._-]+$ && "$bundle_name" != "$bundle_display_name" ]]; then
|
||||||
|
candidates+=("$bundle_name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. CFBundleExecutable (only if not generic and ASCII)
|
||||||
|
if [[ -n "$bundle_executable" && "$bundle_executable" =~ ^[A-Za-z0-9._-]+$ && "$is_generic_executable" == false ]]; then
|
||||||
|
candidates+=("$bundle_executable")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Fallback to non-ASCII names if no ASCII found
|
||||||
|
if [[ ${#candidates[@]} -eq 0 ]]; then
|
||||||
|
[[ -n "$bundle_display_name" ]] && candidates+=("$bundle_display_name")
|
||||||
|
[[ -n "$bundle_name" && "$bundle_name" != "$bundle_display_name" ]] && candidates+=("$bundle_name")
|
||||||
|
candidates+=("$app_name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Select the first (best) candidate
|
||||||
|
display_name="${candidates[0]:-$app_name}"
|
||||||
|
|
||||||
|
# Brand name mapping for better user recognition (post-process)
|
||||||
|
case "$display_name" in
|
||||||
|
"qiyimac"|"爱奇艺") display_name="iQiyi" ;;
|
||||||
|
"wechat"|"微信") display_name="WeChat" ;;
|
||||||
|
"QQ"|"QQ") display_name="QQ" ;;
|
||||||
|
"VooV Meeting"|"腾讯会议") display_name="VooV Meeting" ;;
|
||||||
|
"dingtalk"|"钉钉") display_name="DingTalk" ;;
|
||||||
|
"NeteaseMusic"|"网易云音乐") display_name="NetEase Music" ;;
|
||||||
|
"BaiduNetdisk"|"百度网盘") display_name="Baidu NetDisk" ;;
|
||||||
|
"alipay"|"支付宝") display_name="Alipay" ;;
|
||||||
|
"taobao"|"淘宝") display_name="Taobao" ;;
|
||||||
|
"futunn"|"富途牛牛") display_name="Futu NiuNiu" ;;
|
||||||
|
"tencent lemon"|"Tencent Lemon Cleaner") display_name="Tencent Lemon" ;;
|
||||||
|
"keynote"|"Keynote") display_name="Keynote" ;;
|
||||||
|
"pages"|"Pages") display_name="Pages" ;;
|
||||||
|
"numbers"|"Numbers") display_name="Numbers" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Skip protected system apps early
|
# Skip protected system apps early
|
||||||
@@ -132,8 +185,8 @@ scan_applications() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Store tuple: app_path|app_name|bundle_id
|
# Store tuple: app_path|app_name|bundle_id|display_name
|
||||||
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}")
|
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
|
||||||
done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null)
|
done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null)
|
||||||
|
|
||||||
# Second pass: process each app with accurate size calculation
|
# Second pass: process each app with accurate size calculation
|
||||||
@@ -141,12 +194,12 @@ scan_applications() {
|
|||||||
local total_apps=${#app_data_tuples[@]}
|
local total_apps=${#app_data_tuples[@]}
|
||||||
|
|
||||||
for app_data_tuple in "${app_data_tuples[@]}"; do
|
for app_data_tuple in "${app_data_tuples[@]}"; do
|
||||||
IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple"
|
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
|
||||||
|
|
||||||
# Show progress every few items
|
# Show progress every few items
|
||||||
((app_count++))
|
((app_count++))
|
||||||
if (( app_count % 3 == 0 )) || [[ $app_count -eq $total_apps ]]; then
|
if (( app_count % 5 == 0 )) || [[ $app_count -eq $total_apps ]]; then
|
||||||
echo -ne "\rScanning applications... processing $app_count/$total_apps apps" >&2
|
echo -ne "\rScanning... $app_count/$total_apps" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Accurate size calculation - this is what takes time but user wants it
|
# Accurate size calculation - this is what takes time but user wants it
|
||||||
@@ -155,27 +208,70 @@ scan_applications() {
|
|||||||
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Simplified last used check using file modification time
|
# Get real last used date from macOS metadata
|
||||||
local last_used="Old"
|
local last_used="Never"
|
||||||
local last_used_epoch=0
|
local last_used_epoch=0
|
||||||
|
|
||||||
if [[ -d "$app_path" ]]; then
|
if [[ -d "$app_path" ]]; then
|
||||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||||
if [[ $last_used_epoch -gt 0 ]]; then
|
|
||||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
||||||
if [[ $days_ago -lt 30 ]]; then
|
# Convert macOS date format to epoch
|
||||||
last_used="Recent"
|
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0")
|
||||||
elif [[ $days_ago -lt 365 ]]; then
|
|
||||||
last_used="This year"
|
if [[ $last_used_epoch -gt 0 ]]; then
|
||||||
|
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||||
|
|
||||||
|
if [[ $days_ago -eq 0 ]]; then
|
||||||
|
last_used="Today"
|
||||||
|
elif [[ $days_ago -eq 1 ]]; then
|
||||||
|
last_used="Yesterday"
|
||||||
|
elif [[ $days_ago -lt 7 ]]; then
|
||||||
|
last_used="${days_ago} days ago"
|
||||||
|
elif [[ $days_ago -lt 30 ]]; then
|
||||||
|
local weeks_ago=$(( days_ago / 7 ))
|
||||||
|
if [[ $weeks_ago -eq 1 ]]; then
|
||||||
|
last_used="1 week ago"
|
||||||
|
else
|
||||||
|
last_used="${weeks_ago} weeks ago"
|
||||||
|
fi
|
||||||
|
elif [[ $days_ago -lt 365 ]]; then
|
||||||
|
local months_ago=$(( days_ago / 30 ))
|
||||||
|
if [[ $months_ago -eq 1 ]]; then
|
||||||
|
last_used="1 month ago"
|
||||||
|
else
|
||||||
|
last_used="${months_ago} months ago"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
local years_ago=$(( days_ago / 365 ))
|
||||||
|
if [[ $years_ago -eq 1 ]]; then
|
||||||
|
last_used="1 year ago"
|
||||||
|
else
|
||||||
|
last_used="${years_ago} years ago"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback to file modification time if no usage metadata
|
||||||
|
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||||
|
if [[ $last_used_epoch -gt 0 ]]; then
|
||||||
|
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||||
|
if [[ $days_ago -lt 30 ]]; then
|
||||||
|
last_used="Recent"
|
||||||
|
elif [[ $days_ago -lt 365 ]]; then
|
||||||
|
last_used="This year"
|
||||||
|
else
|
||||||
|
last_used="Old"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Format: epoch|app_path|app_name|bundle_id|size|last_used_display
|
# Format: epoch|app_path|display_name|bundle_id|size|last_used_display
|
||||||
echo "${last_used_epoch}|${app_path}|${app_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -e "\rScanning applications... found $app_count apps ✓" >&2
|
echo -e "\rFound $app_count applications ✓" >&2
|
||||||
|
|
||||||
# Check if we found any applications
|
# Check if we found any applications
|
||||||
if [[ ! -s "$temp_file" ]]; then
|
if [[ ! -s "$temp_file" ]]; then
|
||||||
@@ -221,57 +317,8 @@ load_applications() {
|
|||||||
# Read a single key with proper escape sequence handling
|
# Read a single key with proper escape sequence handling
|
||||||
# This function has been replaced by the menu.sh library
|
# This function has been replaced by the menu.sh library
|
||||||
|
|
||||||
# Old interactive_app_selection and show_selection_help functions removed
|
# Note: App file discovery and size calculation functions moved to lib/common.sh
|
||||||
# They have been replaced by the new menu system in lib/app_selector.sh
|
# Use find_app_files() and calculate_total_size() from common.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 selected applications
|
||||||
uninstall_applications() {
|
uninstall_applications() {
|
||||||
@@ -384,8 +431,8 @@ uninstall_applications() {
|
|||||||
|
|
||||||
# Cleanup function - restore cursor and clean up
|
# Cleanup function - restore cursor and clean up
|
||||||
cleanup() {
|
cleanup() {
|
||||||
# Restore cursor
|
# Restore cursor using common function
|
||||||
printf '\033[?25h'
|
show_cursor
|
||||||
exit "${1:-0}"
|
exit "${1:-0}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +441,8 @@ trap cleanup EXIT INT TERM
|
|||||||
|
|
||||||
# Main function
|
# Main function
|
||||||
main() {
|
main() {
|
||||||
echo "🗑️ Mole - Interactive App Uninstaller"
|
# Hide cursor during operation
|
||||||
echo "============================================"
|
hide_cursor
|
||||||
echo
|
|
||||||
|
|
||||||
# Scan applications
|
# Scan applications
|
||||||
local apps_file=$(scan_applications)
|
local apps_file=$(scan_applications)
|
||||||
@@ -412,17 +458,16 @@ main() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Interactive selection using new menu system
|
# Interactive selection using paginated menu
|
||||||
if ! select_apps_for_uninstall; then
|
if ! select_apps_for_uninstall; then
|
||||||
rm -f "$apps_file"
|
rm -f "$apps_file"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restore cursor for normal interaction
|
# Restore cursor for normal interaction after selection
|
||||||
printf '\033[?25h'
|
show_cursor
|
||||||
clear
|
clear
|
||||||
echo "You selected ${#selected_apps[@]} application(s) for uninstallation:"
|
echo "You selected ${#selected_apps[@]} application(s) for uninstallation:"
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ ${#selected_apps[@]} -gt 0 ]]; then
|
if [[ ${#selected_apps[@]} -gt 0 ]]; then
|
||||||
for selected_app in "${selected_apps[@]}"; do
|
for selected_app in "${selected_apps[@]}"; do
|
||||||
@@ -439,8 +484,6 @@ main() {
|
|||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
rm -f "$apps_file"
|
rm -f "$apps_file"
|
||||||
|
|
||||||
log_success "App uninstaller finished"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run main function
|
# Run main function
|
||||||
|
|||||||
225
install.sh
225
install.sh
@@ -25,6 +25,9 @@ INSTALL_DIR="/usr/local/bin"
|
|||||||
CONFIG_DIR="$HOME/.config/mole"
|
CONFIG_DIR="$HOME/.config/mole"
|
||||||
SOURCE_DIR=""
|
SOURCE_DIR=""
|
||||||
|
|
||||||
|
# Default action (install|update)
|
||||||
|
ACTION="install"
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
Mole Installation Script
|
Mole Installation Script
|
||||||
@@ -36,12 +39,14 @@ USAGE:
|
|||||||
OPTIONS:
|
OPTIONS:
|
||||||
--prefix PATH Install to custom directory (default: /usr/local/bin)
|
--prefix PATH Install to custom directory (default: /usr/local/bin)
|
||||||
--config PATH Config directory (default: ~/.config/mole)
|
--config PATH Config directory (default: ~/.config/mole)
|
||||||
|
--update Update Mole to the latest version
|
||||||
--uninstall Uninstall mole
|
--uninstall Uninstall mole
|
||||||
--help, -h Show this help
|
--help, -h Show this help
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
./install.sh # Install to /usr/local/bin
|
./install.sh # Install to /usr/local/bin
|
||||||
./install.sh --prefix ~/.local/bin # Install to custom directory
|
./install.sh --prefix ~/.local/bin # Install to custom directory
|
||||||
|
./install.sh --update # Update Mole in place
|
||||||
./install.sh --uninstall # Uninstall mole
|
./install.sh --uninstall # Uninstall mole
|
||||||
|
|
||||||
The installer will:
|
The installer will:
|
||||||
@@ -49,22 +54,27 @@ The installer will:
|
|||||||
2. Set up config directory with all modules
|
2. Set up config directory with all modules
|
||||||
3. Make the mole command available system-wide
|
3. Make the mole command available system-wide
|
||||||
EOF
|
EOF
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve the directory containing source files (supports curl | bash)
|
# Resolve the directory containing source files (supports curl | bash)
|
||||||
resolve_source_dir() {
|
resolve_source_dir() {
|
||||||
# 1) If script is on disk, use its directory
|
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" && -f "$SOURCE_DIR/mole" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1) If script is on disk, use its directory (only when mole executable present)
|
||||||
if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
|
if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
|
||||||
local script_dir
|
local script_dir
|
||||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
if [[ -f "$script_dir/mole" || -d "$script_dir/bin" || -d "$script_dir/lib" ]]; then
|
if [[ -f "$script_dir/mole" ]]; then
|
||||||
SOURCE_DIR="$script_dir"
|
SOURCE_DIR="$script_dir"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2) If CLEAN_SOURCE_DIR env is provided, honor it
|
# 2) If CLEAN_SOURCE_DIR env is provided, honor it
|
||||||
if [[ -n "${CLEAN_SOURCE_DIR:-}" && -d "$CLEAN_SOURCE_DIR" ]]; then
|
if [[ -n "${CLEAN_SOURCE_DIR:-}" && -d "$CLEAN_SOURCE_DIR" && -f "$CLEAN_SOURCE_DIR/mole" ]]; then
|
||||||
SOURCE_DIR="$CLEAN_SOURCE_DIR"
|
SOURCE_DIR="$CLEAN_SOURCE_DIR"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -100,6 +110,20 @@ resolve_source_dir() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_source_version() {
|
||||||
|
local source_mole="$SOURCE_DIR/mole"
|
||||||
|
if [[ -f "$source_mole" ]]; then
|
||||||
|
sed -n 's/^VERSION="\(.*\)"$/\1/p' "$source_mole" | head -n1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_installed_version() {
|
||||||
|
local binary="$INSTALL_DIR/mole"
|
||||||
|
if [[ -x "$binary" ]]; then
|
||||||
|
"$binary" --version 2>/dev/null | awk 'NF {print $NF; exit}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
parse_args() {
|
parse_args() {
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -112,6 +136,10 @@ parse_args() {
|
|||||||
CONFIG_DIR="$2"
|
CONFIG_DIR="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--update)
|
||||||
|
ACTION="update"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
--uninstall)
|
--uninstall)
|
||||||
uninstall_mole
|
uninstall_mole
|
||||||
exit 0
|
exit 0
|
||||||
@@ -171,14 +199,25 @@ install_files() {
|
|||||||
|
|
||||||
resolve_source_dir
|
resolve_source_dir
|
||||||
|
|
||||||
# Copy main executable
|
local source_dir_abs
|
||||||
|
local install_dir_abs
|
||||||
|
local config_dir_abs
|
||||||
|
source_dir_abs="$(cd "$SOURCE_DIR" && pwd)"
|
||||||
|
install_dir_abs="$(cd "$INSTALL_DIR" && pwd)"
|
||||||
|
config_dir_abs="$(cd "$CONFIG_DIR" && pwd)"
|
||||||
|
|
||||||
|
# Copy main executable when destination differs
|
||||||
if [[ -f "$SOURCE_DIR/mole" ]]; then
|
if [[ -f "$SOURCE_DIR/mole" ]]; then
|
||||||
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
|
if [[ "$source_dir_abs" == "$install_dir_abs" ]]; then
|
||||||
sudo cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole"
|
log_info "Mole binary already present in $INSTALL_DIR"
|
||||||
sudo chmod +x "$INSTALL_DIR/mole"
|
|
||||||
else
|
else
|
||||||
cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole"
|
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
|
||||||
chmod +x "$INSTALL_DIR/mole"
|
sudo cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole"
|
||||||
|
sudo chmod +x "$INSTALL_DIR/mole"
|
||||||
|
else
|
||||||
|
cp "$SOURCE_DIR/mole" "$INSTALL_DIR/mole"
|
||||||
|
chmod +x "$INSTALL_DIR/mole"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log_error "mole executable not found in ${SOURCE_DIR:-unknown}"
|
log_error "mole executable not found in ${SOURCE_DIR:-unknown}"
|
||||||
@@ -187,26 +226,46 @@ install_files() {
|
|||||||
|
|
||||||
# Copy configuration and modules
|
# Copy configuration and modules
|
||||||
if [[ -d "$SOURCE_DIR/bin" ]]; then
|
if [[ -d "$SOURCE_DIR/bin" ]]; then
|
||||||
cp -r "$SOURCE_DIR/bin"/* "$CONFIG_DIR/bin/"
|
local source_bin_abs="$(cd "$SOURCE_DIR/bin" && pwd)"
|
||||||
chmod +x "$CONFIG_DIR/bin"/*
|
local config_bin_abs="$(cd "$CONFIG_DIR/bin" && pwd)"
|
||||||
|
if [[ "$source_bin_abs" == "$config_bin_abs" ]]; then
|
||||||
|
log_info "Configuration bin directory already synced"
|
||||||
|
else
|
||||||
|
cp -r "$SOURCE_DIR/bin"/* "$CONFIG_DIR/bin/"
|
||||||
|
chmod +x "$CONFIG_DIR/bin"/*
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -d "$SOURCE_DIR/lib" ]]; then
|
if [[ -d "$SOURCE_DIR/lib" ]]; then
|
||||||
cp -r "$SOURCE_DIR/lib"/* "$CONFIG_DIR/lib/"
|
local source_lib_abs="$(cd "$SOURCE_DIR/lib" && pwd)"
|
||||||
|
local config_lib_abs="$(cd "$CONFIG_DIR/lib" && pwd)"
|
||||||
|
if [[ "$source_lib_abs" == "$config_lib_abs" ]]; then
|
||||||
|
log_info "Configuration lib directory already synced"
|
||||||
|
else
|
||||||
|
cp -r "$SOURCE_DIR/lib"/* "$CONFIG_DIR/lib/"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy other files if they exist
|
# Copy other files if they exist and directories differ
|
||||||
for file in README.md LICENSE; do
|
if [[ "$config_dir_abs" != "$source_dir_abs" ]]; then
|
||||||
if [[ -f "$SOURCE_DIR/$file" ]]; then
|
for file in README.md LICENSE install.sh; do
|
||||||
cp "$SOURCE_DIR/$file" "$CONFIG_DIR/"
|
if [[ -f "$SOURCE_DIR/$file" ]]; then
|
||||||
fi
|
cp -f "$SOURCE_DIR/$file" "$CONFIG_DIR/"
|
||||||
done
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Update the mole script to use the config directory
|
if [[ -f "$CONFIG_DIR/install.sh" ]]; then
|
||||||
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
|
chmod +x "$CONFIG_DIR/install.sh"
|
||||||
sudo sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole"
|
fi
|
||||||
else
|
|
||||||
sed -i '' "s|SCRIPT_DIR=.*|SCRIPT_DIR=\"$CONFIG_DIR\"|" "$INSTALL_DIR/mole"
|
# Update the mole script to use the config directory when installed elsewhere
|
||||||
|
if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then
|
||||||
|
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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +276,7 @@ verify_installation() {
|
|||||||
|
|
||||||
# Test if mole command works
|
# Test if mole command works
|
||||||
if "$INSTALL_DIR/mole" --help >/dev/null 2>&1; then
|
if "$INSTALL_DIR/mole" --help >/dev/null 2>&1; then
|
||||||
log_success ""
|
return 0
|
||||||
else
|
else
|
||||||
log_warning "Mole command installed but may not be working properly"
|
log_warning "Mole command installed but may not be working properly"
|
||||||
fi
|
fi
|
||||||
@@ -245,6 +304,43 @@ setup_path() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print_usage_summary() {
|
||||||
|
local action="$1"
|
||||||
|
local new_version="$2"
|
||||||
|
local previous_version="${3:-}"
|
||||||
|
|
||||||
|
if [[ ${VERBOSE} -ne 1 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local message="Mole ${action} successfully"
|
||||||
|
|
||||||
|
if [[ "$action" == "updated" && -n "$previous_version" && -n "$new_version" && "$previous_version" != "$new_version" ]]; then
|
||||||
|
message+=" (${previous_version} -> ${new_version})"
|
||||||
|
elif [[ -n "$new_version" ]]; then
|
||||||
|
message+=" (version ${new_version})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "$message!"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then
|
||||||
|
echo " mole # Interactive menu"
|
||||||
|
echo " mole clean # System cleanup"
|
||||||
|
echo " mole uninstall # Remove applications"
|
||||||
|
echo " mole update # Update Mole to the latest version"
|
||||||
|
echo " mole --version # Show installed version"
|
||||||
|
else
|
||||||
|
echo " $INSTALL_DIR/mole # Interactive menu"
|
||||||
|
echo " $INSTALL_DIR/mole clean # System cleanup"
|
||||||
|
echo " $INSTALL_DIR/mole uninstall # Remove applications"
|
||||||
|
echo " $INSTALL_DIR/mole update # Update Mole to the latest version"
|
||||||
|
echo " $INSTALL_DIR/mole --version # Show installed version"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
# Uninstall function
|
# Uninstall function
|
||||||
uninstall_mole() {
|
uninstall_mole() {
|
||||||
log_info "Uninstalling mole..."
|
log_info "Uninstalling mole..."
|
||||||
@@ -276,7 +372,10 @@ uninstall_mole() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Main installation function
|
# Main installation function
|
||||||
main() {
|
perform_install() {
|
||||||
|
resolve_source_dir
|
||||||
|
local source_version
|
||||||
|
source_version="$(get_source_version || true)"
|
||||||
|
|
||||||
check_requirements
|
check_requirements
|
||||||
create_directories
|
create_directories
|
||||||
@@ -284,23 +383,67 @@ main() {
|
|||||||
verify_installation
|
verify_installation
|
||||||
setup_path
|
setup_path
|
||||||
|
|
||||||
if [[ ${VERBOSE} -eq 1 ]]; then
|
local installed_version
|
||||||
log_success "Mole installed successfully!"
|
installed_version="$(get_installed_version || true)"
|
||||||
echo ""
|
|
||||||
echo "Usage:"
|
if [[ -z "$installed_version" ]]; then
|
||||||
if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then
|
installed_version="$source_version"
|
||||||
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 ""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
print_usage_summary "installed" "$installed_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run installation
|
perform_update() {
|
||||||
|
check_requirements
|
||||||
|
|
||||||
|
local installed_version
|
||||||
|
installed_version="$(get_installed_version || true)"
|
||||||
|
|
||||||
|
if [[ -z "$installed_version" ]]; then
|
||||||
|
log_warning "Mole is not currently installed in $INSTALL_DIR. Running fresh installation."
|
||||||
|
perform_install
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_source_dir
|
||||||
|
local target_version
|
||||||
|
target_version="$(get_source_version || true)"
|
||||||
|
|
||||||
|
if [[ -z "$target_version" ]]; then
|
||||||
|
log_error "Unable to determine the latest Mole version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$installed_version" == "$target_version" ]]; then
|
||||||
|
log_success "Mole is already up to date (version $installed_version)!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating Mole from $installed_version to $target_version..."
|
||||||
|
|
||||||
|
create_directories
|
||||||
|
install_files
|
||||||
|
verify_installation
|
||||||
|
setup_path
|
||||||
|
|
||||||
|
local updated_version
|
||||||
|
updated_version="$(get_installed_version || true)"
|
||||||
|
|
||||||
|
if [[ -z "$updated_version" ]]; then
|
||||||
|
updated_version="$target_version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_usage_summary "updated" "$updated_version" "$installed_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run requested action
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
main
|
|
||||||
|
case "$ACTION" in
|
||||||
|
update)
|
||||||
|
perform_update
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
perform_install
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# App selection functionality
|
||||||
|
|
||||||
# App selection functionality using the new menu system
|
set -euo pipefail
|
||||||
# This replaces the complex interactive_app_selection function
|
|
||||||
|
|
||||||
# Interactive app selection using the menu.sh library
|
# Format app info for display
|
||||||
|
format_app_display() {
|
||||||
|
local display_name="$1" size="$2" last_used="$3"
|
||||||
|
|
||||||
|
# Truncate long names
|
||||||
|
local truncated_name="$display_name"
|
||||||
|
if [[ ${#display_name} -gt 24 ]]; then
|
||||||
|
truncated_name="${display_name:0:21}..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Format size
|
||||||
|
local size_str="Unknown"
|
||||||
|
[[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size"
|
||||||
|
|
||||||
|
printf "%-24s (%s) | %s" "$truncated_name" "$size_str" "$last_used"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global variable to store selection result (bash 3.2 compatible)
|
||||||
|
MOLE_SELECTION_RESULT=""
|
||||||
|
|
||||||
|
# Main app selection function
|
||||||
select_apps_for_uninstall() {
|
select_apps_for_uninstall() {
|
||||||
if [[ ${#apps_data[@]} -eq 0 ]]; then
|
if [[ ${#apps_data[@]} -eq 0 ]]; then
|
||||||
log_warning "No applications available for uninstallation"
|
log_warning "No applications available for uninstallation"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build menu options from apps_data
|
# Build menu options
|
||||||
local -a menu_options=()
|
local -a menu_options=()
|
||||||
for app_data in "${apps_data[@]}"; do
|
for app_data in "${apps_data[@]}"; do
|
||||||
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$app_data"
|
IFS='|' read -r epoch app_path display_name bundle_id size last_used <<< "$app_data"
|
||||||
|
menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")")
|
||||||
# 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
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -42,12 +43,9 @@ select_apps_for_uninstall() {
|
|||||||
echo "Found ${#apps_data[@]} apps. Select apps to remove:"
|
echo "Found ${#apps_data[@]} apps. Select apps to remove:"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Load paginated menu system (arrow key navigation)
|
# Use paginated menu - result will be stored in MOLE_SELECTION_RESULT
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/paginated_menu.sh"
|
MOLE_SELECTION_RESULT=""
|
||||||
|
paginated_multi_select "Select Apps to Remove" "${menu_options[@]}"
|
||||||
# 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=$?
|
local exit_code=$?
|
||||||
|
|
||||||
if [[ $exit_code -ne 0 ]]; then
|
if [[ $exit_code -ne 0 ]]; then
|
||||||
@@ -55,103 +53,30 @@ select_apps_for_uninstall() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$selected_indices" ]]; then
|
if [[ -z "$MOLE_SELECTION_RESULT" ]]; then
|
||||||
echo "No apps selected"
|
echo "No apps selected"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build selected_apps array from indices
|
# Build selected apps array (global variable in bin/uninstall.sh)
|
||||||
|
# Clear existing selections - compatible with bash 3.2
|
||||||
selected_apps=()
|
selected_apps=()
|
||||||
for idx in $selected_indices; do
|
|
||||||
# Validate that idx is a number
|
# Parse indices and build selected apps array
|
||||||
if [[ "$idx" =~ ^[0-9]+$ ]]; then
|
# Convert space-separated string to array for better handling
|
||||||
|
read -a indices_array <<< "$MOLE_SELECTION_RESULT"
|
||||||
|
|
||||||
|
for idx in "${indices_array[@]}"; do
|
||||||
|
if [[ "$idx" =~ ^[0-9]+$ ]] && [[ $idx -ge 0 ]] && [[ $idx -lt ${#apps_data[@]} ]]; then
|
||||||
selected_apps+=("${apps_data[idx]}")
|
selected_apps+=("${apps_data[idx]}")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Selected ${#selected_apps[@]} apps"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Alternative simplified single-select interface for quick selection
|
# Export function for external use
|
||||||
quick_select_app() {
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
if [[ ${#apps_data[@]} -eq 0 ]]; then
|
echo "This is a library file. Source it from other scripts." >&2
|
||||||
log_warning "No applications available for uninstallation"
|
exit 1
|
||||||
return 1
|
fi
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -2,55 +2,7 @@
|
|||||||
|
|
||||||
# Batch uninstall functionality with minimal confirmations
|
# Batch uninstall functionality with minimal confirmations
|
||||||
# Replaces the overly verbose individual confirmation approach
|
# Replaces the overly verbose individual confirmation approach
|
||||||
|
# Note: find_app_files() and calculate_total_size() functions now in lib/common.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"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Batch uninstall with single confirmation
|
# Batch uninstall with single confirmation
|
||||||
batch_uninstall_applications() {
|
batch_uninstall_applications() {
|
||||||
@@ -83,14 +35,12 @@ batch_uninstall_applications() {
|
|||||||
((total_estimated_size += total_kb))
|
((total_estimated_size += total_kb))
|
||||||
|
|
||||||
# Store details for later use
|
# Store details for later use
|
||||||
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$related_files")
|
# Base64 encode related_files to handle multi-line data safely
|
||||||
|
local encoded_files=$(echo "$related_files" | base64)
|
||||||
|
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Show summary and get batch confirmation
|
# Format size display
|
||||||
echo ""
|
|
||||||
echo "📊 Uninstallation Summary:"
|
|
||||||
echo " • Applications to remove: ${#selected_apps[@]}"
|
|
||||||
|
|
||||||
if [[ $total_estimated_size -gt 1048576 ]]; then
|
if [[ $total_estimated_size -gt 1048576 ]]; then
|
||||||
local size_display=$(echo "$total_estimated_size" | awk '{printf "%.2fGB", $1/1024/1024}')
|
local size_display=$(echo "$total_estimated_size" | awk '{printf "%.2fGB", $1/1024/1024}')
|
||||||
elif [[ $total_estimated_size -gt 1024 ]]; then
|
elif [[ $total_estimated_size -gt 1024 ]]; then
|
||||||
@@ -98,31 +48,26 @@ batch_uninstall_applications() {
|
|||||||
else
|
else
|
||||||
local size_display="${total_estimated_size}KB"
|
local size_display="${total_estimated_size}KB"
|
||||||
fi
|
fi
|
||||||
echo " • Estimated space to free: $size_display"
|
|
||||||
|
|
||||||
|
# Show summary and get batch confirmation
|
||||||
|
echo ""
|
||||||
|
echo "Will remove ${#selected_apps[@]} applications, free $size_display"
|
||||||
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
||||||
echo " • ⚠️ Running apps that will be force-quit:"
|
echo "Running apps will be force-quit: ${running_apps[*]}"
|
||||||
for app in "${running_apps[@]}"; do
|
|
||||||
echo " - $app"
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Selected applications:"
|
read -p "Press ENTER to confirm, or any other key to cancel: " -r
|
||||||
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 ""
|
if [[ -n "$REPLY" ]]; then
|
||||||
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"
|
log_info "Uninstallation cancelled by user"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "⚡ Starting uninstallation in 3 seconds... (Press Ctrl+C to abort)"
|
||||||
|
sleep 1 && echo "⚡ 2..."
|
||||||
|
sleep 1 && echo "⚡ 1..."
|
||||||
|
sleep 1
|
||||||
|
|
||||||
# Force quit running apps first (batch)
|
# Force quit running apps first (batch)
|
||||||
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
@@ -142,9 +87,11 @@ batch_uninstall_applications() {
|
|||||||
local failed_count=0
|
local failed_count=0
|
||||||
|
|
||||||
for detail in "${app_details[@]}"; do
|
for detail in "${app_details[@]}"; do
|
||||||
IFS='|' read -r app_name app_path bundle_id total_kb related_files <<< "$detail"
|
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
|
||||||
|
|
||||||
|
# Decode the related files list
|
||||||
|
local related_files=$(echo "$encoded_files" | base64 -d)
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🗑️ Uninstalling: $app_name"
|
echo "🗑️ Uninstalling: $app_name"
|
||||||
|
|
||||||
# Remove the application
|
# Remove the application
|
||||||
@@ -178,20 +125,27 @@ batch_uninstall_applications() {
|
|||||||
|
|
||||||
# Show final summary
|
# Show final summary
|
||||||
echo ""
|
echo ""
|
||||||
log_header "Uninstallation Complete"
|
echo "===================================================================="
|
||||||
|
echo "🎉 UNINSTALLATION COMPLETE!"
|
||||||
|
|
||||||
if [[ $success_count -gt 0 ]]; then
|
if [[ $success_count -gt 0 ]]; then
|
||||||
if [[ $total_size_freed -gt 1048576 ]]; then
|
if [[ $total_size_freed -gt 1048576 ]]; then
|
||||||
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}')
|
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fGB", $1/1024/1024}')
|
||||||
elif [[ $total_size_freed -gt 1024 ]]; then
|
elif [[ $total_size_freed -gt 1024 ]]; then
|
||||||
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
|
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
|
||||||
else
|
else
|
||||||
local freed_display="${total_size_freed}KB"
|
local freed_display="${total_size_freed}KB"
|
||||||
fi
|
fi
|
||||||
log_success "Successfully uninstalled $success_count applications"
|
echo "🗑️ Apps uninstalled: $success_count | Space freed: $freed_display"
|
||||||
log_success "Freed $freed_display of disk space"
|
else
|
||||||
|
echo "🗑️ No applications were uninstalled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $failed_count -gt 0 ]]; then
|
||||||
|
echo "⚠️ Failed to uninstall: $failed_count"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===================================================================="
|
||||||
if [[ $failed_count -gt 0 ]]; then
|
if [[ $failed_count -gt 0 ]]; then
|
||||||
log_warning "$failed_count applications failed to uninstall"
|
log_warning "$failed_count applications failed to uninstall"
|
||||||
fi
|
fi
|
||||||
|
|||||||
363
lib/common.sh
363
lib/common.sh
@@ -78,10 +78,19 @@ clear_screen() {
|
|||||||
printf '\033[2J\033[H'
|
printf '\033[2J\033[H'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hide_cursor() {
|
||||||
|
printf '\033[?25l'
|
||||||
|
}
|
||||||
|
|
||||||
|
show_cursor() {
|
||||||
|
printf '\033[?25h'
|
||||||
|
}
|
||||||
|
|
||||||
# Keyboard input handling (simple and robust)
|
# Keyboard input handling (simple and robust)
|
||||||
read_key() {
|
read_key() {
|
||||||
local key rest
|
local key rest
|
||||||
IFS= read -rsn1 key || return 1
|
# Use macOS bash 3.2 compatible read syntax
|
||||||
|
IFS= read -r -s -n 1 key || return 1
|
||||||
|
|
||||||
# Some terminals can yield empty on Enter with -n1; treat as ENTER
|
# Some terminals can yield empty on Enter with -n1; treat as ENTER
|
||||||
if [[ -z "$key" ]]; then
|
if [[ -z "$key" ]]; then
|
||||||
@@ -91,23 +100,23 @@ read_key() {
|
|||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
$'\n'|$'\r') echo "ENTER" ;;
|
$'\n'|$'\r') echo "ENTER" ;;
|
||||||
' ') echo " " ;;
|
' ') echo "SPACE" ;;
|
||||||
'q'|'Q') echo "QUIT" ;;
|
'q'|'Q') echo "QUIT" ;;
|
||||||
'a'|'A') echo "ALL" ;;
|
'a'|'A') echo "ALL" ;;
|
||||||
'n'|'N') echo "NONE" ;;
|
'n'|'N') echo "NONE" ;;
|
||||||
'?') echo "HELP" ;;
|
'?') echo "HELP" ;;
|
||||||
$'\x1b')
|
$'\x1b')
|
||||||
# Read the next two bytes within 1s; works well on macOS bash 3.2
|
# 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
|
if IFS= read -r -s -n 2 -t 1 rest 2>/dev/null; then
|
||||||
case "$rest" in
|
case "$rest" in
|
||||||
"[A") echo "UP" ;;
|
"[A") echo "UP" ;;
|
||||||
"[B") echo "DOWN" ;;
|
"[B") echo "DOWN" ;;
|
||||||
"[C") echo "RIGHT" ;;
|
"[C") echo "RIGHT" ;;
|
||||||
"[D") echo "LEFT" ;;
|
"[D") echo "LEFT" ;;
|
||||||
*) echo "ESC" ;;
|
*) echo "OTHER" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
echo "ESC"
|
echo "OTHER"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*) echo "OTHER" ;;
|
*) echo "OTHER" ;;
|
||||||
@@ -175,32 +184,6 @@ get_directory_size_bytes() {
|
|||||||
du -sk "$path" 2>/dev/null | cut -f1 | awk '{print $1 * 1024}' || echo "0"
|
du -sk "$path" 2>/dev/null | cut -f1 | awk '{print $1 * 1024}' || echo "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Safe file operation with backup
|
|
||||||
safe_remove() {
|
|
||||||
local path="$1"
|
|
||||||
local backup_dir="${2:-/tmp/mole_backup_$(date +%s)}"
|
|
||||||
local backup_enabled="${MOLE_BACKUP_ENABLED:-true}"
|
|
||||||
|
|
||||||
if [[ ! -e "$path" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$backup_enabled" == "true" ]]; then
|
|
||||||
# Create backup directory if it doesn't exist
|
|
||||||
mkdir -p "$backup_dir" 2>/dev/null || return 1
|
|
||||||
|
|
||||||
local basename_path
|
|
||||||
basename_path=$(basename "$path")
|
|
||||||
|
|
||||||
if ! cp -R "$path" "$backup_dir/$basename_path" 2>/dev/null; then
|
|
||||||
log_warning "Backup failed for $path, skipping removal"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
log_info "Backup created at $backup_dir/$basename_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf "$path" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Permission checks
|
# Permission checks
|
||||||
check_sudo() {
|
check_sudo() {
|
||||||
@@ -223,165 +206,179 @@ request_sudo() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration management
|
# Load basic configuration
|
||||||
readonly CONFIG_FILE="${HOME}/.config/mole/config"
|
|
||||||
|
|
||||||
# Load configuration with defaults
|
|
||||||
load_config() {
|
load_config() {
|
||||||
# Default configuration
|
|
||||||
MOLE_LOG_LEVEL="${MOLE_LOG_LEVEL:-INFO}"
|
|
||||||
MOLE_AUTO_CONFIRM="${MOLE_AUTO_CONFIRM:-false}"
|
|
||||||
MOLE_BACKUP_ENABLED="${MOLE_BACKUP_ENABLED:-true}"
|
|
||||||
MOLE_MAX_LOG_SIZE="${MOLE_MAX_LOG_SIZE:-1048576}"
|
MOLE_MAX_LOG_SIZE="${MOLE_MAX_LOG_SIZE:-1048576}"
|
||||||
MOLE_PARALLEL_JOBS="${MOLE_PARALLEL_JOBS:-}" # Empty means auto-detect
|
|
||||||
|
|
||||||
# Load user configuration if exists
|
|
||||||
if [[ -f "$CONFIG_FILE" ]]; then
|
|
||||||
source "$CONFIG_FILE" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save configuration
|
|
||||||
save_config() {
|
|
||||||
mkdir -p "$(dirname "$CONFIG_FILE")" 2>/dev/null || return 1
|
|
||||||
cat > "$CONFIG_FILE" << EOF
|
|
||||||
# Mole Configuration File
|
|
||||||
# Generated on $(date)
|
|
||||||
|
|
||||||
# Log level: DEBUG, INFO, WARNING, ERROR
|
|
||||||
MOLE_LOG_LEVEL="$MOLE_LOG_LEVEL"
|
|
||||||
|
|
||||||
# Auto confirm operations (true/false)
|
|
||||||
MOLE_AUTO_CONFIRM="$MOLE_AUTO_CONFIRM"
|
|
||||||
|
|
||||||
# Enable backup before deletion (true/false)
|
|
||||||
MOLE_BACKUP_ENABLED="$MOLE_BACKUP_ENABLED"
|
|
||||||
|
|
||||||
# Maximum log file size in bytes
|
|
||||||
MOLE_MAX_LOG_SIZE="$MOLE_MAX_LOG_SIZE"
|
|
||||||
|
|
||||||
# Number of parallel jobs for operations (empty = auto-detect)
|
|
||||||
MOLE_PARALLEL_JOBS="$MOLE_PARALLEL_JOBS"
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Progress tracking
|
|
||||||
# Use parameter expansion for portable global initialization (macOS bash lacks declare -g).
|
|
||||||
: "${PROGRESS_CURRENT:=0}"
|
|
||||||
: "${PROGRESS_TOTAL:=0}"
|
|
||||||
: "${PROGRESS_MESSAGE:=}"
|
|
||||||
|
|
||||||
# Initialize progress tracking
|
|
||||||
init_progress() {
|
|
||||||
PROGRESS_CURRENT=0
|
|
||||||
PROGRESS_TOTAL="$1"
|
|
||||||
PROGRESS_MESSAGE="${2:-Processing}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update progress
|
|
||||||
update_progress() {
|
|
||||||
PROGRESS_CURRENT="$1"
|
|
||||||
local message="${2:-$PROGRESS_MESSAGE}"
|
|
||||||
local percentage=$((PROGRESS_CURRENT * 100 / PROGRESS_TOTAL))
|
|
||||||
|
|
||||||
# Create progress bar
|
|
||||||
local bar_length=20
|
|
||||||
local filled_length=$((percentage * bar_length / 100))
|
|
||||||
local bar=""
|
|
||||||
|
|
||||||
for ((i=0; i<filled_length; i++)); do
|
|
||||||
bar="${bar}█"
|
|
||||||
done
|
|
||||||
|
|
||||||
for ((i=filled_length; i<bar_length; i++)); do
|
|
||||||
bar="${bar}░"
|
|
||||||
done
|
|
||||||
|
|
||||||
printf "\r${BLUE}[%s] %3d%% %s (%d/%d)${NC}" "$bar" "$percentage" "$message" "$PROGRESS_CURRENT" "$PROGRESS_TOTAL"
|
|
||||||
|
|
||||||
if [[ $PROGRESS_CURRENT -eq $PROGRESS_TOTAL ]]; then
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Spinner for indeterminate progress
|
|
||||||
: "${SPINNER_PID:=}"
|
|
||||||
|
|
||||||
start_spinner() {
|
|
||||||
local message="${1:-Working}"
|
|
||||||
stop_spinner # Stop any existing spinner
|
|
||||||
|
|
||||||
(
|
|
||||||
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
||||||
local i=0
|
|
||||||
while true; do
|
|
||||||
printf "\r${BLUE}%s %s${NC}" "${spin:$i:1}" "$message"
|
|
||||||
((i++))
|
|
||||||
if [[ $i -eq ${#spin} ]]; then
|
|
||||||
i=0
|
|
||||||
fi
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
SPINNER_PID=$!
|
|
||||||
}
|
|
||||||
|
|
||||||
stop_spinner() {
|
|
||||||
if [[ -n "$SPINNER_PID" ]]; then
|
|
||||||
kill "$SPINNER_PID" 2>/dev/null || true
|
|
||||||
wait "$SPINNER_PID" 2>/dev/null || true
|
|
||||||
SPINNER_PID=""
|
|
||||||
printf "\r\033[K" # Clear the line
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Calculate optimal parallel jobs based on system resources
|
|
||||||
get_optimal_parallel_jobs() {
|
|
||||||
local operation_type="${1:-default}"
|
|
||||||
local optimal_parallel=4
|
|
||||||
|
|
||||||
# Try to detect optimal parallel jobs based on CPU cores
|
|
||||||
if command -v nproc >/dev/null 2>&1; then
|
|
||||||
optimal_parallel=$(nproc)
|
|
||||||
elif command -v sysctl >/dev/null 2>&1; then
|
|
||||||
optimal_parallel=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Apply operation-specific limits
|
|
||||||
case "$operation_type" in
|
|
||||||
"scan")
|
|
||||||
# For scanning: min 2, max 8
|
|
||||||
if [[ $optimal_parallel -lt 2 ]]; then
|
|
||||||
optimal_parallel=2
|
|
||||||
elif [[ $optimal_parallel -gt 8 ]]; then
|
|
||||||
optimal_parallel=8
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"clean")
|
|
||||||
# For file operations: min 2, max 6 (more conservative)
|
|
||||||
if [[ $optimal_parallel -lt 2 ]]; then
|
|
||||||
optimal_parallel=2
|
|
||||||
elif [[ $optimal_parallel -gt 6 ]]; then
|
|
||||||
optimal_parallel=6
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# Default: min 2, max 4 (safest)
|
|
||||||
if [[ $optimal_parallel -lt 2 ]]; then
|
|
||||||
optimal_parallel=2
|
|
||||||
elif [[ $optimal_parallel -gt 4 ]]; then
|
|
||||||
optimal_parallel=4
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Use configured value if available, otherwise use calculated optimal
|
|
||||||
if [[ -n "${MOLE_PARALLEL_JOBS:-}" ]]; then
|
|
||||||
echo "$MOLE_PARALLEL_JOBS"
|
|
||||||
else
|
|
||||||
echo "$optimal_parallel"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Initialize configuration on sourcing
|
# Initialize configuration on sourcing
|
||||||
load_config
|
load_config
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# App Management Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Essential system and critical app patterns that should never be removed
|
||||||
|
readonly PRESERVED_BUNDLE_PATTERNS=(
|
||||||
|
# System essentials
|
||||||
|
"com.apple.*"
|
||||||
|
"loginwindow"
|
||||||
|
"dock"
|
||||||
|
"systempreferences"
|
||||||
|
"finder"
|
||||||
|
"safari"
|
||||||
|
"keychain*"
|
||||||
|
"security*"
|
||||||
|
"bluetooth*"
|
||||||
|
"wifi*"
|
||||||
|
"network*"
|
||||||
|
"tcc"
|
||||||
|
"notification*"
|
||||||
|
"accessibility*"
|
||||||
|
"universalaccess*"
|
||||||
|
"HIToolbox*"
|
||||||
|
"textinput*"
|
||||||
|
"TextInput*"
|
||||||
|
"keyboard*"
|
||||||
|
"Keyboard*"
|
||||||
|
"inputsource*"
|
||||||
|
"InputSource*"
|
||||||
|
"keylayout*"
|
||||||
|
"KeyLayout*"
|
||||||
|
"GlobalPreferences"
|
||||||
|
".GlobalPreferences"
|
||||||
|
|
||||||
|
# Input methods (critical for international users)
|
||||||
|
"com.tencent.inputmethod.*"
|
||||||
|
"com.sogou.*"
|
||||||
|
"com.baidu.*"
|
||||||
|
"*.inputmethod.*"
|
||||||
|
"*input*"
|
||||||
|
"*inputmethod*"
|
||||||
|
"*InputMethod*"
|
||||||
|
"*ime*"
|
||||||
|
"*IME*"
|
||||||
|
|
||||||
|
# Cleanup and system tools (avoid infinite loops and preserve licenses)
|
||||||
|
"com.nektony.*" # App Cleaner & Uninstaller
|
||||||
|
"com.macpaw.*" # CleanMyMac, CleanMaster
|
||||||
|
"com.freemacsoft.AppCleaner" # AppCleaner
|
||||||
|
"com.omnigroup.omnidisksweeper" # OmniDiskSweeper
|
||||||
|
"com.daisydiskapp.*" # DaisyDisk
|
||||||
|
"com.tunabellysoftware.*" # Disk Utility apps
|
||||||
|
"com.grandperspectiv.*" # GrandPerspective
|
||||||
|
"com.binaryfruit.*" # FusionCast
|
||||||
|
"com.CharlesProxy.*" # Charles Proxy (paid)
|
||||||
|
"com.proxyman.*" # Proxyman (paid)
|
||||||
|
"com.getpaw.*" # Paw (paid)
|
||||||
|
|
||||||
|
# Security and password managers (critical data)
|
||||||
|
"com.1password.*" # 1Password
|
||||||
|
"com.agilebits.*" # 1Password legacy
|
||||||
|
"com.lastpass.*" # LastPass
|
||||||
|
"com.dashlane.*" # Dashlane
|
||||||
|
"com.bitwarden.*" # Bitwarden
|
||||||
|
"com.keepassx.*" # KeePassXC
|
||||||
|
|
||||||
|
# Development tools (licenses and settings)
|
||||||
|
"com.jetbrains.*" # JetBrains IDEs (paid licenses)
|
||||||
|
"com.sublimetext.*" # Sublime Text (paid)
|
||||||
|
"com.panic.transmit*" # Transmit (paid)
|
||||||
|
"com.sequelpro.*" # Database tools
|
||||||
|
"com.sequel-ace.*"
|
||||||
|
"com.tinyapp.*" # TablePlus (paid)
|
||||||
|
|
||||||
|
# Design tools (expensive licenses)
|
||||||
|
"com.adobe.*" # Adobe Creative Suite
|
||||||
|
"com.bohemiancoding.*" # Sketch
|
||||||
|
"com.figma.*" # Figma
|
||||||
|
"com.framerx.*" # Framer
|
||||||
|
"com.zeplin.*" # Zeplin
|
||||||
|
"com.invisionapp.*" # InVision
|
||||||
|
"com.principle.*" # Principle
|
||||||
|
|
||||||
|
# Productivity (important data and licenses)
|
||||||
|
"com.omnigroup.*" # OmniFocus, OmniGraffle, etc.
|
||||||
|
"com.culturedcode.*" # Things
|
||||||
|
"com.todoist.*" # Todoist
|
||||||
|
"com.bear-writer.*" # Bear
|
||||||
|
"com.typora.*" # Typora
|
||||||
|
"com.ulyssesapp.*" # Ulysses
|
||||||
|
"com.literatureandlatte.*" # Scrivener
|
||||||
|
"com.dayoneapp.*" # Day One
|
||||||
|
|
||||||
|
# Media and entertainment (licenses)
|
||||||
|
"com.spotify.client" # Spotify (premium accounts)
|
||||||
|
"com.apple.FinalCutPro" # Final Cut Pro
|
||||||
|
"com.apple.Motion" # Motion
|
||||||
|
"com.apple.Compressor" # Compressor
|
||||||
|
"com.blackmagic-design.*" # DaVinci Resolve
|
||||||
|
"com.pixelmatorteam.*" # Pixelmator
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if bundle should be preserved (system/critical 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find and list app-related files (consolidated from duplicates)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Only print if array has elements to avoid unbound variable error
|
||||||
|
if [[ ${#files_to_clean[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${files_to_clean[@]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate total size of files (consolidated from duplicates)
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|||||||
369
lib/menu.sh
369
lib/menu.sh
@@ -1,369 +0,0 @@
|
|||||||
#!/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 (allow reuse when sourced after lib/common.sh)
|
|
||||||
if [[ -z "${ESC+x}" ]]; then
|
|
||||||
readonly ESC=$'\033'
|
|
||||||
fi
|
|
||||||
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
|
|
||||||
@@ -1,64 +1,29 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Paginated menu with arrow key navigation
|
||||||
|
|
||||||
# Proper paginated menu with arrow key navigation
|
set -euo pipefail
|
||||||
# 10 items per page, up/down to navigate, space to select, left/right to change pages
|
|
||||||
|
|
||||||
# Terminal control functions
|
# Terminal control functions
|
||||||
hide_cursor() { printf '\033[?25l' >&2; }
|
enter_alt_screen() { tput smcup 2>/dev/null || true; }
|
||||||
show_cursor() { printf '\033[?25h' >&2; }
|
leave_alt_screen() { tput rmcup 2>/dev/null || true; }
|
||||||
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)
|
# Main paginated multi-select menu function
|
||||||
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() {
|
paginated_multi_select() {
|
||||||
local title="$1"
|
local title="$1"
|
||||||
shift
|
shift
|
||||||
local -a items=("$@")
|
local -a items=("$@")
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
if [[ ${#items[@]} -eq 0 ]]; then
|
||||||
|
echo "No items provided" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local total_items=${#items[@]}
|
local total_items=${#items[@]}
|
||||||
local items_per_page=10 # Reduced for better readability
|
local items_per_page=10
|
||||||
local total_pages=$(( (total_items + items_per_page - 1) / items_per_page ))
|
local total_pages=$(( (total_items + items_per_page - 1) / items_per_page ))
|
||||||
local current_page=0
|
local current_page=0
|
||||||
local cursor_pos=0 # Position within current page (0-9)
|
local cursor_pos=0
|
||||||
local -a selected=()
|
local -a selected=()
|
||||||
|
|
||||||
# Initialize selection array
|
# Initialize selection array
|
||||||
@@ -69,78 +34,60 @@ paginated_multi_select() {
|
|||||||
# Cleanup function
|
# Cleanup function
|
||||||
cleanup() {
|
cleanup() {
|
||||||
show_cursor
|
show_cursor
|
||||||
stty echo 2>/dev/null || true
|
stty echo icanon 2>/dev/null || true
|
||||||
stty icanon 2>/dev/null || true
|
|
||||||
leave_alt_screen
|
leave_alt_screen
|
||||||
enable_wrap
|
|
||||||
}
|
}
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
# Setup terminal for optimal responsiveness
|
# Setup terminal
|
||||||
stty -echo -icanon min 1 time 0 2>/dev/null || true
|
stty -echo -icanon 2>/dev/null || true
|
||||||
enter_alt_screen
|
enter_alt_screen
|
||||||
disable_wrap
|
|
||||||
hide_cursor
|
hide_cursor
|
||||||
|
|
||||||
# Main display function
|
# Helper functions
|
||||||
first_draw=1
|
print_line() { printf "\r\033[2K%s\n" "$1" >&2; }
|
||||||
# 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() {
|
||||||
render_item_line() {
|
local idx=$1 is_current=$2
|
||||||
local page_pos=$1
|
|
||||||
local start_idx=$((current_page * items_per_page))
|
|
||||||
local i=$((start_idx + page_pos))
|
|
||||||
local checkbox="☐"
|
local checkbox="☐"
|
||||||
local cursor_marker=" "
|
[[ ${selected[idx]} == true ]] && checkbox="☑"
|
||||||
[[ ${selected[i]} == true ]] && checkbox="☑"
|
|
||||||
if [[ $page_pos -eq $cursor_pos ]]; then
|
if [[ $is_current == true ]]; then
|
||||||
cursor_marker="▶ "
|
printf "\r\033[2K\033[7m▶ %s %s\033[0m\n" "$checkbox" "${items[idx]}" >&2
|
||||||
printf "\r\033[2K\033[7m%s%s %s\033[0m\n" "$cursor_marker" "$checkbox" "${items[i]}" >&2
|
|
||||||
else
|
else
|
||||||
printf "\r\033[2K%s%s %s\n" "$cursor_marker" "$checkbox" "${items[i]}" >&2
|
printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: move cursor to top-left anchor saved by tput sc
|
# Draw the complete menu
|
||||||
to_anchor() { tput rc >/dev/null 2>&1 || true; }
|
|
||||||
|
|
||||||
# Full draw of entire screen - simplified for stability
|
|
||||||
draw_menu() {
|
draw_menu() {
|
||||||
# Always do full screen redraw for reliability
|
printf "\033[H\033[J" >&2 # Clear screen and move to top
|
||||||
clear_screen
|
|
||||||
|
|
||||||
# Simple header
|
# Header
|
||||||
printf "%s\n" "$title" >&2
|
printf "%s\n%s\n" "$title" "$(printf '=%.0s' $(seq 1 ${#title}))" >&2
|
||||||
printf "%s\n" "$(printf '=%.0s' $(seq 1 ${#title}))" >&2
|
|
||||||
|
|
||||||
# Status bar
|
# Status
|
||||||
local selected_count=0
|
local selected_count=0
|
||||||
for ((i = 0; i < total_items; i++)); do
|
for ((i = 0; i < total_items; i++)); do
|
||||||
[[ ${selected[i]} == true ]] && ((selected_count++))
|
[[ ${selected[i]} == true ]] && ((selected_count++))
|
||||||
done
|
done
|
||||||
|
printf "Page %d/%d │ Total: %d │ Selected: %d\n\n" \
|
||||||
printf "Page %d/%d │ Total: %d │ Selected: %d\n" \
|
|
||||||
$((current_page + 1)) $total_pages $total_items $selected_count >&2
|
$((current_page + 1)) $total_pages $total_items $selected_count >&2
|
||||||
print_line ""
|
|
||||||
|
|
||||||
# Calculate page boundaries
|
# Items for current page
|
||||||
local start_idx=$((current_page * items_per_page))
|
local start_idx=$((current_page * items_per_page))
|
||||||
local end_idx=$((start_idx + items_per_page - 1))
|
local end_idx=$((start_idx + items_per_page - 1))
|
||||||
[[ $end_idx -ge $total_items ]] && end_idx=$((total_items - 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
|
for ((i = start_idx; i <= end_idx; i++)); do
|
||||||
local page_pos=$((i - start_idx))
|
local is_current=false
|
||||||
render_item_line "$page_pos"
|
[[ $((i - start_idx)) -eq $cursor_pos ]] && is_current=true
|
||||||
|
render_item $i $is_current
|
||||||
done
|
done
|
||||||
|
|
||||||
# Fill empty slots to always print items_per_page lines
|
# Fill empty slots
|
||||||
local items_on_page=$((end_idx - start_idx + 1))
|
local items_shown=$((end_idx - start_idx + 1))
|
||||||
for ((i = items_on_page; i < items_per_page; i++)); do
|
for ((i = items_shown; i < items_per_page; i++)); do
|
||||||
print_line ""
|
print_line ""
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -148,45 +95,42 @@ paginated_multi_select() {
|
|||||||
print_line "↑↓: Navigate | Space: Select | Enter: Confirm | Q: Exit"
|
print_line "↑↓: Navigate | Space: Select | Enter: Confirm | Q: Exit"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Help screen
|
# Show help screen
|
||||||
show_help() {
|
show_help() {
|
||||||
clear_screen
|
printf "\033[H\033[J" >&2
|
||||||
echo "App Uninstaller - Help" >&2
|
cat >&2 << 'EOF'
|
||||||
echo "======================" >&2
|
Help - Navigation Controls
|
||||||
echo >&2
|
==========================
|
||||||
echo " ↑ / ↓ Navigate up/down" >&2
|
|
||||||
echo " ← / → Previous/next page" >&2
|
↑ / ↓ Navigate up/down
|
||||||
echo " Space Select/deselect app" >&2
|
Space Select/deselect item
|
||||||
echo " Enter Confirm selection" >&2
|
Enter Confirm selection
|
||||||
echo " A Select all" >&2
|
A Select all
|
||||||
echo " N Deselect all" >&2
|
N Deselect all
|
||||||
echo " Q Exit" >&2
|
Q Exit
|
||||||
echo >&2
|
|
||||||
read -p "Press any key to continue..." -n 1 >&2
|
Press any key to continue...
|
||||||
|
EOF
|
||||||
|
read -n 1 -s >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main loop - simplified to always do full redraws for stability
|
# Main interaction loop
|
||||||
while true; do
|
while true; do
|
||||||
draw_menu # Always full redraw to avoid display issues
|
draw_menu
|
||||||
|
|
||||||
local key=$(read_key)
|
local key=$(read_key)
|
||||||
|
|
||||||
# Immediate exit key
|
|
||||||
if [[ "$key" == "QUIT" ]]; then
|
|
||||||
cleanup
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
|
"QUIT") cleanup; return 1 ;;
|
||||||
"UP")
|
"UP")
|
||||||
if [[ $cursor_pos -gt 0 ]]; then
|
if [[ $cursor_pos -gt 0 ]]; then
|
||||||
((cursor_pos--))
|
((cursor_pos--))
|
||||||
elif [[ $current_page -gt 0 ]]; then
|
elif [[ $current_page -gt 0 ]]; then
|
||||||
((current_page--))
|
((current_page--))
|
||||||
cursor_pos=$((items_per_page - 1))
|
# Calculate cursor position for new page
|
||||||
local start_idx=$((current_page * items_per_page))
|
local start_idx=$((current_page * items_per_page))
|
||||||
local end_idx=$((start_idx + items_per_page - 1))
|
local items_on_page=$((total_items - start_idx))
|
||||||
[[ $end_idx -ge $total_items ]] && cursor_pos=$((total_items - start_idx - 1))
|
[[ $items_on_page -gt $items_per_page ]] && items_on_page=$items_per_page
|
||||||
|
cursor_pos=$((items_on_page - 1))
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"DOWN")
|
"DOWN")
|
||||||
@@ -201,33 +145,13 @@ paginated_multi_select() {
|
|||||||
cursor_pos=0
|
cursor_pos=0
|
||||||
fi
|
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")
|
"SPACE")
|
||||||
local actual_idx=$((current_page * items_per_page + cursor_pos))
|
local idx=$((current_page * items_per_page + cursor_pos))
|
||||||
if [[ $actual_idx -lt $total_items ]]; then
|
if [[ $idx -lt $total_items ]]; then
|
||||||
if [[ ${selected[actual_idx]} == true ]]; then
|
if [[ ${selected[idx]} == true ]]; then
|
||||||
selected[actual_idx]=false
|
selected[idx]=false
|
||||||
else
|
else
|
||||||
selected[actual_idx]=true
|
selected[idx]=true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
@@ -241,11 +165,9 @@ paginated_multi_select() {
|
|||||||
selected[i]=false
|
selected[i]=false
|
||||||
done
|
done
|
||||||
;;
|
;;
|
||||||
"HELP")
|
"HELP") show_help ;;
|
||||||
show_help
|
|
||||||
;;
|
|
||||||
"ENTER")
|
"ENTER")
|
||||||
# If no items are selected, select the current item
|
# Auto-select current item if nothing selected
|
||||||
local has_selection=false
|
local has_selection=false
|
||||||
for ((i = 0; i < total_items; i++)); do
|
for ((i = 0; i < total_items; i++)); do
|
||||||
if [[ ${selected[i]} == true ]]; then
|
if [[ ${selected[i]} == true ]]; then
|
||||||
@@ -255,58 +177,38 @@ paginated_multi_select() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ $has_selection == false ]]; then
|
if [[ $has_selection == false ]]; then
|
||||||
# Select current item under cursor
|
local idx=$((current_page * items_per_page + cursor_pos))
|
||||||
local actual_idx=$((current_page * items_per_page + cursor_pos))
|
[[ $idx -lt $total_items ]] && selected[idx]=true
|
||||||
if [[ $actual_idx -lt $total_items ]]; then
|
|
||||||
selected[actual_idx]=true
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build result
|
# Store result in global variable instead of returning via stdout
|
||||||
local result=""
|
local result=""
|
||||||
for ((i = 0; i < total_items; i++)); do
|
for ((i = 0; i < total_items; i++)); do
|
||||||
if [[ ${selected[i]} == true ]]; then
|
if [[ ${selected[i]} == true ]]; then
|
||||||
result="$result $i"
|
result="$result $i"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
cleanup
|
local final_result="${result# }"
|
||||||
echo "${result# }"
|
|
||||||
|
# Remove the trap to avoid cleanup on normal exit
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
|
||||||
|
# Store result in global variable
|
||||||
|
MOLE_SELECTION_RESULT="$final_result"
|
||||||
|
|
||||||
|
# Manually cleanup terminal before returning
|
||||||
|
show_cursor
|
||||||
|
stty echo icanon 2>/dev/null || true
|
||||||
|
leave_alt_screen
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
|
||||||
# Ignore unrecognized keys - just continue the loop
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Demo function
|
# Export function for external use
|
||||||
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
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
demo_paginated
|
echo "This is a library file. Source it from other scripts." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
167
mole
167
mole
@@ -24,8 +24,12 @@ VERSION="1.0.0"
|
|||||||
MOLE_TAGLINE="Dig deep like a mole to clean your Mac."
|
MOLE_TAGLINE="Dig deep like a mole to clean your Mac."
|
||||||
|
|
||||||
show_brand_banner() {
|
show_brand_banner() {
|
||||||
printf '%b🦡 %bMOLE%b — %b%s%b\n' \
|
printf '%b🦡 Mole — %s%b\n' \
|
||||||
"$PURPLE" "$BLUE" "$NC" "$GREEN" "$MOLE_TAGLINE" "$NC"
|
"$GREEN" "$MOLE_TAGLINE" "$NC"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_version() {
|
||||||
|
printf 'Mole version %s\n' "$VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
@@ -38,115 +42,142 @@ show_help() {
|
|||||||
printf " %s%-16s%s %s\n" "$GREEN" "mole" "$NC" "Interactive main menu"
|
printf " %s%-16s%s %s\n" "$GREEN" "mole" "$NC" "Interactive main menu"
|
||||||
printf " %s%-16s%s %s\n" "$GREEN" "mole clean" "$NC" "Deeper system cleanup"
|
printf " %s%-16s%s %s\n" "$GREEN" "mole clean" "$NC" "Deeper system cleanup"
|
||||||
printf " %s%-16s%s %s\n" "$GREEN" "mole uninstall" "$NC" "Remove applications completely"
|
printf " %s%-16s%s %s\n" "$GREEN" "mole uninstall" "$NC" "Remove applications completely"
|
||||||
|
printf " %s%-16s%s %s\n" "$GREEN" "mole update" "$NC" "Update Mole to the latest version"
|
||||||
|
printf " %s%-16s%s %s\n" "$GREEN" "mole --version" "$NC" "Show installed version"
|
||||||
printf " %s%-16s%s %s\n" "$GREEN" "mole --help" "$NC" "Show this help message"
|
printf " %s%-16s%s %s\n" "$GREEN" "mole --help" "$NC" "Show this help message"
|
||||||
|
|
||||||
printf "\n%s%s%s\n" "$BLUE" "MORE" "$NC"
|
printf "\n%s%s%s\n" "$BLUE" "MORE" "$NC"
|
||||||
printf " https://github.com/tw93/mole\n"
|
printf " https://github.com/tw93/mole\n\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Simple update function
|
||||||
|
update_mole() {
|
||||||
|
log_info "Updating Mole..."
|
||||||
|
|
||||||
|
local installer_url="https://raw.githubusercontent.com/tw93/mole/main/install.sh"
|
||||||
|
local tmp_installer
|
||||||
|
tmp_installer="$(mktemp)" || { log_error "Failed to create temp file"; exit 1; }
|
||||||
|
|
||||||
|
# Download installer
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if ! curl -fsSL "$installer_url" -o "$tmp_installer"; then
|
||||||
|
rm -f "$tmp_installer"
|
||||||
|
log_error "Failed to download installer"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
if ! wget -qO "$tmp_installer" "$installer_url"; then
|
||||||
|
rm -f "$tmp_installer"
|
||||||
|
log_error "Failed to download installer"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$tmp_installer"
|
||||||
|
log_error "Please install curl or wget to update Mole"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$tmp_installer"
|
||||||
|
|
||||||
|
# Determine install directory
|
||||||
|
local mole_path
|
||||||
|
mole_path="$(command -v mole 2>/dev/null || echo "$0")"
|
||||||
|
local install_dir
|
||||||
|
install_dir="$(cd "$(dirname "$mole_path")" && pwd)"
|
||||||
|
|
||||||
|
# Run installer
|
||||||
|
if "$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" --update 2>/dev/null; then
|
||||||
|
log_success "Mole updated successfully"
|
||||||
|
else
|
||||||
|
log_warning "Update failed, trying reinstall..."
|
||||||
|
if "$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole"; then
|
||||||
|
log_success "Mole reinstalled successfully"
|
||||||
|
else
|
||||||
|
rm -f "$tmp_installer"
|
||||||
|
log_error "Update failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$tmp_installer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display main menu options
|
||||||
show_main_menu() {
|
show_main_menu() {
|
||||||
local selected="${1:-1}"
|
local selected="${1:-1}"
|
||||||
local redraw_full="${2:-true}"
|
local full_draw="${2:-true}"
|
||||||
|
|
||||||
if [[ "$redraw_full" == "true" ]]; then
|
if [[ "$full_draw" == "true" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
show_brand_banner
|
show_brand_banner
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Save cursor position before printing menu items
|
printf '\033[s' # Save cursor position
|
||||||
printf '\033[s'
|
|
||||||
|
|
||||||
show_menu_option 1 "Clean System - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)"
|
show_menu_option 1 "Clean System - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)"
|
||||||
show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)"
|
show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $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 3 "Help & Information - Usage guide and tips" "$([[ $selected -eq 3 ]] && echo true || echo false)"
|
||||||
show_menu_option 4 "Exit - Close Mole" "$([[ $selected -eq 4 ]] && echo true || echo false)"
|
show_menu_option 4 "Exit - Close Mole" "$([[ $selected -eq 4 ]] && echo true || echo false)"
|
||||||
|
|
||||||
if [[ "$redraw_full" == "true" ]]; then
|
if [[ "$full_draw" == "true" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}Use ↑/↓ arrows to navigate, ENTER to select, ESC/q to quit${NC}"
|
echo -e "${BLUE}↑/↓ to navigate, ENTER to select, Q to quit${NC}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Interactive main menu loop
|
||||||
interactive_main_menu() {
|
interactive_main_menu() {
|
||||||
local current_option=1
|
local current_option=1
|
||||||
local total_options=4
|
|
||||||
local first_draw=true
|
local first_draw=true
|
||||||
|
|
||||||
# Set up signal trapping to handle Ctrl+C gracefully
|
cleanup_and_exit() {
|
||||||
trap 'echo ""; echo "Thank you for using Mole!"; exit 0' INT
|
printf '\033[u\033[J' # Restore cursor and clear
|
||||||
|
show_cursor
|
||||||
cleanup_menu_display() {
|
echo ""
|
||||||
printf '\033[u\033[J'
|
echo "Thank you for using Mole!"
|
||||||
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trap cleanup_and_exit INT
|
||||||
|
hide_cursor
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
if [[ "$first_draw" == "true" ]]; then
|
if [[ "$first_draw" == "true" ]]; then
|
||||||
show_main_menu $current_option true
|
show_main_menu $current_option true
|
||||||
first_draw=false
|
first_draw=false
|
||||||
else
|
else
|
||||||
# Restore cursor to saved position and only redraw menu items
|
printf '\033[u' # Restore cursor position
|
||||||
printf '\033[u'
|
|
||||||
show_main_menu $current_option false
|
show_main_menu $current_option false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Use a more robust way to capture key input
|
local key=$(read_key)
|
||||||
local key
|
[[ $? -ne 0 ]] && continue
|
||||||
key=$(read_key)
|
|
||||||
|
|
||||||
# Check if read_key failed
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
"UP")
|
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
||||||
((current_option > 1)) && ((current_option--))
|
"DOWN") ((current_option < 4)) && ((current_option++)) ;;
|
||||||
;;
|
"ENTER"|"$current_option")
|
||||||
"DOWN")
|
printf '\033[u\033[J' # Clear menu
|
||||||
((current_option < total_options)) && ((current_option++))
|
show_cursor
|
||||||
;;
|
|
||||||
"ENTER")
|
|
||||||
cleanup_menu_display
|
|
||||||
case $current_option in
|
case $current_option in
|
||||||
1)
|
1) exec "$SCRIPT_DIR/bin/clean.sh" ;;
|
||||||
exec "$SCRIPT_DIR/bin/clean.sh"
|
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
||||||
;;
|
3) clear; show_help; exit 0 ;;
|
||||||
2)
|
4) cleanup_and_exit ;;
|
||||||
exec "$SCRIPT_DIR/bin/uninstall.sh"
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
clear_screen
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
clear_screen
|
|
||||||
echo ""
|
|
||||||
echo "Thank you for using Mole!"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
"QUIT"|"ESC")
|
"QUIT") cleanup_and_exit ;;
|
||||||
cleanup_menu_display
|
[1-4])
|
||||||
clear_screen
|
printf '\033[u\033[J'
|
||||||
echo ""
|
show_cursor
|
||||||
echo "Thank you for using Mole!"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
"1"|"2"|"3"|"4")
|
|
||||||
cleanup_menu_display
|
|
||||||
case $key in
|
case $key in
|
||||||
1) exec "$SCRIPT_DIR/bin/clean.sh" ;;
|
1) exec "$SCRIPT_DIR/bin/clean.sh" ;;
|
||||||
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
||||||
3) clear_screen; show_help; exit 0 ;;
|
3) clear; show_help; exit 0 ;;
|
||||||
4) clear_screen; echo ""; echo "Thank you for using Mole!"; exit 0 ;;
|
4) cleanup_and_exit ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
*)
|
|
||||||
# Ignore unrecognized keys
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -159,10 +190,18 @@ main() {
|
|||||||
"uninstall")
|
"uninstall")
|
||||||
exec "$SCRIPT_DIR/bin/uninstall.sh"
|
exec "$SCRIPT_DIR/bin/uninstall.sh"
|
||||||
;;
|
;;
|
||||||
|
"update")
|
||||||
|
update_mole
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
"help"|"--help"|"-h")
|
"help"|"--help"|"-h")
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
"version"|"--version"|"-V")
|
||||||
|
show_version
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
"")
|
"")
|
||||||
interactive_main_menu
|
interactive_main_menu
|
||||||
;;
|
;;
|
||||||
|
|||||||
Reference in New Issue
Block a user