1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 10:21:45 +00:00

Neat and uniform output

This commit is contained in:
Tw93
2025-10-09 14:24:00 +08:00
parent d34bf99d8f
commit 70c5db8c9a
9 changed files with 361 additions and 159 deletions

View File

@@ -1220,17 +1220,38 @@ display_file_types() {
return
fi
# Analyze common file types
local -A type_map=(
["Videos"]="kMDItemContentType == 'public.movie' || kMDItemContentType == 'public.video'"
["Images"]="kMDItemContentType == 'public.image'"
["Archives"]="kMDItemContentType == 'public.archive' || kMDItemContentType == 'public.zip-archive'"
["Documents"]="kMDItemContentType == 'com.adobe.pdf' || kMDItemContentType == 'public.text'"
["Audio"]="kMDItemContentType == 'public.audio'"
)
for type_name in "${!type_map[@]}"; do
local query="${type_map[$type_name]}"
# Analyze common file types (bash 3.2 compatible - no associative arrays)
local -a type_names=("Videos" "Images" "Archives" "Documents" "Audio")
local type_name
for type_name in "${type_names[@]}"; do
local query=""
local badge="$BADGE_FILE"
# Map type name to query and badge
case "$type_name" in
"Videos")
query="kMDItemContentType == 'public.movie' || kMDItemContentType == 'public.video'"
badge="$BADGE_MEDIA"
;;
"Images")
query="kMDItemContentType == 'public.image'"
badge="$BADGE_MEDIA"
;;
"Archives")
query="kMDItemContentType == 'public.archive' || kMDItemContentType == 'public.zip-archive'"
badge="$BADGE_BUNDLE"
;;
"Documents")
query="kMDItemContentType == 'com.adobe.pdf' || kMDItemContentType == 'public.text'"
badge="$BADGE_FILE"
;;
"Audio")
query="kMDItemContentType == 'public.audio'"
badge="🎵"
;;
esac
local files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null)
local count=$(echo "$files" | grep -c . || echo "0")
local total_size=0
@@ -1245,13 +1266,6 @@ display_file_types() {
if [[ $total_size -gt 0 ]]; then
local human_size=$(bytes_to_human "$total_size")
local badge="$BADGE_FILE"
case "$type_name" in
"Videos"|"Images") badge="$BADGE_MEDIA" ;;
"Archives") badge="$BADGE_BUNDLE" ;;
"Documents") badge="$BADGE_FILE" ;;
"Audio") badge="🎵" ;;
esac
printf " %s %-12s %8s (%d files)\n" "$badge" "$type_name:" "$human_size" "$count"
fi
fi

View File

@@ -1325,11 +1325,30 @@ perform_cleanup() {
local freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}')
if [[ "$DRY_RUN" == "true" ]]; then
echo "Potential reclaimable space: ${GREEN}${freed_gb}GB${NC} (no changes made) | Free space now: $(get_free_space)"
# Show file/category stats for dry run
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
printf "Files to clean: %s | Categories: %s\n" "$files_cleaned" "$total_items"
elif [[ $files_cleaned -gt 0 ]]; then
printf "Files to clean: %s\n" "$files_cleaned"
elif [[ $total_items -gt 0 ]]; then
printf "Categories: %s\n" "$total_items"
fi
echo ""
echo "To protect specific cache files from deletion, run: mole clean --whitelist"
else
echo "Space freed: ${GREEN}${freed_gb}GB${NC} | Free space now: $(get_free_space)"
fi
if [[ "$DRY_RUN" != "true" ]]; then
# Show file/category stats for actual cleanup
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
printf "Files cleaned: %s | Categories: %s\n" "$files_cleaned" "$total_items"
elif [[ $files_cleaned -gt 0 ]]; then
printf "Files cleaned: %s\n" "$files_cleaned"
elif [[ $total_items -gt 0 ]]; then
printf "Categories: %s\n" "$total_items"
fi
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
@@ -1344,14 +1363,6 @@ perform_cleanup() {
echo "No significant space was freed (system was already clean) | Free space: $(get_free_space)"
fi
fi
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
printf "Files cleaned: %s | Categories processed: %s\n" "$files_cleaned" "$total_items"
elif [[ $files_cleaned -gt 0 ]]; then
printf "Files cleaned: %s\n" "$files_cleaned"
elif [[ $total_items -gt 0 ]]; then
printf "Categories processed: %s\n" "$total_items"
fi
printf "====================================================================\n"
}

