diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be60c0b..c5d20a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,8 +54,8 @@ Config: `.editorconfig` and `.shellcheckrc` # Single file/directory safe_remove "/path/to/file" -# Batch delete with find -safe_find_delete "$dir" "*.log" 7 "f" # files older than 7 days +# Purge files older than 7 days +safe_find_delete "$dir" "*.log" 7 "f" # With sudo safe_sudo_remove "/Library/Caches/com.example" @@ -137,7 +137,7 @@ Format: `[MODULE_NAME] message` output to stderr. - macOS 10.14 or newer, works on Intel and Apple Silicon - Default macOS Bash 3.2+ plus administrator privileges for cleanup tasks - Install Command Line Tools with `xcode-select --install` for curl, tar, and related utilities -- Go 1.24+ required when building the `mo status` or `mo analyze` TUI binaries locally +- Go 1.24+ is required to build the `mo status` or `mo analyze` TUI binaries locally. ## Go Components @@ -161,7 +161,7 @@ Format: `[MODULE_NAME] message` output to stderr. - Keep files focused on single responsibility - Extract constants instead of magic numbers - Use context for timeout control on external commands -- Add comments explaining why, not what +- Add comments explaining **why** something is done, not just **what** is being done. ## Pull Requests diff --git a/README.md b/README.md index 140355f..e586719 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ ## Features -- **All-in-one toolkit** combining CleanMyMac, AppCleaner, DaisyDisk, Sensei, and iStat in one **trusted binary** -- **Deep cleanup** scans and removes caches, logs, browser leftovers, and junk to **reclaim tens of gigabytes** -- **Smart uninstall** completely removes apps including launch agents, preferences, caches, and **hidden leftovers** -- **Disk insight + optimization** visualizes usage, handles large files, **rebuilds caches**, and refreshes services -- **Live status** monitors CPU, GPU, memory, disk, network, battery, and proxy stats to **diagnose issues** +- **Unified toolkit**: Consolidated features of CleanMyMac, AppCleaner, DaisyDisk, and iStat into a **single binary** +- **Deep cleaning**: Scans and removes caches, logs, and browser leftovers to **reclaim gigabytes of space** +- **Smart uninstaller**: Thoroughly removes apps along with launch agents, preferences, and **hidden remnants** +- **Disk insights**: Visualizes usage, manages large files, **rebuilds caches**, and refreshes system services +- **Live monitoring**: Real-time stats for CPU, GPU, memory, disk, and network to **diagnose performance issues** ## Quick Start @@ -55,10 +55,10 @@ mo remove # Remove Mole from system mo --help # Show help mo --version # Show installed version -mo clean --dry-run # Preview cleanup plan -mo clean --whitelist # Adjust protected caches -mo uninstall --force-rescan # Rescan apps and refresh cache -mo optimize --whitelist # Adjust protected optimization items +mo clean --dry-run # Preview the cleanup plan +mo clean --whitelist # Manage protected caches +mo uninstall --force-rescan # Rescan applications and refresh cache +mo optimize --whitelist # Manage protected optimization rules ``` ## Tips @@ -210,7 +210,7 @@ Launch Mole commands instantly from Raycast or Alfred: curl -fsSL https://raw.githubusercontent.com/tw93/Mole/main/scripts/setup-quick-launchers.sh | bash ``` -Adds 5 commands: `clean`, `uninstall`, `optimize`, `analyze`, `status`. Finds your terminal automatically or set `MO_LAUNCHER_APP=` to override. For Raycast, search "Reload Script Directories" to load new commands. +Adds 5 commands: `clean`, `uninstall`, `optimize`, `analyze`, `status`. Mole automatically detects your terminal, or you can set `MO_LAUNCHER_APP=` to override. For Raycast, run "Reload Script Directories" to load the new commands. ## Support diff --git a/lib/clean/app_caches.sh b/lib/clean/app_caches.sh index 4f6ed2b..85db1ae 100644 --- a/lib/clean/app_caches.sh +++ b/lib/clean/app_caches.sh @@ -5,9 +5,6 @@ set -euo pipefail # Clean Xcode and iOS development tools -# Archives can be significant in size (app packaging files) -# DeviceSupport files for old iOS versions can accumulate -# Note: Skips critical files if Xcode is running clean_xcode_tools() { # Check if Xcode is running for safer cleanup of critical resources local xcode_running=false @@ -107,9 +104,7 @@ clean_productivity_apps() { safe_clean ~/Library/Caches/com.flomoapp.mac/* "Flomo cache" } -# Clean music and media players -# Note: Spotify cache is protected by default (may contain offline music) -# Users can override via whitelist settings +# Clean music and media players (protects Spotify offline music) clean_media_players() { # Spotify cache protection: check for offline music indicators local spotify_cache="$HOME/Library/Caches/com.spotify.client" diff --git a/lib/clean/system.sh b/lib/clean/system.sh index e197222..4988510 100644 --- a/lib/clean/system.sh +++ b/lib/clean/system.sh @@ -11,7 +11,7 @@ clean_deep_system() { safe_sudo_find_delete "/Library/Caches" "*.tmp" "$MOLE_TEMP_FILE_AGE_DAYS" "f" || true safe_sudo_find_delete "/Library/Caches" "*.log" "$MOLE_LOG_AGE_DAYS" "f" || true - # Clean temp files - use real paths (macOS /tmp is symlink to /private/tmp) + # Clean temporary files (macOS /tmp is a symlink to /private/tmp) local tmp_cleaned=0 safe_sudo_find_delete "/private/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true safe_sudo_find_delete "/private/var/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true @@ -21,13 +21,12 @@ clean_deep_system() { safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*" "$MOLE_CRASH_REPORT_AGE_DAYS" "f" || true log_success "System crash reports" - # Clean system logs - use real path (macOS /var is symlink to /private/var) + # Clean system logs (macOS /var is a symlink to /private/var) safe_sudo_find_delete "/private/var/log" "*.log" "$MOLE_LOG_AGE_DAYS" "f" || true safe_sudo_find_delete "/private/var/log" "*.gz" "$MOLE_LOG_AGE_DAYS" "f" || true log_success "System logs" - # Clean Library Updates safely - skip if SIP is enabled to avoid error messages - # SIP-protected files in /Library/Updates cannot be deleted even with sudo + # Clean Library Updates safely (skip if SIP is enabled) if [[ -d "/Library/Updates" && ! -L "/Library/Updates" ]]; then if ! is_sip_enabled; then # SIP is disabled, attempt cleanup with restricted flag check @@ -54,8 +53,7 @@ clean_deep_system() { fi fi - # Clean macOS Install Data (system upgrade leftovers) - # Only remove if older than 30 days to ensure system stability + # Clean macOS Install Data (legacy upgrade leftovers) if [[ -d "/macOS Install Data" ]]; then local mtime=$(get_file_mtime "/macOS Install Data") local age_days=$((($(date +%s) - mtime) / 86400)) @@ -78,22 +76,20 @@ clean_deep_system() { fi # Clean browser code signature caches - # These are regenerated automatically when needed if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning system caches..." fi local code_sign_cleaned=0 local found_count=0 - # Stream processing with progress updates (efficient for large directories) - # Reduce timeout to 5s for faster completion when no caches exist + # Efficient stream processing for large directories while IFS= read -r -d '' cache_dir; do if safe_remove "$cache_dir" true; then ((code_sign_cleaned++)) fi ((found_count++)) - # Update spinner every 50 items to show progress + # Update progress spinner periodically if [[ -t 1 ]] && ((found_count % 50 == 0)); then stop_inline_spinner MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning system caches... ($found_count found)" @@ -115,7 +111,7 @@ clean_deep_system() { log_success "Power logs" } -# Clean Time Machine incomplete backups +# Clean incomplete Time Machine backups clean_time_machine_failed_backups() { local tm_cleaned=0 @@ -324,7 +320,7 @@ clean_time_machine_failed_backups() { fi } -# Clean local APFS snapshots (older than 24h) +# Clean local APFS snapshots (older than 24 hours) clean_local_snapshots() { # Check if tmutil is available if ! command -v tmutil > /dev/null 2>&1; then diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index a9766ab..5541192 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -13,10 +13,10 @@ _MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" [[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh" # ============================================================================ -# App Management Functions +# Application Management # ============================================================================ -# System critical components that should NEVER be uninstalled +# Critical system components protected from uninstallation readonly SYSTEM_CRITICAL_BUNDLES=( "com.apple.*" # System essentials "loginwindow" @@ -68,7 +68,7 @@ readonly SYSTEM_CRITICAL_BUNDLES=( "com.apple.TextInputSwitcher" ) -# Apps with important data/licenses - protect during cleanup but allow uninstall +# Applications with sensitive data; protected during cleanup but removable readonly DATA_PROTECTED_BUNDLES=( # ============================================================================ # System Utilities & Cleanup Tools @@ -165,7 +165,7 @@ readonly DATA_PROTECTED_BUNDLES=( "com.usebruno.app" # Bruno (API client) # ============================================================================ - # Network Proxy & VPN Tools (Broad Glob Protection) + # Network Proxy & VPN Tools (pattern-based protection) # ============================================================================ # Clash variants "*clash*" # All Clash variants (ClashX, ClashX Pro, Clash Verge, etc) @@ -439,7 +439,7 @@ readonly DATA_PROTECTED_BUNDLES=( # Use should_protect_from_uninstall() or should_protect_data() instead readonly PRESERVED_BUNDLE_PATTERNS=("${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}") -# Check whether a bundle ID matches a pattern (supports globs) +# Check if bundle ID matches pattern (glob support) bundle_matches_pattern() { local bundle_id="$1" local pattern="$2" @@ -454,7 +454,7 @@ bundle_matches_pattern() { return 1 } -# Check if app is a system component that should never be uninstalled +# Check if application is a protected system component should_protect_from_uninstall() { local bundle_id="$1" for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do @@ -465,7 +465,7 @@ should_protect_from_uninstall() { return 1 } -# Check if app data should be protected during cleanup (but app can be uninstalled) +# Check if application data should be protected during cleanup should_protect_data() { local bundle_id="$1" # Protect both system critical and data protected bundles during cleanup @@ -477,7 +477,7 @@ should_protect_data() { return 1 } -# Check if a specific path should be protected from deletion +# Check if a path is protected from deletion # Centralized logic to protect system settings, control center, and critical apps # # Args: $1 - path to check @@ -489,7 +489,7 @@ should_protect_path() { local path_lower path_lower=$(echo "$path" | tr '[:upper:]' '[:lower:]') - # 1. Check for explicit critical system keywords in path (case-insensitive) + # 1. Keyword-based matching for system components # Protect System Settings, Preferences, Control Center, and related XPC services # Also protect "Settings" (used in macOS Sequoia) and savedState files if [[ "$path_lower" =~ systemsettings || "$path_lower" =~ systempreferences || "$path_lower" =~ controlcenter ]]; then @@ -501,7 +501,7 @@ should_protect_path() { return 0 fi - # 2. Protect system-critical cache directories that cause UI corruption + # 2. Protect caches critical for system UI rendering # These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering case "$path" in # System Settings and Control Center caches (CRITICAL - prevents blank panel bug) @@ -525,7 +525,7 @@ should_protect_path() { ;; esac - # 3. Extract bundle ID from app container/group container paths + # 3. Extract bundle ID from sandbox paths # Matches: .../Library/Containers/bundle.id/... # Matches: .../Library/Group Containers/group.id/... if [[ "$path" =~ /Library/Containers/([^/]+) ]] || [[ "$path" =~ /Library/Group\ Containers/([^/]+) ]]; then @@ -553,7 +553,7 @@ should_protect_path() { ;; esac - # 6. Check the full path against protected patterns (Broad Glob Match) + # 6. Match full path against protected patterns # This catches things like /Users/tw93/Library/Caches/Claude when pattern is *Claude* for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do if bundle_matches_pattern "$path" "$pattern"; then @@ -571,13 +571,13 @@ should_protect_path() { return 1 } -# Find and list app-related files (consolidated from duplicates) +# Locate files associated with an application find_app_files() { local bundle_id="$1" local app_name="$2" local -a files_to_clean=() - # Sanitized App Name (remove spaces) + # Normalize app name for matching local nospace_name="${app_name// /}" local underscore_name="${app_name// /_}" @@ -635,7 +635,7 @@ find_app_files() { [[ -e "$expanded_path" ]] && files_to_clean+=("$expanded_path") done - # Preferences and ByHost (special handling) + # Handle Preferences and ByHost variants [[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist") [[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do files_to_clean+=("$pref") @@ -655,7 +655,7 @@ find_app_files() { done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null) fi - # Specialized toolchain cleanup (non-loopable or highly specific) + # Handle specialized toolchains and development environments # 1. DevEco-Studio (Huawei) if [[ "$app_name" =~ DevEco|deveco ]] || [[ "$bundle_id" =~ huawei.*deveco ]]; then for d in ~/DevEcoStudioProjects ~/DevEco-Studio ~/Library/Application\ Support/Huawei ~/Library/Caches/Huawei ~/Library/Logs/Huawei ~/Library/Huawei ~/Huawei ~/HarmonyOS ~/.huawei ~/.ohos; do @@ -697,7 +697,7 @@ find_app_files() { [[ ${#files_to_clean[@]} -gt 0 ]] && printf '%s\n' "${files_to_clean[@]}" } -# Find system-level app files (requires sudo) +# Locate system-level application files find_app_system_files() { local bundle_id="$1" local app_name="$2" @@ -765,7 +765,7 @@ find_app_system_files() { find_app_receipt_files "$bundle_id" } -# Find files from installation receipts (Bom files) +# Locate files using installation receipts (BOM) find_app_receipt_files() { local bundle_id="$1" @@ -798,13 +798,13 @@ find_app_receipt_files() { # Standardize path (remove leading dot) local clean_path="${file_path#.}" - # Ensure it starts with / + # Ensure absolute path if [[ "$clean_path" != /* ]]; then clean_path="/$clean_path" fi # ------------------------------------------------------------------------ - # SAFETY FILTER: Only allow specific removal paths + # Safety check: restrict removal to trusted paths # ------------------------------------------------------------------------ local is_safe=false @@ -839,15 +839,7 @@ find_app_receipt_files() { esac if [[ "$is_safe" == "true" && -e "$clean_path" ]]; then - # Only valid files - # Don't delete directories if they are non-empty parents? - # lsbom lists directories too. - # If we return a directory, `safe_remove` logic handles it. - # `uninstall.sh` uses `remove_file_list`. - # If `lsbom` lists `/Applications` (it shouldn't, only contents), we must be careful. - # `lsbom` usually lists `./Applications/MyApp.app`. - # If it lists `./Applications`, we must skip it. - + # If lsbom lists /Applications, skip to avoid system damage. # Extra check: path must be deep enough? # If path is just "/Applications", skip. if [[ "$clean_path" == "/Applications" || "$clean_path" == "/Library" || "$clean_path" == "/usr/local" ]]; then @@ -865,9 +857,9 @@ find_app_receipt_files() { fi } -# Force quit an application +# Terminate a running application force_kill_app() { - # Args: app_name [app_path]; tries graceful then force kill; returns 0 if stopped, 1 otherwise + # Gracefully terminates or force-kills an application local app_name="$1" local app_path="${2:-""}" diff --git a/lib/core/base.sh b/lib/core/base.sh index 5ad3edf..2337372 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -44,15 +44,15 @@ readonly ICON_NAV_RIGHT="→" # ============================================================================ # Global Configuration Constants # ============================================================================ -readonly MOLE_TEMP_FILE_AGE_DAYS=7 # Temp file cleanup threshold -readonly MOLE_ORPHAN_AGE_DAYS=60 # Orphaned data threshold +readonly MOLE_TEMP_FILE_AGE_DAYS=7 # Temp file retention (days) +readonly MOLE_ORPHAN_AGE_DAYS=60 # Orphaned data retention (days) readonly MOLE_MAX_PARALLEL_JOBS=15 # Parallel job limit -readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachments size threshold -readonly MOLE_MAIL_AGE_DAYS=30 # Mail attachment cleanup threshold (30+ days) -readonly MOLE_LOG_AGE_DAYS=7 # System log retention -readonly MOLE_CRASH_REPORT_AGE_DAYS=7 # Crash report retention -readonly MOLE_SAVED_STATE_AGE_DAYS=7 # App saved state retention -readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # Time Machine failed backup safety window +readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachment size threshold +readonly MOLE_MAIL_AGE_DAYS=30 # Mail attachment retention (days) +readonly MOLE_LOG_AGE_DAYS=7 # Log retention (days) +readonly MOLE_CRASH_REPORT_AGE_DAYS=7 # Crash report retention (days) +readonly MOLE_SAVED_STATE_AGE_DAYS=7 # Saved state retention (days) +readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # TM backup safety window (hours) # ============================================================================ # Seasonal Functions @@ -99,7 +99,7 @@ declare -a DEFAULT_OPTIMIZE_WHITELIST_PATTERNS=( # ============================================================================ readonly STAT_BSD="/usr/bin/stat" -# Get file size in bytes using BSD stat +# Get file size in bytes get_file_size() { local file="$1" local result @@ -107,8 +107,7 @@ get_file_size() { echo "${result:-0}" } -# Get file modification time using BSD stat -# Returns: epoch seconds +# Get file modification time in epoch seconds get_file_mtime() { local file="$1" [[ -z "$file" ]] && { @@ -120,7 +119,7 @@ get_file_mtime() { echo "${result:-0}" } -# Get file owner username using BSD stat +# Get file owner username get_file_owner() { local file="$1" $STAT_BSD -f%Su "$file" 2> /dev/null || echo "" @@ -147,8 +146,7 @@ is_sip_enabled() { fi } -# Check if running in interactive terminal -# Returns: 0 if interactive, 1 otherwise +# Check if running in an interactive terminal is_interactive() { [[ -t 1 ]] } @@ -169,9 +167,7 @@ get_free_space() { command df -h / | awk 'NR==2 {print $4}' } -# Get optimal number of parallel jobs for a given operation type -# Args: $1 - operation type (scan|io|compute|default) -# Returns: number of jobs +# Get optimal parallel jobs for operation type (scan|io|compute|default) get_optimal_parallel_jobs() { local operation_type="${1:-default}" local cpu_cores @@ -193,9 +189,7 @@ get_optimal_parallel_jobs() { # Formatting Utilities # ============================================================================ -# Convert bytes to human-readable format -# Args: $1 - size in bytes -# Returns: formatted string (e.g., "1.50GB", "256MB", "4KB") +# Convert bytes to human-readable format (e.g., 1.5GB) bytes_to_human() { local bytes="$1" if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then @@ -245,9 +239,7 @@ bytes_to_human_kb() { bytes_to_human "$((${1:-0} * 1024))" } -# Get brand-friendly name for an application -# Args: $1 - application name -# Returns: localized name based on system language preference +# Get brand-friendly localized name for an application get_brand_name() { local name="$1" @@ -305,7 +297,6 @@ declare -a MOLE_TEMP_FILES=() declare -a MOLE_TEMP_DIRS=() # Create tracked temporary file -# Returns: temp file path create_temp_file() { local temp temp=$(mktemp) || return 1 @@ -314,7 +305,6 @@ create_temp_file() { } # Create tracked temporary directory -# Returns: temp directory path create_temp_dir() { local temp temp=$(mktemp -d) || return 1 diff --git a/lib/core/common.sh b/lib/core/common.sh index b7bb7bc..7a32a6a 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -12,7 +12,7 @@ readonly MOLE_COMMON_LOADED=1 _MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Load core modules in dependency order +# Load core modules source "$_MOLE_CORE_DIR/base.sh" source "$_MOLE_CORE_DIR/log.sh" @@ -26,8 +26,7 @@ if [[ -f "$_MOLE_CORE_DIR/sudo.sh" ]]; then source "$_MOLE_CORE_DIR/sudo.sh" fi -# Update Mole via Homebrew -# Args: $1 = current version +# Update via Homebrew update_via_homebrew() { local current_version="$1" @@ -75,8 +74,7 @@ update_via_homebrew() { rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message" 2> /dev/null || true } -# Remove apps from Dock -# Args: app paths to remove +# Remove applications from Dock remove_apps_from_dock() { if [[ $# -eq 0 ]]; then return 0 @@ -89,7 +87,7 @@ remove_apps_from_dock() { return 0 fi - # Execute Python helper to prune dock entries for the given app paths + # Prune dock entries using Python helper python3 - "$@" << 'PY' 2> /dev/null || return 0 import os import plistlib diff --git a/lib/core/file_ops.sh b/lib/core/file_ops.sh index 4446d7b..23519a9 100644 --- a/lib/core/file_ops.sh +++ b/lib/core/file_ops.sh @@ -29,11 +29,7 @@ fi # Path Validation # ============================================================================ -# Validate path for deletion operations -# Checks: non-empty, absolute, no traversal, no control chars, not system dir -# -# Args: $1 - path to validate -# Returns: 0 if safe, 1 if unsafe +# Validate path for deletion (absolute, no traversal, not system dir) validate_path_for_deletion() { local path="$1" @@ -76,13 +72,7 @@ validate_path_for_deletion() { # Safe Removal Operations # ============================================================================ -# Safe wrapper around rm -rf with path validation -# -# Args: -# $1 - path to remove -# $2 - silent mode (optional, default: false) -# -# Returns: 0 on success, 1 on failure +# Safe wrapper around rm -rf with validation safe_remove() { local path="$1" local silent="${2:-false}" @@ -108,10 +98,7 @@ safe_remove() { fi } -# Safe sudo remove with additional symlink protection -# -# Args: $1 - path to remove -# Returns: 0 on success, 1 on failure +# Safe sudo removal with symlink protection safe_sudo_remove() { local path="$1" @@ -147,15 +134,7 @@ safe_sudo_remove() { # Safe Find and Delete Operations # ============================================================================ -# Safe find delete with depth limit and validation -# -# Args: -# $1 - base directory -# $2 - file pattern (e.g., "*.log") -# $3 - age in days (0 = all files, default: 7) -# $4 - type filter ("f" or "d", default: "f") -# -# Returns: 0 on success, 1 on failure +# Safe file discovery and deletion with depth and age limits safe_find_delete() { local base_dir="$1" local pattern="$2" @@ -202,10 +181,7 @@ safe_find_delete() { return 0 } -# Safe sudo find delete (same as safe_find_delete but with sudo) -# -# Args: same as safe_find_delete -# Returns: 0 on success, 1 on failure +# Safe sudo discovery and deletion safe_sudo_find_delete() { local base_dir="$1" local pattern="$2" @@ -254,11 +230,7 @@ safe_sudo_find_delete() { # Size Calculation # ============================================================================ -# Get path size in kilobytes -# Uses timeout protection to prevent du from hanging on large directories -# -# Args: $1 - path -# Returns: size in KB (0 if path doesn't exist) +# Get path size in KB (returns 0 if not found) get_path_size_kb() { local path="$1" [[ -z "$path" || ! -e "$path" ]] && { @@ -271,10 +243,7 @@ get_path_size_kb() { echo "${size:-0}" } -# Calculate total size of multiple paths -# -# Args: $1 - newline-separated list of paths -# Returns: total size in KB +# Calculate total size for multiple paths calculate_total_size() { local files="$1" local total_kb=0 diff --git a/lib/core/log.sh b/lib/core/log.sh index 6c08276..968abf4 100644 --- a/lib/core/log.sh +++ b/lib/core/log.sh @@ -32,8 +32,7 @@ mkdir -p "$(dirname "$LOG_FILE")" 2> /dev/null || true # Log Rotation # ============================================================================ -# Rotate log file if it exceeds max size -# Called once at module load, not per log entry +# Rotate log file if it exceeds maximum size rotate_log_once() { # Skip if already checked this session [[ -n "${MOLE_LOG_ROTATED:-}" ]] && return 0 @@ -51,7 +50,6 @@ rotate_log_once() { # ============================================================================ # Log informational message -# Args: $1 - message log_info() { echo -e "${BLUE}$1${NC}" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') @@ -62,7 +60,6 @@ log_info() { } # Log success message -# Args: $1 - message log_success() { echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') @@ -73,7 +70,6 @@ log_success() { } # Log warning message -# Args: $1 - message log_warning() { echo -e "${YELLOW}$1${NC}" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') @@ -84,7 +80,6 @@ log_warning() { } # Log error message -# Args: $1 - message log_error() { echo -e "${RED}${ICON_ERROR}${NC} $1" >&2 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') @@ -94,8 +89,7 @@ log_error() { fi } -# Debug logging - only shown when MO_DEBUG=1 -# Args: $@ - debug message components +# Debug logging (active when MO_DEBUG=1) debug_log() { if [[ "${MO_DEBUG:-}" == "1" ]]; then echo -e "${GRAY}[DEBUG]${NC} $*" >&2 @@ -143,15 +137,12 @@ log_system_info() { # Command Execution Wrappers # ============================================================================ -# Run command silently, ignore errors -# Args: $@ - command and arguments +# Run command silently (ignore errors) run_silent() { "$@" > /dev/null 2>&1 || true } # Run command with error logging -# Args: $@ - command and arguments -# Returns: command exit code run_logged() { local cmd="$1" # Log to main file, and also to debug file if enabled @@ -173,8 +164,7 @@ run_logged() { # Formatted Output # ============================================================================ -# Print formatted summary block with heading and details -# Args: $1=status (ignored), $2=heading, $@=details +# Print formatted summary block print_summary_block() { local heading="" local -a details=() diff --git a/lib/core/sudo.sh b/lib/core/sudo.sh index d44b719..11c6857 100644 --- a/lib/core/sudo.sh +++ b/lib/core/sudo.sh @@ -16,6 +16,7 @@ check_touchid_support() { return 1 } +# Detect clamshell mode (lid closed) is_clamshell_mode() { # ioreg is missing (not macOS) -> treat as lid open if ! command -v ioreg > /dev/null 2>&1; then @@ -182,8 +183,7 @@ request_sudo_access() { MOLE_SUDO_KEEPALIVE_PID="" MOLE_SUDO_ESTABLISHED="false" -# Start sudo keepalive background process -# Returns: PID of keepalive process +# Start sudo keepalive _start_sudo_keepalive() { # Start background keepalive process with all outputs redirected # This is critical: command substitution waits for all file descriptors to close @@ -212,8 +212,7 @@ _start_sudo_keepalive() { echo $pid } -# Stop sudo keepalive process -# Args: $1 - PID of keepalive process +# Stop sudo keepalive _stop_sudo_keepalive() { local pid="${1:-}" if [[ -n "$pid" ]]; then @@ -227,8 +226,7 @@ has_sudo_session() { sudo -n true 2> /dev/null } -# Request sudo access (wrapper for common.sh function) -# Args: $1 - prompt message +# Request administrative access request_sudo() { local prompt_msg="${1:-Admin access required}" @@ -244,8 +242,7 @@ request_sudo() { fi } -# Ensure sudo session is established with keepalive -# Args: $1 - prompt message +# Maintain active sudo session with keepalive ensure_sudo_session() { local prompt="${1:-Admin access required}" @@ -287,8 +284,7 @@ register_sudo_cleanup() { trap stop_sudo_session EXIT INT TERM } -# Check if sudo is likely needed for given operations -# Args: $@ - list of operations to check +# Predict if operation requires administrative access will_need_sudo() { local -a operations=("$@") for op in "${operations[@]}"; do diff --git a/lib/core/ui.sh b/lib/core/ui.sh index 66ecd68..d2018e2 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -17,10 +17,7 @@ clear_screen() { printf '\033[2J\033[H'; } hide_cursor() { [[ -t 1 ]] && printf '\033[?25l' >&2 || true; } show_cursor() { [[ -t 1 ]] && printf '\033[?25h' >&2 || true; } -# Calculate display width of a string (CJK characters count as 2) -# Args: $1 - string to measure -# Returns: display width -# Note: Works correctly even when LC_ALL=C is set +# Calculate display width (CJK characters count as 2) get_display_width() { local str="$1" @@ -66,8 +63,7 @@ get_display_width() { echo "$width" } -# Truncate string by display width (handles CJK correctly) -# Args: $1 - string, $2 - max display width +# Truncate string by display width (handles CJK) truncate_by_display_width() { local str="$1" local max_width="$2" @@ -140,7 +136,7 @@ truncate_by_display_width() { echo "${truncated}..." } -# Keyboard input - read single keypress +# Read single keyboard input read_key() { local key rest read_status IFS= read -r -s -n 1 key @@ -222,7 +218,7 @@ drain_pending_input() { done } -# Menu display +# Format menu option display show_menu_option() { local number="$1" local text="$2" @@ -235,7 +231,7 @@ show_menu_option() { fi } -# Inline spinner +# Background spinner implementation INLINE_SPINNER_PID="" start_inline_spinner() { stop_inline_spinner 2> /dev/null || true @@ -281,7 +277,7 @@ stop_inline_spinner() { fi } -# Wrapper for running commands with spinner +# Run command with a terminal spinner with_spinner() { local msg="$1" shift || true @@ -302,9 +298,7 @@ mo_spinner_chars() { printf "%s" "$chars" } -# Format last used time for display -# Args: $1 = last used string (e.g., "3 days ago", "Today", "Never") -# Returns: Compact version (e.g., "3d ago", "Today", "Never") +# Format relative time for compact display (e.g., 3d ago) format_last_used_summary() { local value="$1" diff --git a/lib/optimize/maintenance.sh b/lib/optimize/maintenance.sh index 2baaee6..1a521c0 100644 --- a/lib/optimize/maintenance.sh +++ b/lib/optimize/maintenance.sh @@ -9,9 +9,7 @@ set -euo pipefail # Find and remove corrupted .plist files # ============================================================================ -# Clean broken preference files -# Uses plutil -lint to validate plist files -# Returns: count of broken files fixed +# Clean corrupted preference files fix_broken_preferences() { local prefs_dir="$HOME/Library/Preferences" [[ -d "$prefs_dir" ]] || return 0 @@ -68,8 +66,7 @@ fix_broken_preferences() { # Find and remove login items pointing to non-existent files # ============================================================================ -# Clean broken login items (LaunchAgents pointing to missing executables) -# Returns: count of broken items fixed +# Clean login items with missing executables fix_broken_login_items() { local launch_agents_dir="$HOME/Library/LaunchAgents" [[ -d "$launch_agents_dir" ]] || return 0 diff --git a/lib/optimize/tasks.sh b/lib/optimize/tasks.sh index 7314663..8054a32 100644 --- a/lib/optimize/tasks.sh +++ b/lib/optimize/tasks.sh @@ -15,7 +15,7 @@ flush_dns_cache() { sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null } -# System maintenance: rebuild databases and flush caches +# Rebuild databases and flush caches opt_system_maintenance() { echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..." run_with_timeout 10 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user > /dev/null 2>&1 || true @@ -39,7 +39,7 @@ opt_system_maintenance() { } -# Cache refresh: update Finder/Safari caches +# Reset Finder and Safari caches opt_cache_refresh() { echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Quick Look cache..." qlmanage -r cache > /dev/null 2>&1 || true @@ -61,7 +61,7 @@ opt_cache_refresh() { echo -e "${GREEN}${ICON_SUCCESS}${NC} Finder and Safari caches updated" } -# Maintenance scripts: run periodic tasks +# Run periodic maintenance scripts opt_maintenance_scripts() { # Run newsyslog to rotate system logs echo -e "${BLUE}${ICON_ARROW}${NC} Rotating system logs..." @@ -72,7 +72,7 @@ opt_maintenance_scripts() { fi } -# Log cleanup: remove diagnostic and crash logs +# Remove diagnostic and crash logs opt_log_cleanup() { echo -e "${BLUE}${ICON_ARROW}${NC} Clearing diagnostic & crash logs..." local -a user_logs=( @@ -92,7 +92,7 @@ opt_log_cleanup() { fi } -# Recent items: clear recent file lists +# Clear recent file lists opt_recent_items() { echo -e "${BLUE}${ICON_ARROW}${NC} Clearing recent items lists..." local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist" diff --git a/mole b/mole index 920ba73..f365440 100755 --- a/mole +++ b/mole @@ -25,7 +25,7 @@ source "$SCRIPT_DIR/lib/core/common.sh" VERSION="1.13.10" MOLE_TAGLINE="Deep clean and optimize your Mac." -# Check if Touch ID is already configured +# Check TouchID configuration is_touchid_configured() { local pam_sudo_file="/etc/pam.d/sudo" [[ -f "$pam_sudo_file" ]] && grep -q "pam_tid.so" "$pam_sudo_file" 2> /dev/null @@ -38,8 +38,7 @@ get_latest_version() { grep '^VERSION=' | head -1 | sed 's/VERSION="\(.*\)"/\1/' } -# Get latest version from GitHub API -# This works for both Homebrew and manual installs since versions are synced +# Get latest version from GitHub API (works for both Homebrew and manual installations) get_latest_version_from_github() { local version version=$(curl -fsSL --connect-timeout 2 --max-time 3 \ @@ -61,7 +60,7 @@ check_for_updates() { local msg_cache="$HOME/.cache/mole/update_message" mkdir -p "$(dirname "$msg_cache")" 2> /dev/null - # Background version check (save to file, don't output) + # Background version check # Always check in background, display result from previous check ( local latest @@ -104,7 +103,7 @@ EOF } animate_mole_intro() { - # Skip animation if stdout isn't a TTY (non-interactive) + # Non-interactive: skip animation if [[ ! -t 1 ]]; then return fi @@ -481,7 +480,7 @@ remove_mole() { exit 0 fi - # Show what will be removed + # List items for removal echo -e "${YELLOW}Remove Mole${NC} - will delete the following:" if [[ "$is_homebrew" == "true" ]]; then echo " - Mole via Homebrew"