diff --git a/.gitignore b/.gitignore
index 6aafc4a..0c59c53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,5 @@ temp/
*.lock
# Claude Code
-.claude/
\ No newline at end of file
+.claude/
+CLAUDE.md
diff --git a/README.md b/README.md
index 39d96b6..9d12abb 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,24 @@
-

+
Mole
-
π§Ή Like a mole, dig deep to clean your mac.
+
𦑠Dig deep like a mole to clean your Mac.
+
A Bash toolkit that tunnels through caches, leftovers, and forgotten libraries so your macOS stays fast without risking the essentials.
-## Features
+## Highlights
-- π¦ Deep Clean: System/user caches, logs, temp and more
-- π‘οΈ Safe by default: Skips critical system and input method settings
-- π App Uninstall: Remove app bundle and related data comprehensively
-- π» Smooth TUI: Fast arrow-key menus with pagination for large lists
+- 𦑠Deep-clean hidden caches, logs, and temp files in one sweep
+- π‘ Guardrails built in: skip vital macOS and input method data
+- π¦ Smart uninstall removes apps together with every leftover directory
+- β‘οΈ Fast arrow-key TUI with pagination for big app lists
-## Installation
+## Install & Update
```bash
curl -fsSL https://raw.githubusercontent.com/tw93/mole/main/install.sh | bash
```
-## Usage
+## Daily Commands
```bash
mole # Interactive main menu
@@ -26,33 +27,23 @@ mole uninstall # Interactive app uninstaller
mole --help # Show help
```
-### Example Output
+### Quick Peek
```bash
-π³οΈ Mole - Deeper system cleanup
-========================
-π Detected: Apple Silicon | πΎ Free space: 45.2GB
-π Mode: User-level cleanup (no password required)
+$ mole clean
+𦑠MOLE β Dig deep like a mole to clean your Mac.
-βΆ System essentials
- β User app cache
- β User app logs
- β Trash
+Collecting inventory ...
-βΆ Browser cleanup
- β Safari cache
- β Chrome cache
+βΆ System essentials freed 3.1GB (caches, logs, trash)
+βΆ Browser cleanup freed 820MB (Safari, Chrome, Arc)
+βΆ Developer tools freed 4.6GB (npm, Docker, Homebrew)
-βΆ Developer tools
- β npm cache
- β Docker resources
- β Homebrew cache
-
-π Cleanup complete | πΎ Freed space: 8.45GB
-π Items processed: 342 | πΎ Free space now: 53.7GB
+π Done! 8.5GB reclaimed across 342 items.
+π‘ Tip: run `mole --help` to discover more commands.
```
-## What Gets Cleaned
+## What Mole Cleans
| Category | Items Cleaned | Safety |
|---|---|---|
@@ -63,7 +54,7 @@ mole --help # Show help
| π± Apps | Common app caches (e.g., Slack, Discord, Teams, Notion, 1Password) | Safe |
| π Apple Silicon | Rosetta 2, media services, user activity caches | Safe |
-## Uninstaller
+## Smart Uninstall
- Fast scan of `/Applications` with system-app filtering (e.g., `com.apple.*`)
- Ranks apps by last used time and shows size hints
diff --git a/bin/clean.sh b/bin/clean.sh
index ea3de07..91f3105 100755
--- a/bin/clean.sh
+++ b/bin/clean.sh
@@ -298,12 +298,7 @@ start_cleanup() {
echo "π³οΈ Mole - Deeper system cleanup"
echo "=================================================="
echo ""
- echo "This will clean:"
- echo " β’ App caches and logs"
- echo " β’ Browser data"
- echo " β’ Developer tool caches"
- echo " β’ Temporary files"
- echo " β’ And much more..."
+ echo "This will clean: App caches & logs, Browser data, Developer tools, Temporary files & more..."
echo ""
# Check if we're in an interactive terminal
@@ -697,7 +692,7 @@ perform_cleanup() {
end_section
# ===== 5. Orphaned leftovers =====
- log_header "Checking for orphaned app files"
+ start_section "Orphaned app files"
# Build a list of installed application bundle identifiers
echo -e " ${BLUE}π${NC} Building app list..."
@@ -801,6 +796,7 @@ perform_cleanup() {
if [ "$found_orphaned" = false ]; then
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"
@@ -823,12 +819,13 @@ perform_cleanup() {
# System cleanup was moved to the beginning (right after password verification)
# ===== 7. iOS device backups =====
- log_header "Checking iOS device backups..."
+ start_section "iOS device backups"
backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then
backup_kb=$(du -sk "$backup_dir" 2>/dev/null | awk '{print $1}')
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then # >100MB
backup_human=$(du -shm "$backup_dir" 2>/dev/null | awk '{print $1"M"}')
+ note_activity
echo -e " π Found ${GREEN}${backup_human}${NC}, you can delete it manually"
echo -e " π ${backup_dir}"
else
@@ -837,9 +834,11 @@ perform_cleanup() {
else
echo -e " ${BLUE}β¨${NC} Nothing to tidy"
fi
+ end_section
# ===== 8. Summary =====
- log_header "Cleanup summary"
+ start_section "Cleanup summary"
+ note_activity
space_after=$(df / | tail -1 | awk '{print $4}')
current_space_after=$(get_free_space)
@@ -864,6 +863,7 @@ perform_cleanup() {
fi
echo "==================================================================="
+ end_section
}
main() {
diff --git a/lib/common.sh b/lib/common.sh
old mode 100644
new mode 100755
index b426da0..28c5415
--- a/lib/common.sh
+++ b/lib/common.sh
@@ -2,20 +2,63 @@
# Mole - Common Functions Library
# Shared utilities and functions for all modules
-# Color definitions
-GREEN='\033[0;32m'
-BLUE='\033[0;34m'
-YELLOW='\033[1;33m'
-PURPLE='\033[0;35m'
-RED='\033[0;31m'
-NC='\033[0m'
+set -euo pipefail
-# Logging functions
-log_info() { echo -e "${BLUE}$1${NC}"; }
-log_success() { echo -e "${GREEN}β
$1${NC}"; }
-log_warning() { echo -e "${YELLOW}β οΈ $1${NC}"; }
-log_error() { echo -e "${RED}β $1${NC}"; }
-log_header() { echo -e "\n${PURPLE}βΆ $1${NC}"; }
+# Color definitions (readonly for safety)
+readonly ESC=$'\033'
+readonly GREEN="${ESC}[0;32m"
+readonly BLUE="${ESC}[0;34m"
+readonly YELLOW="${ESC}[1;33m"
+readonly PURPLE="${ESC}[0;35m"
+readonly RED="${ESC}[0;31m"
+readonly NC="${ESC}[0m"
+
+# Logging configuration
+readonly LOG_FILE="${HOME}/.config/mole/mole.log"
+readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB
+
+# Ensure log directory exists
+mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
+
+# Enhanced logging functions with file logging support
+log_info() {
+ rotate_log
+ echo -e "${BLUE}$1${NC}"
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE" 2>/dev/null || true
+}
+
+log_success() {
+ rotate_log
+ echo -e "${GREEN}β
$1${NC}"
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true
+}
+
+log_warning() {
+ rotate_log
+ echo -e "${YELLOW}β οΈ $1${NC}"
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE" 2>/dev/null || true
+}
+
+log_error() {
+ rotate_log
+ echo -e "${RED}β $1${NC}" >&2
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" 2>/dev/null || true
+}
+
+log_header() {
+ rotate_log
+ echo -e "\n${PURPLE}βΆ $1${NC}"
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] SECTION: $1" >> "$LOG_FILE" 2>/dev/null || true
+}
+
+# Log file maintenance
+rotate_log() {
+ local max_size="${MOLE_MAX_LOG_SIZE:-$LOG_MAX_SIZE_DEFAULT}"
+ if [[ -f "$LOG_FILE" ]] && [[ $(stat -f%z "$LOG_FILE" 2>/dev/null || echo 0) -gt "$max_size" ]]; then
+ mv "$LOG_FILE" "${LOG_FILE}.old" 2>/dev/null || true
+ touch "$LOG_FILE" 2>/dev/null || true
+ fi
+}
# System detection
detect_architecture() {
@@ -96,9 +139,69 @@ handle_error() {
# File size utilities
get_human_size() {
local path="$1"
+ if [[ ! -e "$path" ]]; then
+ echo "N/A"
+ return 1
+ fi
du -sh "$path" 2>/dev/null | cut -f1 || echo "N/A"
}
+# Convert bytes to human readable format
+bytes_to_human() {
+ local bytes="$1"
+ if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then
+ echo "0B"
+ return 1
+ fi
+
+ if ((bytes >= 1073741824)); then # >= 1GB
+ echo "$bytes" | awk '{printf "%.2fGB", $1/1073741824}'
+ elif ((bytes >= 1048576)); then # >= 1MB
+ echo "$bytes" | awk '{printf "%.1fMB", $1/1048576}'
+ elif ((bytes >= 1024)); then # >= 1KB
+ echo "$bytes" | awk '{printf "%.0fKB", $1/1024}'
+ else
+ echo "${bytes}B"
+ fi
+}
+
+# Calculate directory size in bytes
+get_directory_size_bytes() {
+ local path="$1"
+ if [[ ! -d "$path" ]]; then
+ echo "0"
+ return 1
+ fi
+ 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
check_sudo() {
if ! sudo -n true 2>/dev/null; then
@@ -119,3 +222,166 @@ request_sudo() {
return 1
fi
}
+
+# Configuration management
+readonly CONFIG_FILE="${HOME}/.config/mole/config"
+
+# Load configuration with defaults
+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_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/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
+load_config
diff --git a/lib/menu.sh b/lib/menu.sh
index 6bee66c..b3d3a9d 100755
--- a/lib/menu.sh
+++ b/lib/menu.sh
@@ -7,8 +7,10 @@ declare -a menu_options=()
declare -i selected=0
declare -i menu_size=0
-# ANSI escape sequences
-readonly ESC=$'\033'
+# 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'
diff --git a/mole b/mole
index 1589836..bf027a2 100755
--- a/mole
+++ b/mole
@@ -21,23 +21,27 @@ source "$SCRIPT_DIR/lib/common.sh"
# Version info
VERSION="1.0.0"
+MOLE_TAGLINE="Dig deep like a mole to clean your Mac."
+
+show_brand_banner() {
+ printf '%b𦑠%bMOLE%b β %b%s%b\n' \
+ "$PURPLE" "$BLUE" "$NC" "$GREEN" "$MOLE_TAGLINE" "$NC"
+}
show_help() {
- cat <<'EOF'
-Mole can dig deep to clean your mac.
-=====================================
+ show_brand_banner
+ echo
+ printf "%s%s%s\n" "$BLUE" "USAGE" "$NC"
+ printf " %s%s%s [command]\n\n" "$GREEN" "mole" "$NC"
-USAGE:
- mole [command]
+ printf "%s%s%s\n" "$BLUE" "COMMANDS" "$NC"
+ 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 uninstall" "$NC" "Remove applications completely"
+ printf " %s%-16s%s %s\n" "$GREEN" "mole --help" "$NC" "Show this help message"
-COMMANDS:
- mole # Interactive main menu
- mole clean # Deeper system cleanup
- mole uninstall # Remove applications completely
- mole --help # Show this help message
-
-For more information, visit: https://github.com/tw93/mole
-EOF
+ printf "\n%s%s%s\n" "$BLUE" "MORE" "$NC"
+ printf " https://github.com/tw93/mole\n"
}
show_main_menu() {
@@ -46,7 +50,7 @@ show_main_menu() {
if [[ "$redraw_full" == "true" ]]; then
echo ""
- echo "Mole can dig deep to clean your mac."
+ show_brand_banner
echo ""
fi