View File

@@ -122,7 +122,7 @@ scan_applications() {
fi
fi
local temp_file=$(mktemp_file)
local temp_file=$(create_temp_file)
echo "" >&2 # Add space before scanning output without breaking stdout return
# Pre-cache current epoch to avoid repeated calls
@@ -375,7 +375,8 @@ load_applications() {
uninstall_applications() {
local total_size_freed=0
log_header "Uninstalling selected applications"
echo ""
echo -e "${PURPLE}▶ Uninstalling selected applications${NC}"
if [[ ${#selected_apps[@]} -eq 0 ]]; then
log_warning "No applications selected for uninstallation"
@@ -389,14 +390,14 @@ uninstall_applications() {
# Check if app is running
if pgrep -f "$app_name" >/dev/null 2>&1; then
log_warning "$app_name is currently running"
echo -e "${YELLOW}$app_name is currently running${NC}"
read -p " Force quit $app_name? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
pkill -f "$app_name" 2>/dev/null || true
sleep 2
else
log_warning "Skipping $app_name (still running)"
echo -e " ${BLUE}${NC} Skipped $app_name"
continue
fi
fi
@@ -414,7 +415,7 @@ uninstall_applications() {
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
# Show what will be removed
echo -e " ${YELLOW}Files to be removed:${NC}"
echo -e "${BLUE}${NC} $app_name - Files to be removed:"
echo -e " ${GREEN}${NC} Application: $(echo "$app_path" | sed "s|$HOME|~|")"
# Show user-level files
@@ -425,7 +426,7 @@ uninstall_applications() {
# Show system-level files
if [[ -n "$system_files" ]]; then
while IFS= read -r file; do
[[ -n "$file" && -e "$file" ]] && echo -e " ${YELLOW}${NC} [System] $file"
[[ -n "$file" && -e "$file" ]] && echo -e " ${BLUE}${NC} System: $file"
done <<< "$system_files"
fi
@@ -448,7 +449,7 @@ uninstall_applications() {
if rm -rf "$app_path" 2>/dev/null; then
echo -e " ${GREEN}${NC} Removed application"
else
log_error "Failed to remove $app_path"
echo -e " ${RED}${NC} Failed to remove $app_path"
continue
fi
@@ -463,13 +464,13 @@ uninstall_applications() {
# Remove system-level files (requires sudo)
if [[ -n "$system_files" ]]; then
echo -e " ${YELLOW}System-level files require administrator privileges${NC}"
echo -e " ${BLUE}${NC} Admin access required for system files"
while IFS= read -r file; do
if [[ -n "$file" && -e "$file" ]]; then
if sudo rm -rf "$file" 2>/dev/null; then
echo -e " ${GREEN}${NC} Removed [System] $(basename "$file")"
echo -e " ${GREEN}${NC} Removed $(basename "$file")"
else
log_warning "Failed to remove system file: $file"
echo -e " ${YELLOW}${NC} Failed to remove: $file"
fi
fi
done <<< "$system_files"
@@ -479,15 +480,15 @@ uninstall_applications() {
((files_cleaned++))
((total_items++))
log_success "$app_name uninstalled successfully"
echo -e " ${GREEN}${NC} $app_name uninstalled successfully"
else
echo -e " ${BLUE}${NC} Skipped $app_name"
echo -e " ${BLUE}${NC} Skipped $app_name"
fi
done
# Show final summary
echo ""
log_header "Uninstallation Summary"
echo -e "${PURPLE}Uninstallation Summary${NC}"
if [[ $total_size_freed -gt 0 ]]; then
if [[ $total_size_freed -gt 1048576 ]]; then # > 1GB
@@ -498,10 +499,10 @@ uninstall_applications() {
local freed_display="${total_size_freed}KB"
fi
log_success "Freed $freed_display of disk space"
echo -e " ${GREEN}${NC} Freed $freed_display of disk space"
fi
echo "Applications uninstalled: $files_cleaned"
echo -e " ${GREEN}${NC} Applications uninstalled: $files_cleaned"
((total_size_cleaned += total_size_freed))
}
@@ -561,7 +562,7 @@ main() {
local extra=$((selection_count-3))
local list="${names[*]}"
[[ $extra -gt 0 ]] && list+=" +${extra}"
echo " ${selection_count} apps: ${list}"
echo -e "${BLUE}${NC} ${selection_count} apps: ${list}"
# Execute batch uninstallation (handles confirmation)
batch_uninstall_applications

View File

@@ -19,17 +19,25 @@ start_line_spinner() {
( while true; do c="${chars:$((i % ${#chars})):1}"; printf "\r ${BLUE}%s${NC} %s" "$c" "$msg"; ((i++)); sleep 0.12; done ) &
_SPINNER_PID=$!
}
stop_line_spinner() { if [[ -n "$_SPINNER_PID" ]]; then kill "$_SPINNER_PID" 2>/dev/null || true; wait "$_SPINNER_PID" 2>/dev/null || true; _SPINNER_PID=""; printf "\r"; fi; }
stop_line_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"; fi; }
# Verbosity (0 = quiet, 1 = verbose)
VERBOSE=1
# Icons (duplicated from lib/common.sh - necessary as install.sh runs standalone)
readonly ICON_SUCCESS="✓"
readonly ICON_ADMIN="●"
readonly ICON_CONFIRM="◎"
readonly ICON_ERROR="✗"
# Logging functions
log_info() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${BLUE}$1${NC}"; }
log_success() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${GREEN}$1${NC}"; }
log_success() { [[ ${VERBOSE} -eq 1 ]] && echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"; }
log_warning() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${YELLOW}$1${NC}"; }
log_error() { echo -e "${RED}$1${NC}"; }
log_error() { echo -e "${RED}${ICON_ERROR}${NC} $1"; }
log_admin() { [[ ${VERBOSE} -eq 1 ]] && echo -e " ${BLUE}${ICON_ADMIN}${NC} $1"; }
log_confirm() { [[ ${VERBOSE} -eq 1 ]] && echo -e "${BLUE}${ICON_CONFIRM}${NC} $1"; }
# Default installation directory
INSTALL_DIR="/usr/local/bin"
@@ -239,12 +247,14 @@ install_files() {
if [[ -f "$SOURCE_DIR/mole" ]]; then
if [[ "$source_dir_abs" != "$install_dir_abs" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
log_admin "Admin access required for /usr/local/bin"
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
log_success "Installed mole to $INSTALL_DIR"
fi
else
log_error "mole executable not found in ${SOURCE_DIR:-unknown}"
@@ -254,7 +264,7 @@ install_files() {
# Install mo alias for Mole if available
if [[ -f "$SOURCE_DIR/mo" ]]; then
if [[ "$source_dir_abs" == "$install_dir_abs" ]]; then
log_info "mo alias already present in $INSTALL_DIR"
log_success "mo alias already present"
else
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
sudo cp "$SOURCE_DIR/mo" "$INSTALL_DIR/mo"
@@ -263,6 +273,7 @@ install_files() {
cp "$SOURCE_DIR/mo" "$INSTALL_DIR/mo"
chmod +x "$INSTALL_DIR/mo"
fi
log_success "Installed mo alias"
fi
fi
@@ -271,10 +282,11 @@ install_files() {
local source_bin_abs="$(cd "$SOURCE_DIR/bin" && pwd)"
local config_bin_abs="$(cd "$CONFIG_DIR/bin" && pwd)"
if [[ "$source_bin_abs" == "$config_bin_abs" ]]; then
log_info "Configuration bin directory already synced"
log_success "Modules already synced"
else
cp -r "$SOURCE_DIR/bin"/* "$CONFIG_DIR/bin/"
chmod +x "$CONFIG_DIR/bin"/*
log_success "Installed modules"
fi
fi
@@ -282,9 +294,10 @@ install_files() {
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"
log_success "Libraries already synced"
else
cp -r "$SOURCE_DIR/lib"/* "$CONFIG_DIR/lib/"
log_success "Installed libraries"
fi
fi
@@ -355,6 +368,8 @@ print_usage_summary() {
return
fi
echo ""
local message="Mole ${action} successfully"
if [[ "$action" == "updated" && -n "$previous_version" && -n "$new_version" && "$previous_version" != "$new_version" ]]; then
@@ -363,7 +378,7 @@ print_usage_summary() {
message+=" (version ${new_version})"
fi
log_success "$message!"
log_confirm "$message"
echo ""
echo "Usage:"
@@ -389,16 +404,18 @@ print_usage_summary() {
# Uninstall function
uninstall_mole() {
log_info "Uninstalling mole..."
log_confirm "Uninstalling Mole"
echo ""
# Remove executable
if [[ -f "$INSTALL_DIR/mole" ]]; then
if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] && [[ ! -w "$INSTALL_DIR" ]]; then
log_admin "Admin access required"
sudo rm -f "$INSTALL_DIR/mole"
else
rm -f "$INSTALL_DIR/mole"
fi
log_success "Removed executable from $INSTALL_DIR"
log_success "Removed mole executable"
fi
if [[ -f "$INSTALL_DIR/mo" ]]; then
@@ -407,7 +424,7 @@ uninstall_mole() {
else
rm -f "$INSTALL_DIR/mo"
fi
log_success "Removed mo alias from $INSTALL_DIR"
log_success "Removed mo alias"
fi
# SAFETY CHECK: Verify config directory is safe to remove
@@ -442,14 +459,15 @@ uninstall_mole() {
echo ""
read -p "Remove configuration directory $CONFIG_DIR? (y/N): " -n 1 -r; echo ""; if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$CONFIG_DIR"
log_success "Removed configuration directory"
log_success "Removed configuration"
else
log_info "Configuration directory preserved"
log_success "Configuration preserved"
fi
fi
fi
log_success "Mole uninstalled successfully"
echo ""
log_confirm "Mole uninstalled successfully"
}
# Main installation function
@@ -486,12 +504,26 @@ perform_update() {
update_via_homebrew "$VERSION"
else
# Fallback: inline implementation
echo -e "${BLUE}|${NC} Updating Homebrew..."
if [[ -t 1 ]]; then
start_line_spinner "Updating Homebrew..."
else
echo "Updating Homebrew..."
fi
brew update 2>&1 | grep -Ev "^(==>|Already up-to-date)" || true
if [[ -t 1 ]]; then
stop_line_spinner
fi
echo -e "${BLUE}|${NC} Upgrading Mole..."
if [[ -t 1 ]]; then
start_line_spinner "Upgrading Mole..."
else
echo "Upgrading Mole..."
fi
local upgrade_output
upgrade_output=$(brew upgrade mole 2>&1) || true
if [[ -t 1 ]]; then
stop_line_spinner
fi
if echo "$upgrade_output" | grep -q "already installed"; then
local current_version

View File

@@ -57,23 +57,32 @@ batch_uninstall_applications() {
# Format size display (convert KB to bytes for bytes_to_human())
local size_display=$(bytes_to_human "$((total_estimated_size * 1024))")
# Request sudo access if needed (do this before confirmation)
# Show summary and get batch confirmation first (before asking for password)
local app_total=${#selected_apps[@]}
local app_text="app"
[[ $app_total -gt 1 ]] && app_text="apps"
if [[ ${#running_apps[@]} -gt 0 ]]; then
echo -n "${BLUE}${ICON_CONFIRM}${NC} Remove ${app_total} ${app_text} | ${size_display} | Force quit: ${running_apps[*]} | Enter=go / ESC=q: "
else
echo -n "${BLUE}${ICON_CONFIRM}${NC} Remove ${app_total} ${app_text} | ${size_display} | Enter=go / ESC=q: "
fi
IFS= read -r -s -n1 key || key=""
case "$key" in
$'\e'|q|Q) echo ""; return 0 ;;
""|$'\n'|$'\r'|y|Y) echo "" ;;
*) echo ""; return 0 ;;
esac
# User confirmed, now request sudo access if needed
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
# Check if sudo is already cached
if sudo -n true 2>/dev/null; then
echo "Admin access confirmed for: ${sudo_apps[*]}"
else
echo "◎ Admin required for: ${sudo_apps[*]}"
echo ""
if ! request_sudo_access "Uninstalling system apps requires admin access"; then
if ! sudo -n true 2>/dev/null; then
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
echo ""
log_error "Admin access denied"
return 1
fi
echo ""
echo "✓ Admin access granted"
fi
echo "◎ Gathering targets..."
(while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null) &
local sudo_keepalive_pid=$!
local _trap_cleanup_cmd="kill $sudo_keepalive_pid 2>/dev/null || true; wait $sudo_keepalive_pid 2>/dev/null || true"
@@ -87,22 +96,7 @@ batch_uninstall_applications() {
done
fi
# Show summary and get batch confirmation
local app_total=${#selected_apps[@]}
if [[ ${#running_apps[@]} -gt 0 ]]; then
echo -n "${BLUE}◎ Remove ${app_total} app(s) (${size_display}) | Quit: ${running_apps[*]} | Enter=go / ESC=q:${NC} "
else
echo -n "${BLUE}◎ Remove ${app_total} app(s) (${size_display}) | Enter=go / ESC=q:${NC} "
fi
IFS= read -r -s -n1 key || key=""
case "$key" in
$'\e'|q|Q) echo ""; return 0 ;;
""|$'\n'|$'\r'|y|Y) echo "" ;;
*) echo ""; return 0 ;;
esac
echo -n "◎ Starting in 3s... 3"; sleep 1; echo -ne "\r◎ Starting in 3s... 2"; sleep 1; echo -ne "\r◎ Starting in 3s... 1"; sleep 1
echo -ne "\r\033[K"
echo ""
if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi
# Force quit running apps first (batch)
@@ -113,11 +107,11 @@ batch_uninstall_applications() {
if pgrep -f "${running_apps[0]}" >/dev/null 2>&1; then sleep 1; fi
fi
# Perform uninstallations (compact output)
# Perform uninstallations (silent mode, show results at end)
if [[ -t 1 ]]; then stop_inline_spinner; fi
echo ""
local success_count=0 failed_count=0
local -a failed_items=()
local -a success_items=()
for detail in "${app_details[@]}"; do
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
local related_files=$(echo "$encoded_files" | base64 -d)
@@ -144,7 +138,7 @@ batch_uninstall_applications() {
((success_count++))
((files_cleaned++))
((total_items++))
printf " ${GREEN}OK${NC} %-20s%s\n" "$app_name" $([[ $files_removed -gt 0 ]] && echo "+$files_removed" )
success_items+=("$app_name")
else
((failed_count++))
failed_items+=("$app_name:$reason")
@@ -152,32 +146,36 @@ batch_uninstall_applications() {
done
# Summary
local freed_display="0B"
if [[ $total_size_freed -gt 0 ]]; then
local freed_kb=$total_size_freed
if [[ $freed_kb -ge 1048576 ]]; then
freed_display=$(echo "$freed_kb" | awk '{printf "%.2fGB", $1/1024/1024}')
elif [[ $freed_kb -ge 1024 ]]; then
freed_display=$(echo "$freed_kb" | awk '{printf "%.1fMB", $1/1024}')
else
freed_display="${freed_kb}KB"
fi
fi
local freed_display=$(bytes_to_human "$((total_size_freed * 1024))")
local bar="================================================================================"
echo ""
echo "$bar"
if [[ $success_count -gt 0 ]]; then
local success_list="${success_items[*]}"
echo -e "Removed: ${GREEN}${success_list}${NC} | Freed: ${GREEN}${freed_display}${NC}"
fi
if [[ $failed_count -gt 0 ]]; then
echo -e "Removed: ${GREEN}$success_count${NC} | Failed: ${RED}$failed_count${NC} | Freed: ${GREEN}$freed_display${NC}"
local failed_names=()
local reason_summary=""
for item in "${failed_items[@]}"; do
local name=${item%%:*}
failed_names+=("$name")
done
local failed_list="${failed_names[*]}"
# Determine primary reason
if [[ $failed_count -eq 1 ]]; then
local first="${failed_items[0]}"
local name=${first%%:*}
local reason=${first#*:}
echo "${name} $(map_uninstall_reason "$reason")"
local first_reason=${failed_items[0]#*:}
case "$first_reason" in
still*running*) reason_summary="still running" ;;
remove*failed*) reason_summary="could not be removed" ;;
permission*) reason_summary="permission denied" ;;
*) reason_summary="$first_reason" ;;
esac
echo -e "Failed: ${RED}${failed_list}${NC} ${reason_summary}"
else
local joined="${failed_items[*]}"; echo "Failures: $joined"
echo -e "Failed: ${RED}${failed_list}${NC} could not be removed"
fi
else
echo -e "Removed: ${GREEN}$success_count${NC} | Failed: ${RED}$failed_count${NC} | Freed: ${GREEN}$freed_display${NC}"
fi
echo "$bar"

View File

@@ -20,6 +20,15 @@ readonly RED="${ESC}[0;31m"
readonly GRAY="${ESC}[0;90m"
readonly NC="${ESC}[0m"
# Icon definitions
readonly ICON_CONFIRM="◎" # Confirm operation
readonly ICON_ADMIN="●" # Admin permission
readonly ICON_SUCCESS="✓" # Success
readonly ICON_ERROR="✗" # Error
readonly ICON_EMPTY="○" # Empty state
readonly ICON_LIST="-" # List item
readonly ICON_MENU="▸" # Menu item
# Spinner character helpers (ASCII by default, overridable via env)
mo_spinner_chars() {
local chars="${MO_SPINNER_CHARS:-|/-\\}"
@@ -52,7 +61,7 @@ log_info() {
log_success() {
rotate_log
echo -e " ${GREEN}${NC} $1"
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true
}
@@ -64,16 +73,47 @@ log_warning() {
log_error() {
rotate_log
echo -e "${RED}$1${NC}" >&2
echo -e "${RED}${ICON_ERROR}${NC} $1" >&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 -e "\n${PURPLE}${ICON_MENU} $1${NC}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SECTION: $1" >> "$LOG_FILE" 2>/dev/null || true
}
# Icon output helpers
icon_confirm() {
echo -e "${BLUE}${ICON_CONFIRM}${NC} $1"
}
icon_admin() {
echo -e "${BLUE}${ICON_ADMIN}${NC} $1"
}
icon_success() {
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"
}
icon_error() {
echo -e " ${RED}${ICON_ERROR}${NC} $1"
}
icon_empty() {
echo -e " ${BLUE}${ICON_EMPTY}${NC} $1"
}
icon_list() {
echo -e " ${ICON_LIST} $1"
}
icon_menu() {
local num="$1"
local text="$2"
echo -e "${BLUE}${ICON_MENU} ${num}. ${text}${NC}"
}
# System detection
detect_architecture() {
if [[ "$(uname -m)" == "arm64" ]]; then
@@ -276,7 +316,7 @@ request_sudo_access() {
# If Touch ID is supported and not forced to use password
if [[ "$force_password" != "true" ]] && check_touchid_support; then
echo -e "${BLUE}${prompt_msg}${NC} ${GRAY}(Touch ID or password)${NC}"
echo -e "${BLUE}${ICON_ADMIN}${NC} ${prompt_msg} ${GRAY}(Touch ID or password)${NC}"
if sudo -v 2>/dev/null; then
return 0
else
@@ -284,8 +324,8 @@ request_sudo_access() {
fi
else
# Traditional password method
echo -e "${BLUE}${prompt_msg}${NC}"
echo -ne "${BLUE} Password> ${NC}"
echo -e "${BLUE}${ICON_ADMIN}${NC} ${prompt_msg}"
echo -ne "${BLUE}${ICON_MENU}${NC} Password: "
read -s password
echo ""
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
@@ -313,13 +353,27 @@ request_sudo() {
update_via_homebrew() {
local version="${1:-unknown}"
echo -e "${BLUE}|${NC} Updating Homebrew..."
if [[ -t 1 ]]; then
start_inline_spinner "Updating Homebrew..."
else
echo "Updating Homebrew..."
fi
# Filter out common noise but show important info
brew update 2>&1 | grep -Ev "^(==>|Already up-to-date)" || true
if [[ -t 1 ]]; then
stop_inline_spinner
fi
echo -e "${BLUE}|${NC} Upgrading Mole..."
if [[ -t 1 ]]; then
start_inline_spinner "Upgrading Mole..."
else
echo "Upgrading Mole..."
fi
local upgrade_output
upgrade_output=$(brew upgrade mole 2>&1) || true
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if echo "$upgrade_output" | grep -q "already installed"; then
# Get current version
@@ -397,17 +451,21 @@ start_inline_spinner() {
if [[ -t 1 ]]; then
(
trap 'exit 0' TERM INT EXIT
local chars
chars="$(mo_spinner_chars)"
[[ -z "$chars" ]] && chars='|/-\'
local i=0
while true; do
local c="${chars:$((i % ${#chars})):1}"
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}%s${NC} %s" "$c" "$message"
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}%s${NC} %s" "$c" "$message" 2>/dev/null || exit 0
((i++))
sleep 0.12
# macOS supports decimal sleep, this is the primary target
sleep 0.1 2>/dev/null || sleep 1 2>/dev/null || exit 0
done
) &
INLINE_SPINNER_PID=$!
disown 2>/dev/null || true
else
echo -n " ${BLUE}|${NC} $message"
fi
@@ -419,7 +477,7 @@ stop_inline_spinner() {
kill "$INLINE_SPINNER_PID" 2>/dev/null || true
wait "$INLINE_SPINNER_PID" 2>/dev/null || true
INLINE_SPINNER_PID=""
[[ -t 1 ]] && printf "\r"
[[ -t 1 ]] && printf "\r\033[K"
fi
}
@@ -556,10 +614,45 @@ parallel_execute() {
# Set MOLE_SPINNER_PREFIX=" " for indented spinner (e.g., in clean context)
with_spinner() {
local msg="$1"; shift || true
local timeout="${MOLE_CMD_TIMEOUT:-180}" # Default 3min timeout
if [[ -t 1 ]]; then
start_inline_spinner "$msg"
fi
"$@" >/dev/null 2>&1 || return $?
# Run command with timeout protection
if command -v timeout >/dev/null 2>&1; then
# GNU timeout available
timeout "$timeout" "$@" >/dev/null 2>&1 || {
local exit_code=$?
if [[ -t 1 ]]; then stop_inline_spinner; fi
# Exit code 124 means timeout
[[ $exit_code -eq 124 ]] && echo -e " ${YELLOW}${NC} $msg timed out (skipped)" >&2
return $exit_code
}
else
# Fallback: run in background with manual timeout
"$@" >/dev/null 2>&1 &
local cmd_pid=$!
local elapsed=0
while kill -0 $cmd_pid 2>/dev/null; do
if [[ $elapsed -ge $timeout ]]; then
kill -TERM $cmd_pid 2>/dev/null || true
wait $cmd_pid 2>/dev/null || true
if [[ -t 1 ]]; then stop_inline_spinner; fi
echo -e " ${YELLOW}${NC} $msg timed out (skipped)" >&2
return 124
fi
sleep 1
((elapsed++))
done
wait $cmd_pid 2>/dev/null || {
local exit_code=$?
if [[ -t 1 ]]; then stop_inline_spinner; fi
return $exit_code
}
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
@@ -575,8 +668,16 @@ clean_tool_cache() {
echo -e " ${YELLOW}${NC} $label (would clean)"
return 0
fi
MOLE_SPINNER_PREFIX=" " with_spinner "$label" "$@"
echo -e " ${GREEN}${NC} $label"
if MOLE_SPINNER_PREFIX=" " with_spinner "$label" "$@"; then
echo -e " ${GREEN}${NC} $label"
else
local exit_code=$?
# Timeout returns 124, don't show error message (already shown by with_spinner)
if [[ $exit_code -ne 124 ]]; then
echo -e " ${YELLOW}${NC} $label failed (skipped)" >&2
fi
fi
return 0 # Always return success to continue cleanup
}
# ============================================================================

View File

@@ -42,13 +42,21 @@ collect_files_to_be_cleaned() {
local clean_sh="$SCRIPT_DIR/../bin/clean.sh"
local -a items=()
echo -e "${BLUE}|${NC} Scanning cache files..."
echo ""
if [[ -t 1 ]]; then
start_inline_spinner "Scanning cache files..."
else
echo "Scanning cache files..."
fi
# Run clean.sh in dry-run mode
local temp_output=$(create_temp_file)
echo "" | bash "$clean_sh" --dry-run 2>&1 > "$temp_output" || true
if [[ -t 1 ]]; then
stop_inline_spinner
fi
echo ""
# Strip ANSI color codes for parsing
local temp_plain=$(create_temp_file)
sed $'s/\033\[[0-9;]*m//g' "$temp_output" > "$temp_plain"

2
mo
View File

@@ -1,5 +1,7 @@
#!/bin/bash
# Lightweight alias to run Mole via `mo`
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/mole" "$@"

107
mole
View File

@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
# Version info
VERSION="1.7.0"
VERSION="1.7.1"
MOLE_TAGLINE="can dig deep to clean your Mac."
# Check for updates (non-blocking, cached)
@@ -148,7 +148,11 @@ update_mole() {
fi
# Download and run installer with progress
echo -e "${BLUE}|${NC} Downloading latest version..."
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
@@ -157,22 +161,26 @@ update_mole() {
# 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
@@ -181,23 +189,35 @@ update_mole() {
local install_dir
install_dir="$(cd "$(dirname "$mole_path")" && pwd)"
echo -e "${BLUE}|${NC} Installing update..."
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
echo "$install_output" | grep -Ev "^$" || true
local new_version
new_version=$("$mole_path" --version 2>/dev/null | awk 'NF {print $NF}' || echo "")
echo -e "${GREEN}✓${NC} Updated to latest version (${new_version:-unknown})"
else
# Retry without --update flag
if install_output=$("$tmp_installer" --prefix "$install_dir" --config "$HOME/.config/mole" 2>&1); then
echo "$install_output" | grep -Ev "^$" || true
# Only show success message if not already shown by installer
if ! echo "$install_output" | grep -q "Already on latest version"; then
local new_version
new_version=$("$mole_path" --version 2>/dev/null | awk 'NF {print $NF}' || echo "")
echo -e "${GREEN}✓${NC} Updated to latest version (${new_version:-unknown})"
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
echo "$install_output" | grep -Ev "^$" || true
if ! echo "$install_output" | grep -q "Already on latest version"; then
local new_version
new_version=$("$mole_path" --version 2>/dev/null | awk 'NF {print $NF}' || echo "")
echo -e "${GREEN}✓${NC} Updated to latest version (${new_version:-unknown})"
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
@@ -216,7 +236,13 @@ remove_mole() {
echo -e "${YELLOW}Remove Mole${NC}"
echo ""
# Detect all installations
# 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=()
@@ -254,43 +280,52 @@ remove_mole() {
fi
done
# Show what will be removed
echo "This will remove:"
echo ""
if [[ "$is_homebrew" == "true" ]]; then
echo -e " ${GREEN}✓${NC} Mole (via Homebrew)"
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if [[ ${#manual_installs[@]} -gt 0 ]]; then
for install in "${manual_installs[@]}"; do
echo -e " ${GREEN}✓${NC} $install"
done
fi
if [[ ${#alias_installs[@]} -gt 0 ]]; then
for alias in "${alias_installs[@]}"; do
echo -e " ${GREEN}✓${NC} $alias"
done
fi
echo -e " ${GREEN}✓${NC} ~/.config/mole/ (configuration)"
echo -e " ${GREEN}✓${NC} ~/.cache/mole/ (cache)"
# Check if anything to remove
if [[ "$is_homebrew" == "false" && ${#manual_installs[@]} -eq 0 && ${#alias_installs[@]} -eq 0 ]]; then
echo ""
echo -e "${YELLOW}No Mole installation detected${NC}"
exit 0
fi
# Show what will be removed
echo "Will remove:"
echo ""
# Confirm removal
read -p "Are you sure you want to remove Mole? (y/N): " -n 1 -r; echo ""; if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
if [[ "$is_homebrew" == "true" ]]; then
echo " - Mole via Homebrew"
fi
for install in "${manual_installs[@]}" "${alias_installs[@]}"; do
echo " - $install"
done
echo " - ~/.config/mole"
echo " - ~/.cache/mole"
echo ""
echo -n "Press Enter to confirm, ESC or q to cancel: "
# Read single key
IFS= read -r -s -n1 key || key=""
echo ""
case "$key" in
$'\e'|q|Q)
echo "Cancelled"
exit 0
;;
""|$'\n'|$'\r')
# Continue with removal
;;
*)
echo "Cancelled"
exit 0
;;
esac
echo ""
# Remove Homebrew installation