1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 12:41:46 +00:00
Files
Mole/mole
2025-11-15 20:15:48 +08:00

596 lines
19 KiB
Bash
Executable File

#!/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.9.2"
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 optimize" "$NC" "System health check & optimization"
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=""
drain_pending_input # Clean up any escape sequence remnants
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 "Optimize Mac - System health & tuning" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 4 ]] && 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}1-4${NC} Quick Select ${GRAY}|${NC} ${GRAY}H${NC} Help ${GRAY}|${NC} ${GRAY}Q${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 < 4)) && ((current_option++)) ;;
"ENTER")
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/optimize.sh" ;;
4) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
esac
;;
"CHAR:1") show_cursor; exec "$SCRIPT_DIR/bin/clean.sh" ;;
"CHAR:2") show_cursor; exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
"CHAR:3") show_cursor; exec "$SCRIPT_DIR/bin/optimize.sh" ;;
"CHAR:4") show_cursor; exec "$SCRIPT_DIR/bin/analyze.sh" ;;
"HELP")
show_cursor
clear
show_help
exit 0
;;
"QUIT") cleanup_and_exit ;;
esac
done
}
main() {
case "${1:-""}" in
"optimize")
exec "$SCRIPT_DIR/bin/optimize.sh"
;;
"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 "$@"