#!/bin/bash # Mole - Main Entry Point # A comprehensive macOS maintenance tool # # Clean - Remove junk files and optimize system # Uninstall - Remove applications completely # Analyze - Interactive disk space explorer # # Usage: # ./mole # Interactive main menu # ./mole clean # Direct clean mode # ./mole uninstall # Direct uninstall mode # ./mole analyze # Disk space explorer # ./mole --help # Show help set -euo pipefail # Get script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source common functions source "$SCRIPT_DIR/lib/common.sh" # Version info VERSION="1.8.3" MOLE_TAGLINE="can dig deep to clean your Mac." # Get latest version from remote repository get_latest_version() { curl -fsSL --connect-timeout 2 --max-time 3 -H "Cache-Control: no-cache" \ "https://raw.githubusercontent.com/tw93/mole/main/mole" 2> /dev/null | grep '^VERSION=' | head -1 | sed 's/VERSION="\(.*\)"/\1/' } # Check for updates (non-blocking, cached) check_for_updates() { local cache="$HOME/.cache/mole/version_check" local msg_cache="$HOME/.cache/mole/update_message" local ttl="${MO_UPDATE_CHECK_TTL:-3600}" mkdir -p "$(dirname "$cache")" 2> /dev/null # Skip if checked recently if [[ -f "$cache" ]]; then local age=$(($(date +%s) - $(stat -f%m "$cache" 2> /dev/null || echo 0))) [[ $age -lt $ttl ]] && return fi # Background version check (save to file, don't output) ( local latest latest=$(get_latest_version) if [[ -n "$latest" && "$VERSION" != "$latest" && "$(printf '%s\n' "$VERSION" "$latest" | sort -V | head -1)" == "$VERSION" ]]; then printf "\nUpdate available: %s → %s, run %smo update%s\n\n" "$VERSION" "$latest" "$GREEN" "$NC" > "$msg_cache" else echo -n > "$msg_cache" fi touch "$cache" 2> /dev/null ) & disown 2> /dev/null || true } # Show update notification if available show_update_notification() { local msg_cache="$HOME/.cache/mole/update_message" if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then cat "$msg_cache" echo fi } show_brand_banner() { cat << EOF ${GREEN} __ __ _ ${NC} ${GREEN}| \/ | ___ | | ___ ${NC} ${GREEN}| |\/| |/ _ \| |/ _ \\${NC} ${GREEN}| | | | (_) | | __/${NC} ${BLUE}https://github.com/tw93/mole${NC} ${GREEN}|_| |_|\___/|_|\___|${NC} ${GREEN}${MOLE_TAGLINE}${NC} EOF } animate_mole_intro() { # Skip animation if stdout isn't a TTY (non-interactive) if [[ ! -t 1 ]]; then return fi clear_screen printf '\n' hide_cursor local -a mole_lines=() while IFS= read -r line; do mole_lines+=("$line") done << 'EOF' /\_/\ ____/ o o \ /~____ =o= / (______)__m_m) / \ __/ /\ \__ /__/ \__\_ EOF local idx local body_cutoff=4 local body_color="${PURPLE}" local ground_color="${GREEN}" for idx in "${!mole_lines[@]}"; do if ((idx < body_cutoff)); then printf "%s\n" "${body_color}${mole_lines[$idx]}${NC}" else printf "%s\n" "${ground_color}${mole_lines[$idx]}${NC}" fi sleep 0.1 done printf '\n' sleep 0.5 printf '\033[2J\033[H' show_cursor } show_version() { printf '\nMole version %s\n\n' "$VERSION" } show_help() { show_brand_banner echo printf "%s%s%s\n" "$BLUE" "COMMANDS" "$NC" printf " %s%-28s%s %s\n" "$GREEN" "mo" "$NC" "Interactive main menu" printf " %s%-28s%s %s\n" "$GREEN" "mo clean" "$NC" "Deeper system cleanup" printf " %s%-28s%s %s\n" "$GREEN" "mo clean --dry-run" "$NC" "Preview cleanup (no deletions)" printf " %s%-28s%s %s\n" "$GREEN" "mo clean --whitelist" "$NC" "Manage protected caches" printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall" "$NC" "Remove applications completely" printf " %s%-28s%s %s\n" "$GREEN" "mo analyze" "$NC" "Interactive disk space explorer" printf " %s%-28s%s %s\n" "$GREEN" "mo touchid" "$NC" "Configure Touch ID for sudo" printf " %s%-28s%s %s\n" "$GREEN" "mo update" "$NC" "Update Mole to the latest version" printf " %s%-28s%s %s\n" "$GREEN" "mo remove" "$NC" "Remove Mole from the system" printf " %s%-28s%s %s\n" "$GREEN" "mo --version" "$NC" "Show installed version" printf " %s%-28s%s %s\n" "$GREEN" "mo --help" "$NC" "Show this help message" printf "\n%s%s%s\n" "$BLUE" "MORE" "$NC" printf " https://github.com/tw93/mole\n\n" } # Simple update function update_mole() { # Check if installed via Homebrew if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then update_via_homebrew "$VERSION" exit 0 fi # Check for updates local latest latest=$(get_latest_version) if [[ -z "$latest" ]]; then log_error "Unable to check for updates. Check network connection." exit 1 fi if [[ "$VERSION" == "$latest" ]]; then echo "" echo -e "${GREEN}${ICON_SUCCESS}${NC} Already on latest version (${VERSION})" echo "" exit 0 fi # Download and run installer with progress if [[ -t 1 ]]; then start_inline_spinner "Downloading latest version..." else echo "Downloading latest version..." fi local installer_url="https://raw.githubusercontent.com/tw93/mole/main/install.sh" local tmp_installer tmp_installer="$(mktemp_file)" || { log_error "Update failed" exit 1 } # Download installer with progress if command -v curl > /dev/null 2>&1; then if ! curl -fsSL --connect-timeout 10 --max-time 60 "$installer_url" -o "$tmp_installer" 2>&1; then if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" log_error "Update failed. Check network connection." exit 1 fi elif command -v wget > /dev/null 2>&1; then if ! wget --timeout=10 --tries=3 -qO "$tmp_installer" "$installer_url" 2>&1; then if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" log_error "Update failed. Check network connection." exit 1 fi else if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" log_error "curl or wget required" exit 1 fi if [[ -t 1 ]]; then stop_inline_spinner; 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)" if [[ -t 1 ]]; then start_inline_spinner "Installing update..." else echo "Installing update..." fi # Run installer with visible output (but capture for error handling) local install_output if install_output=$("$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" --update 2>&1); then if [[ -t 1 ]]; then stop_inline_spinner; fi local filtered_output filtered_output=$(printf '%s\n' "$install_output" | sed '/^$/d') if [[ -n "$filtered_output" ]]; then printf '\n%s\n' "$filtered_output" fi # Only show success message if installer didn't already do so if ! printf '%s\n' "$install_output" | grep -Eq "Updated to latest version|Already on latest version"; then local new_version new_version=$("$mole_path" --version 2> /dev/null | awk 'NF {print $NF}' || echo "") printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-unknown})" else printf '\n' fi else # Retry without --update flag if install_output=$("$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" 2>&1); then if [[ -t 1 ]]; then stop_inline_spinner; fi local filtered_output filtered_output=$(printf '%s\n' "$install_output" | sed '/^$/d') if [[ -n "$filtered_output" ]]; then printf '\n%s\n' "$filtered_output" fi if ! printf '%s\n' "$install_output" | grep -Eq "Updated to latest version|Already on latest version"; then local new_version new_version=$("$mole_path" --version 2> /dev/null | awk 'NF {print $NF}' || echo "") printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-unknown})" else printf '\n' fi else if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" log_error "Update failed" echo "$install_output" | tail -10 >&2 # Show last 10 lines of error exit 1 fi fi rm -f "$tmp_installer" rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message" } # Remove Mole from system remove_mole() { # Detect all installations with loading if [[ -t 1 ]]; then start_inline_spinner "Detecting Mole installations..." else echo "Detecting installations..." fi local is_homebrew=false local -a manual_installs=() local -a alias_installs=() # Check Homebrew if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then is_homebrew=true fi # Check common manual install locations local -a common_paths=( "/usr/local/bin/mole" "$HOME/.local/bin/mole" "/opt/local/bin/mole" ) for path in "${common_paths[@]}"; do if [[ -f "$path" ]]; then # Check if it's not a Homebrew symlink if [[ ! -L "$path" ]] || ! readlink "$path" | grep -q "Cellar/mole"; then manual_installs+=("$path") fi fi done local -a alias_candidates=( "/usr/local/bin/mo" "$HOME/.local/bin/mo" "/opt/local/bin/mo" ) for alias in "${alias_candidates[@]}"; do if [[ -f "$alias" ]]; then alias_installs+=("$alias") fi done if [[ -t 1 ]]; then stop_inline_spinner fi printf '\n' # Check if anything to remove local manual_count=${#manual_installs[@]} local alias_count=${#alias_installs[@]} if [[ "$is_homebrew" == "false" && ${manual_count:-0} -eq 0 && ${alias_count:-0} -eq 0 ]]; then printf '%s\n\n' "${YELLOW}No Mole installation detected${NC}" exit 0 fi # Show what will be removed echo -e "${YELLOW}Remove Mole${NC} - will delete the following:" if [[ "$is_homebrew" == "true" ]]; then echo " - Mole via Homebrew" fi for install in ${manual_installs[@]+"${manual_installs[@]}"} ${alias_installs[@]+"${alias_installs[@]}"}; do echo " - $install" done echo " - ~/.config/mole" echo " - ~/.cache/mole" echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to confirm, ${GRAY}ESC${NC} to cancel: " # Read single key IFS= read -r -s -n1 key || key="" case "$key" in $'\e') echo -e "${GRAY}Cancelled${NC}" echo "" exit 0 ;; "" | $'\n' | $'\r') printf "\r\033[K" # Clear the prompt line # Continue with removal ;; *) echo -e "${GRAY}Cancelled${NC}" echo "" exit 0 ;; esac # Remove Homebrew installation (silent) local has_error=false if [[ "$is_homebrew" == "true" ]]; then if ! brew uninstall mole > /dev/null 2>&1; then has_error=true fi fi # Remove manual installations if [[ ${manual_count:-0} -gt 0 ]]; then for install in "${manual_installs[@]}"; do if [[ -f "$install" ]]; then # Check if directory requires sudo (deletion is a directory operation) if [[ ! -w "$(dirname "$install")" ]]; then # Requires sudo if ! sudo rm -f "$install" 2> /dev/null; then has_error=true fi else # Regular user permission if ! rm -f "$install" 2> /dev/null; then has_error=true fi fi fi done fi if [[ ${alias_count:-0} -gt 0 ]]; then for alias in "${alias_installs[@]}"; do if [[ -f "$alias" ]]; then # Check if directory requires sudo if [[ ! -w "$(dirname "$alias")" ]]; then sudo rm -f "$alias" 2> /dev/null || true else rm -f "$alias" 2> /dev/null || true fi fi done fi # Clean up cache first (silent) if [[ -d "$HOME/.cache/mole" ]]; then rm -rf "$HOME/.cache/mole" 2> /dev/null || true fi # Clean up configuration last (silent) if [[ -d "$HOME/.config/mole" ]]; then rm -rf "$HOME/.config/mole" 2> /dev/null || true fi # Show final result local final_message if [[ "$has_error" == "true" ]]; then final_message="${YELLOW}${ICON_ERROR} Mole uninstalled with some errors, thank you for using Mole!${NC}" else final_message="${GREEN}${ICON_SUCCESS} Mole uninstalled successfully, thank you for using Mole!${NC}" fi printf '\n%s\n\n' "$final_message" exit 0 } # Display main menu options with minimal refresh to avoid flicker show_main_menu() { local selected="${1:-1}" local _full_draw="${2:-true}" # Kept for compatibility (unused) local banner="${MAIN_MENU_BANNER:-}" local update_message="${MAIN_MENU_UPDATE_MESSAGE:-}" # Fallback if globals missing (should not happen) if [[ -z "$banner" ]]; then banner="$(show_brand_banner)" MAIN_MENU_BANNER="$banner" fi printf '\033[H' # Move cursor to home local line="" # Leading spacer printf '\r\033[2K\n' # Brand banner while IFS= read -r line || [[ -n "$line" ]]; do printf '\r\033[2K%s\n' "$line" done <<< "$banner" # Update notification block (if present) if [[ -n "$update_message" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do printf '\r\033[2K%s\n' "$line" done <<< "$update_message" fi # Spacer before menu options printf '\r\033[2K\n' printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)")" printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")" printf '\r\033[2K%s\n' "$(show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)")" printf '\r\033[2K%s\n' "$(show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)")" printf '\r\033[2K%s\n' "$(show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)")" if [[ -t 0 ]]; then printf '\r\033[2K\n' printf '\r\033[2K%s\n' " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit" printf '\r\033[2K\n' fi # Clear any remaining content below without full screen wipe printf '\033[J' } # Interactive main menu loop interactive_main_menu() { # Show intro animation only once per terminal tab if [[ -t 1 ]]; then local tty_name tty_name=$(tty 2> /dev/null || echo "") if [[ -n "$tty_name" ]]; then local flag_file flag_file="/tmp/mole_intro_$(echo "$tty_name" | tr -c '[:alnum:]_' '_')" if [[ ! -f "$flag_file" ]]; then animate_mole_intro touch "$flag_file" 2> /dev/null || true fi fi fi local current_option=1 local first_draw=true local brand_banner="" local msg_cache="$HOME/.cache/mole/update_message" local update_message="" brand_banner="$(show_brand_banner)" MAIN_MENU_BANNER="$brand_banner" if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then update_message="$(cat "$msg_cache" 2> /dev/null || echo "")" fi MAIN_MENU_UPDATE_MESSAGE="$update_message" cleanup_and_exit() { show_cursor exit 0 } trap cleanup_and_exit INT hide_cursor while true; do show_main_menu $current_option "$first_draw" if [[ "$first_draw" == "true" ]]; then first_draw=false fi # Drain any pending input to prevent touchpad scroll issues drain_pending_input local key if ! key=$(read_key); then continue fi case "$key" in "UP") ((current_option > 1)) && ((current_option--)) ;; "DOWN") ((current_option < 5)) && ((current_option++)) ;; "ENTER" | "$current_option") show_cursor case $current_option in 1) exec "$SCRIPT_DIR/bin/clean.sh" ;; 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; 3) exec "$SCRIPT_DIR/bin/analyze.sh" ;; 4) clear show_help exit 0 ;; 5) cleanup_and_exit ;; esac ;; "QUIT") cleanup_and_exit ;; [1-5]) show_cursor case $key in 1) exec "$SCRIPT_DIR/bin/clean.sh" ;; 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; 3) exec "$SCRIPT_DIR/bin/analyze.sh" ;; 4) clear show_help exit 0 ;; 5) cleanup_and_exit ;; esac ;; esac done } main() { case "${1:-""}" in "clean") exec "$SCRIPT_DIR/bin/clean.sh" "${@:2}" ;; "uninstall") exec "$SCRIPT_DIR/bin/uninstall.sh" ;; "analyze") exec "$SCRIPT_DIR/bin/analyze.sh" "${@:2}" ;; "touchid") exec "$SCRIPT_DIR/bin/touchid.sh" "${@:2}" ;; "update") update_mole exit 0 ;; "remove") remove_mole exit 0 ;; "help" | "--help" | "-h") show_help exit 0 ;; "version" | "--version" | "-V") show_version exit 0 ;; "") check_for_updates interactive_main_menu ;; *) echo "Unknown command: $1" echo "Use 'mole --help' for usage information." exit 1 ;; esac } main "$@"