mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 21:55:08 +00:00
Merge branch 'main' into dev
This commit is contained in:
@@ -33,7 +33,7 @@ clean_ds_store_tree() {
|
||||
local size
|
||||
size=$(get_file_size "$ds_file")
|
||||
total_bytes=$((total_bytes + size))
|
||||
((file_count++))
|
||||
file_count=$((file_count + 1))
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
rm -f "$ds_file" 2> /dev/null || true
|
||||
fi
|
||||
@@ -53,9 +53,9 @@ clean_ds_store_tree() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label${NC}, ${GREEN}$file_count files, $size_human${NC}"
|
||||
fi
|
||||
local size_kb=$(((total_bytes + 1023) / 1024))
|
||||
((files_cleaned += file_count))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + file_count))
|
||||
total_size_cleaned=$((total_size_cleaned + size_kb))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
@@ -113,12 +113,12 @@ scan_installed_apps() {
|
||||
local bundle_id=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$plist_path" 2> /dev/null || echo "")
|
||||
if [[ -n "$bundle_id" ]]; then
|
||||
echo "$bundle_id"
|
||||
((count++))
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done
|
||||
) > "$scan_tmp_dir/apps_${dir_idx}.txt" &
|
||||
pids+=($!)
|
||||
((dir_idx++))
|
||||
dir_idx=$((dir_idx + 1))
|
||||
done
|
||||
# Collect running apps and LaunchAgents to avoid false orphan cleanup.
|
||||
(
|
||||
@@ -300,7 +300,7 @@ clean_orphaned_app_data() {
|
||||
fi
|
||||
for match in "${matches[@]}"; do
|
||||
[[ -e "$match" ]] || continue
|
||||
((iteration_count++))
|
||||
iteration_count=$((iteration_count + 1))
|
||||
if [[ $iteration_count -gt $MOLE_MAX_ORPHAN_ITERATIONS ]]; then
|
||||
break
|
||||
fi
|
||||
@@ -314,8 +314,8 @@ clean_orphaned_app_data() {
|
||||
continue
|
||||
fi
|
||||
if safe_clean "$match" "Orphaned $label: $bundle_id"; then
|
||||
((orphaned_count++))
|
||||
((total_orphaned_kb += size_kb))
|
||||
orphaned_count=$((orphaned_count + 1))
|
||||
total_orphaned_kb=$((total_orphaned_kb + size_kb))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
@@ -326,7 +326,7 @@ clean_orphaned_app_data() {
|
||||
stop_section_spinner
|
||||
if [[ $orphaned_count -gt 0 ]]; then
|
||||
local orphaned_mb=$(echo "$total_orphaned_kb" | awk '{printf "%.1f", $1/1024}')
|
||||
echo " ${GREEN}${ICON_SUCCESS}${NC} Cleaned $orphaned_count items, about ${orphaned_mb}MB"
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Cleaned $orphaned_count items, about ${orphaned_mb}MB"
|
||||
note_activity
|
||||
fi
|
||||
rm -f "$installed_bundles"
|
||||
@@ -430,8 +430,8 @@ clean_orphaned_system_services() {
|
||||
orphaned_files+=("$plist")
|
||||
local size_kb
|
||||
size_kb=$(sudo du -skP "$plist" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
((total_orphaned_kb += size_kb))
|
||||
((orphaned_count++))
|
||||
total_orphaned_kb=$((total_orphaned_kb + size_kb))
|
||||
orphaned_count=$((orphaned_count + 1))
|
||||
break
|
||||
fi
|
||||
done
|
||||
@@ -461,8 +461,8 @@ clean_orphaned_system_services() {
|
||||
orphaned_files+=("$plist")
|
||||
local size_kb
|
||||
size_kb=$(sudo du -skP "$plist" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
((total_orphaned_kb += size_kb))
|
||||
((orphaned_count++))
|
||||
total_orphaned_kb=$((total_orphaned_kb + size_kb))
|
||||
orphaned_count=$((orphaned_count + 1))
|
||||
break
|
||||
fi
|
||||
done
|
||||
@@ -491,8 +491,8 @@ clean_orphaned_system_services() {
|
||||
orphaned_files+=("$helper")
|
||||
local size_kb
|
||||
size_kb=$(sudo du -skP "$helper" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
((total_orphaned_kb += size_kb))
|
||||
((orphaned_count++))
|
||||
total_orphaned_kb=$((total_orphaned_kb + size_kb))
|
||||
orphaned_count=$((orphaned_count + 1))
|
||||
break
|
||||
fi
|
||||
done
|
||||
@@ -673,7 +673,7 @@ clean_orphaned_launch_agents() {
|
||||
if is_launch_item_orphaned "$plist"; then
|
||||
local size_kb=$(get_path_size_kb "$plist")
|
||||
orphaned_items+=("$bundle_id|$plist")
|
||||
((total_orphaned_kb += size_kb))
|
||||
total_orphaned_kb=$((total_orphaned_kb + size_kb))
|
||||
fi
|
||||
done < <(find "$launch_agents_dir" -maxdepth 1 -name "*.plist" -print0 2> /dev/null)
|
||||
|
||||
@@ -696,7 +696,7 @@ clean_orphaned_launch_agents() {
|
||||
IFS='|' read -r bundle_id plist_path <<< "$item"
|
||||
|
||||
if [[ "$is_dry_run" == "true" ]]; then
|
||||
((dry_run_count++))
|
||||
dry_run_count=$((dry_run_count + 1))
|
||||
log_operation "clean" "DRY_RUN" "$plist_path" "orphaned launch agent"
|
||||
continue
|
||||
fi
|
||||
@@ -706,7 +706,7 @@ clean_orphaned_launch_agents() {
|
||||
|
||||
# Remove the plist file
|
||||
if safe_remove "$plist_path" false; then
|
||||
((removed_count++))
|
||||
removed_count=$((removed_count + 1))
|
||||
log_operation "clean" "REMOVED" "$plist_path" "orphaned launch agent"
|
||||
else
|
||||
log_operation "clean" "FAILED" "$plist_path" "permission denied"
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
clean_homebrew() {
|
||||
command -v brew > /dev/null 2>&1 || return 0
|
||||
if [[ "${DRY_RUN:-false}" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Homebrew · would cleanup and autoremove"
|
||||
# Check if Homebrew cache is whitelisted
|
||||
if is_path_whitelisted "$HOME/Library/Caches/Homebrew"; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew · skipped whitelist"
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Homebrew · would cleanup and autoremove"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
# Skip if cleaned recently to avoid repeated heavy operations.
|
||||
|
||||
@@ -146,6 +146,8 @@ clean_project_caches() {
|
||||
done
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
# Extra clear to prevent spinner character remnants in terminal
|
||||
[[ -t 1 ]] && printf "\r\033[2K" >&2 || true
|
||||
fi
|
||||
[[ "$has_dev_projects" == "false" ]] && return 0
|
||||
fi
|
||||
@@ -208,7 +210,7 @@ clean_project_caches() {
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
((grace_period++))
|
||||
grace_period=$((grace_period + 1))
|
||||
done
|
||||
if kill -0 "$pid" 2> /dev/null; then
|
||||
kill -KILL "$pid" 2> /dev/null || true
|
||||
|
||||
@@ -7,7 +7,17 @@ clean_tool_cache() {
|
||||
local description="$1"
|
||||
shift
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
local command_succeeded=false
|
||||
if [[ -t 1 ]]; then
|
||||
start_section_spinner "Cleaning $description..."
|
||||
fi
|
||||
if "$@" > /dev/null 2>&1; then
|
||||
command_succeeded=true
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
if [[ "$command_succeeded" == "true" ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $description"
|
||||
fi
|
||||
else
|
||||
@@ -85,7 +95,8 @@ clean_dev_npm() {
|
||||
}
|
||||
# Python/pip ecosystem caches.
|
||||
clean_dev_python() {
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
# Check pip3 is functional (not just macOS stub that triggers CLT install dialog)
|
||||
if command -v pip3 > /dev/null 2>&1 && pip3 --version > /dev/null 2>&1; then
|
||||
clean_tool_cache "pip cache" bash -c 'pip3 cache purge > /dev/null 2>&1 || true'
|
||||
note_activity
|
||||
fi
|
||||
@@ -249,11 +260,11 @@ clean_xcode_documentation_cache() {
|
||||
local entry
|
||||
for entry in "${sorted_entries[@]}"; do
|
||||
if [[ $idx -eq 0 ]]; then
|
||||
((idx++))
|
||||
idx=$((idx + 1))
|
||||
continue
|
||||
fi
|
||||
stale_entries+=("$entry")
|
||||
((idx++))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
if [[ ${#stale_entries[@]} -eq 0 ]]; then
|
||||
@@ -380,7 +391,7 @@ clean_xcode_simulator_runtime_volumes() {
|
||||
local unused_count=0
|
||||
for candidate in "${sorted_candidates[@]}"; do
|
||||
local status="UNUSED"
|
||||
if _sim_runtime_is_path_in_use "$candidate" "${mount_points[@]}"; then
|
||||
if [[ ${#mount_points[@]} -gt 0 ]] && _sim_runtime_is_path_in_use "$candidate" "${mount_points[@]}"; then
|
||||
status="IN_USE"
|
||||
in_use_count=$((in_use_count + 1))
|
||||
else
|
||||
@@ -791,12 +802,12 @@ clean_dev_jetbrains_toolbox() {
|
||||
local dir_path
|
||||
for dir_path in "${sorted_dirs[@]}"; do
|
||||
if [[ $idx -lt $keep_previous ]]; then
|
||||
((idx++))
|
||||
idx=$((idx + 1))
|
||||
continue
|
||||
fi
|
||||
safe_clean "$dir_path" "JetBrains Toolbox old IDE version"
|
||||
note_activity
|
||||
((idx++))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
done < <(command find "$product_dir" -mindepth 1 -maxdepth 1 -type d -name "ch-*" -print0 2> /dev/null)
|
||||
done
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_MOLE_HINTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
mole_hints_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1090
|
||||
source "$_MOLE_HINTS_DIR/purge_shared.sh"
|
||||
source "$mole_hints_dir/purge_shared.sh"
|
||||
|
||||
# Quick reminder probe for project build artifacts handled by `mo purge`.
|
||||
# Designed to be very fast: shallow directory checks only, no deep find scans.
|
||||
@@ -58,7 +58,7 @@ hint_get_path_size_kb_with_timeout() {
|
||||
record_project_artifact_hint() {
|
||||
local path="$1"
|
||||
|
||||
((PROJECT_ARTIFACT_HINT_COUNT++))
|
||||
PROJECT_ARTIFACT_HINT_COUNT=$((PROJECT_ARTIFACT_HINT_COUNT + 1))
|
||||
|
||||
if [[ ${#PROJECT_ARTIFACT_HINT_EXAMPLES[@]} -lt 2 ]]; then
|
||||
PROJECT_ARTIFACT_HINT_EXAMPLES+=("${path/#$HOME/~}")
|
||||
@@ -74,8 +74,8 @@ record_project_artifact_hint() {
|
||||
local size_kb=""
|
||||
if size_kb=$(hint_get_path_size_kb_with_timeout "$path" "$timeout_seconds"); then
|
||||
if [[ "$size_kb" =~ ^[0-9]+$ ]]; then
|
||||
((PROJECT_ARTIFACT_HINT_ESTIMATED_KB += size_kb))
|
||||
((PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES++))
|
||||
PROJECT_ARTIFACT_HINT_ESTIMATED_KB=$((PROJECT_ARTIFACT_HINT_ESTIMATED_KB + size_kb))
|
||||
PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES=$((PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES + 1))
|
||||
else
|
||||
PROJECT_ARTIFACT_HINT_ESTIMATE_PARTIAL=true
|
||||
fi
|
||||
@@ -140,8 +140,8 @@ probe_project_artifact_hints() {
|
||||
local root_projects_scanned=0
|
||||
|
||||
if is_quick_purge_project_root "$root"; then
|
||||
((scanned_projects++))
|
||||
((root_projects_scanned++))
|
||||
scanned_projects=$((scanned_projects + 1))
|
||||
root_projects_scanned=$((root_projects_scanned + 1))
|
||||
if [[ $scanned_projects -gt $max_projects ]]; then
|
||||
PROJECT_ARTIFACT_HINT_TRUNCATED=true
|
||||
stop_scan=true
|
||||
@@ -175,8 +175,8 @@ probe_project_artifact_hints() {
|
||||
break
|
||||
fi
|
||||
|
||||
((scanned_projects++))
|
||||
((root_projects_scanned++))
|
||||
scanned_projects=$((scanned_projects + 1))
|
||||
root_projects_scanned=$((root_projects_scanned + 1))
|
||||
if [[ $scanned_projects -gt $max_projects ]]; then
|
||||
PROJECT_ARTIFACT_HINT_TRUNCATED=true
|
||||
stop_scan=true
|
||||
@@ -206,7 +206,7 @@ probe_project_artifact_hints() {
|
||||
;;
|
||||
esac
|
||||
|
||||
((nested_count++))
|
||||
nested_count=$((nested_count + 1))
|
||||
if [[ $nested_count -gt $max_nested_per_project ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -569,16 +569,38 @@ select_purge_categories() {
|
||||
fi
|
||||
done
|
||||
local original_stty=""
|
||||
local previous_exit_trap=""
|
||||
local previous_int_trap=""
|
||||
local previous_term_trap=""
|
||||
local terminal_restored=false
|
||||
if [[ -t 0 ]] && command -v stty > /dev/null 2>&1; then
|
||||
original_stty=$(stty -g 2> /dev/null || echo "")
|
||||
fi
|
||||
previous_exit_trap=$(trap -p EXIT || true)
|
||||
previous_int_trap=$(trap -p INT || true)
|
||||
previous_term_trap=$(trap -p TERM || true)
|
||||
# Terminal control functions
|
||||
restore_terminal() {
|
||||
# Avoid trap churn when restore is called repeatedly via RETURN/EXIT paths.
|
||||
if [[ "${terminal_restored:-false}" == "true" ]]; then
|
||||
return
|
||||
fi
|
||||
terminal_restored=true
|
||||
|
||||
trap - EXIT INT TERM
|
||||
show_cursor
|
||||
if [[ -n "${original_stty:-}" ]]; then
|
||||
stty "${original_stty}" 2> /dev/null || stty sane 2> /dev/null || true
|
||||
fi
|
||||
if [[ -n "$previous_exit_trap" ]]; then
|
||||
eval "$previous_exit_trap"
|
||||
fi
|
||||
if [[ -n "$previous_int_trap" ]]; then
|
||||
eval "$previous_int_trap"
|
||||
fi
|
||||
if [[ -n "$previous_term_trap" ]]; then
|
||||
eval "$previous_term_trap"
|
||||
fi
|
||||
}
|
||||
# shellcheck disable=SC2329
|
||||
handle_interrupt() {
|
||||
@@ -618,7 +640,7 @@ select_purge_categories() {
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
if [[ ${selected[i]} == true ]]; then
|
||||
selected_size=$((selected_size + ${sizes[i]:-0}))
|
||||
((selected_count++))
|
||||
selected_count=$((selected_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -633,7 +655,6 @@ select_purge_categories() {
|
||||
scroll_indicator=" ${GRAY}[${current_pos}/${total_items}]${NC}"
|
||||
fi
|
||||
|
||||
printf "%s\n" "$clear_line"
|
||||
printf "%s${PURPLE_BOLD}Select Categories to Clean${NC}%s${GRAY}, ${selected_size_human}, ${selected_count} selected${NC}\n" "$clear_line" "$scroll_indicator"
|
||||
printf "%s\n" "$clear_line"
|
||||
|
||||
@@ -656,15 +677,42 @@ select_purge_categories() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Fill empty slots to clear previous content
|
||||
local items_shown=$visible_count
|
||||
for ((i = items_shown; i < items_per_page; i++)); do
|
||||
printf "%s\n" "$clear_line"
|
||||
done
|
||||
|
||||
# Keep one blank line between the list and footer tips.
|
||||
printf "%s\n" "$clear_line"
|
||||
|
||||
printf "%s${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}/J/K | Space Select | Enter Confirm | A All | I Invert | Q Quit${NC}\n" "$clear_line"
|
||||
# Adaptive footer hints — mirrors menu_paginated.sh pattern
|
||||
local _term_w
|
||||
_term_w=$(tput cols 2> /dev/null || echo 80)
|
||||
[[ "$_term_w" =~ ^[0-9]+$ ]] || _term_w=80
|
||||
|
||||
local _sep=" ${GRAY}|${NC} "
|
||||
local _nav="${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}"
|
||||
local _space="${GRAY}Space Select${NC}"
|
||||
local _enter="${GRAY}Enter Confirm${NC}"
|
||||
local _all="${GRAY}A All${NC}"
|
||||
local _invert="${GRAY}I Invert${NC}"
|
||||
local _quit="${GRAY}Q Quit${NC}"
|
||||
|
||||
# Strip ANSI to measure real length
|
||||
_ph_len() { printf "%s" "$1" | LC_ALL=C awk '{gsub(/\033\[[0-9;]*[A-Za-z]/,""); printf "%d", length}'; }
|
||||
|
||||
# Level 0 (full): ↑↓ | Space Select | Enter Confirm | A All | I Invert | Q Quit
|
||||
local _full="${_nav}${_sep}${_space}${_sep}${_enter}${_sep}${_all}${_sep}${_invert}${_sep}${_quit}"
|
||||
if (($(_ph_len "$_full") <= _term_w)); then
|
||||
printf "%s${_full}${NC}\n" "$clear_line"
|
||||
else
|
||||
# Level 1: ↑↓ | Enter Confirm | A All | I Invert | Q Quit
|
||||
local _l1="${_nav}${_sep}${_enter}${_sep}${_all}${_sep}${_invert}${_sep}${_quit}"
|
||||
if (($(_ph_len "$_l1") <= _term_w)); then
|
||||
printf "%s${_l1}${NC}\n" "$clear_line"
|
||||
else
|
||||
# Level 2 (minimal): ↑↓ | Enter | Q Quit
|
||||
printf "%s${_nav}${_sep}${_enter}${_sep}${_quit}${NC}\n" "$clear_line"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clear stale content below the footer when list height shrinks.
|
||||
printf '\033[J'
|
||||
}
|
||||
move_cursor_up() {
|
||||
if [[ $cursor_pos -gt 0 ]]; then
|
||||
@@ -680,9 +728,9 @@ select_purge_categories() {
|
||||
local visible_count=$((total_items - top_index))
|
||||
[[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page
|
||||
if [[ $cursor_pos -lt $((visible_count - 1)) ]]; then
|
||||
((cursor_pos++))
|
||||
cursor_pos=$((cursor_pos + 1))
|
||||
elif [[ $((top_index + visible_count)) -lt $total_items ]]; then
|
||||
((top_index++))
|
||||
top_index=$((top_index + 1))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -767,6 +815,48 @@ select_purge_categories() {
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Final confirmation before deleting selected purge artifacts.
|
||||
confirm_purge_cleanup() {
|
||||
local item_count="${1:-0}"
|
||||
local total_size_kb="${2:-0}"
|
||||
local unknown_count="${3:-0}"
|
||||
|
||||
[[ "$item_count" =~ ^[0-9]+$ ]] || item_count=0
|
||||
[[ "$total_size_kb" =~ ^[0-9]+$ ]] || total_size_kb=0
|
||||
[[ "$unknown_count" =~ ^[0-9]+$ ]] || unknown_count=0
|
||||
|
||||
local item_text="artifact"
|
||||
[[ $item_count -ne 1 ]] && item_text="artifacts"
|
||||
|
||||
local size_display
|
||||
size_display=$(bytes_to_human "$((total_size_kb * 1024))")
|
||||
|
||||
local unknown_hint=""
|
||||
if [[ $unknown_count -gt 0 ]]; then
|
||||
local unknown_text="unknown size"
|
||||
[[ $unknown_count -gt 1 ]] && unknown_text="unknown sizes"
|
||||
unknown_hint=", ${unknown_count} ${unknown_text}"
|
||||
fi
|
||||
|
||||
echo -ne "${PURPLE}${ICON_ARROW}${NC} Remove ${item_count} ${item_text}, ${size_display}${unknown_hint} ${GREEN}Enter${NC} confirm, ${GRAY}ESC${NC} cancel: "
|
||||
drain_pending_input
|
||||
local key=""
|
||||
IFS= read -r -s -n1 key || key=""
|
||||
drain_pending_input
|
||||
|
||||
case "$key" in
|
||||
"" | $'\n' | $'\r' | y | Y)
|
||||
echo ""
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main cleanup function - scans and prompts user to select artifacts to clean
|
||||
clean_project_artifacts() {
|
||||
local -a all_found_items=()
|
||||
@@ -825,8 +915,6 @@ clean_project_artifacts() {
|
||||
# Give monitor process time to exit and clear its output
|
||||
if [[ -t 1 ]]; then
|
||||
sleep 0.2
|
||||
# Clear the scanning line but preserve the title
|
||||
printf '\n\033[K'
|
||||
fi
|
||||
|
||||
# Collect all results
|
||||
@@ -1041,32 +1129,57 @@ clean_project_artifacts() {
|
||||
echo "$artifact_name"
|
||||
fi
|
||||
}
|
||||
# Format display with alignment (like app_selector)
|
||||
# Format display with alignment (mirrors app_selector.sh approach)
|
||||
# Args: $1=project_path $2=artifact_type $3=size_str $4=terminal_width $5=max_path_width $6=artifact_col_width
|
||||
format_purge_display() {
|
||||
local project_path="$1"
|
||||
local artifact_type="$2"
|
||||
local size_str="$3"
|
||||
# Terminal width for alignment
|
||||
local terminal_width=$(tput cols 2> /dev/null || echo 80)
|
||||
local fixed_width=32 # Reserve for size and artifact type (9 + 3 + 20)
|
||||
local available_width=$((terminal_width - fixed_width))
|
||||
# Bounds: 30 chars min, but cap at 70% of terminal width to preserve aesthetics
|
||||
local max_aesthetic_width=$((terminal_width * 70 / 100))
|
||||
[[ $available_width -gt $max_aesthetic_width ]] && available_width=$max_aesthetic_width
|
||||
[[ $available_width -lt 30 ]] && available_width=30
|
||||
local terminal_width="${4:-$(tput cols 2> /dev/null || echo 80)}"
|
||||
local max_path_width="${5:-}"
|
||||
local artifact_col="${6:-12}"
|
||||
local available_width
|
||||
|
||||
if [[ -n "$max_path_width" ]]; then
|
||||
available_width="$max_path_width"
|
||||
else
|
||||
# Standalone fallback: overhead = prefix(4)+space(1)+size(9)+sep(3)+artifact_col+recent(9) = artifact_col+26
|
||||
local fixed_width=$((artifact_col + 26))
|
||||
available_width=$((terminal_width - fixed_width))
|
||||
|
||||
local min_width=10
|
||||
if [[ $terminal_width -ge 120 ]]; then
|
||||
min_width=48
|
||||
elif [[ $terminal_width -ge 100 ]]; then
|
||||
min_width=38
|
||||
elif [[ $terminal_width -ge 80 ]]; then
|
||||
min_width=25
|
||||
fi
|
||||
|
||||
[[ $available_width -lt $min_width ]] && available_width=$min_width
|
||||
[[ $available_width -gt 60 ]] && available_width=60
|
||||
fi
|
||||
|
||||
# Truncate project path if needed
|
||||
local truncated_path=$(truncate_by_display_width "$project_path" "$available_width")
|
||||
local current_width=$(get_display_width "$truncated_path")
|
||||
local truncated_path
|
||||
truncated_path=$(truncate_by_display_width "$project_path" "$available_width")
|
||||
local current_width
|
||||
current_width=$(get_display_width "$truncated_path")
|
||||
local char_count=${#truncated_path}
|
||||
local padding=$((available_width - current_width))
|
||||
local printf_width=$((char_count + padding))
|
||||
# Format: "project_path size | artifact_type"
|
||||
printf "%-*s %9s | %-17s" "$printf_width" "$truncated_path" "$size_str" "$artifact_type"
|
||||
printf "%-*s %9s | %-*s" "$printf_width" "$truncated_path" "$size_str" "$artifact_col" "$artifact_type"
|
||||
}
|
||||
# Build menu options - one line per artifact
|
||||
# Pass 1: collect data into parallel arrays (needed for pre-scan of widths)
|
||||
local -a raw_project_paths=()
|
||||
local -a raw_artifact_types=()
|
||||
for item in "${safe_to_clean[@]}"; do
|
||||
local project_path=$(get_project_path "$item")
|
||||
local artifact_type=$(get_artifact_display_name "$item")
|
||||
local project_path
|
||||
project_path=$(get_project_path "$item")
|
||||
local artifact_type
|
||||
artifact_type=$(get_artifact_display_name "$item")
|
||||
local size_raw
|
||||
size_raw=$(get_dir_size_kb "$item")
|
||||
local size_kb=0
|
||||
@@ -1095,13 +1208,66 @@ clean_project_artifacts() {
|
||||
break
|
||||
fi
|
||||
done
|
||||
menu_options+=("$(format_purge_display "$project_path" "$artifact_type" "$size_human")")
|
||||
raw_project_paths+=("$project_path")
|
||||
raw_artifact_types+=("$artifact_type")
|
||||
item_paths+=("$item")
|
||||
item_sizes+=("$size_kb")
|
||||
item_size_unknown_flags+=("$size_unknown")
|
||||
item_recent_flags+=("$is_recent")
|
||||
done
|
||||
|
||||
# Pre-scan: find max path and artifact display widths (mirrors app_selector.sh approach)
|
||||
local terminal_width
|
||||
terminal_width=$(tput cols 2> /dev/null || echo 80)
|
||||
[[ "$terminal_width" =~ ^[0-9]+$ ]] || terminal_width=80
|
||||
|
||||
local max_path_display_width=0
|
||||
local max_artifact_width=0
|
||||
for pp in "${raw_project_paths[@]+"${raw_project_paths[@]}"}"; do
|
||||
local w
|
||||
w=$(get_display_width "$pp")
|
||||
[[ $w -gt $max_path_display_width ]] && max_path_display_width=$w
|
||||
done
|
||||
for at in "${raw_artifact_types[@]+"${raw_artifact_types[@]}"}"; do
|
||||
[[ ${#at} -gt $max_artifact_width ]] && max_artifact_width=${#at}
|
||||
done
|
||||
|
||||
# Artifact column: cap at 17, floor at 6 (shortest typical names like "dist")
|
||||
[[ $max_artifact_width -lt 6 ]] && max_artifact_width=6
|
||||
[[ $max_artifact_width -gt 17 ]] && max_artifact_width=17
|
||||
|
||||
# Exact overhead: prefix(4) + space(1) + size(9) + " | "(3) + artifact_col + " | Recent"(9) = artifact_col + 26
|
||||
local fixed_overhead=$((max_artifact_width + 26))
|
||||
local available_for_path=$((terminal_width - fixed_overhead))
|
||||
|
||||
local min_path_width=10
|
||||
if [[ $terminal_width -ge 120 ]]; then
|
||||
min_path_width=48
|
||||
elif [[ $terminal_width -ge 100 ]]; then
|
||||
min_path_width=38
|
||||
elif [[ $terminal_width -ge 80 ]]; then
|
||||
min_path_width=25
|
||||
fi
|
||||
|
||||
[[ $max_path_display_width -lt $min_path_width ]] && max_path_display_width=$min_path_width
|
||||
[[ $available_for_path -lt $max_path_display_width ]] && max_path_display_width=$available_for_path
|
||||
[[ $max_path_display_width -gt 60 ]] && max_path_display_width=60
|
||||
# Ensure path width is at least 5 on very narrow terminals
|
||||
[[ $max_path_display_width -lt 5 ]] && max_path_display_width=5
|
||||
|
||||
# Pass 2: build menu_options using pre-computed widths
|
||||
for ((idx = 0; idx < ${#raw_project_paths[@]}; idx++)); do
|
||||
local size_kb_val="${item_sizes[idx]}"
|
||||
local size_unknown_val="${item_size_unknown_flags[idx]}"
|
||||
local size_human_val=""
|
||||
if [[ "$size_unknown_val" == "true" ]]; then
|
||||
size_human_val="unknown"
|
||||
else
|
||||
size_human_val=$(bytes_to_human "$((size_kb_val * 1024))")
|
||||
fi
|
||||
menu_options+=("$(format_purge_display "${raw_project_paths[idx]}" "${raw_artifact_types[idx]}" "$size_human_val" "$terminal_width" "$max_path_display_width" "$max_artifact_width")")
|
||||
done
|
||||
|
||||
# Sort by size descending (largest first) - requested in issue #311
|
||||
# Use external sort for better performance with many items
|
||||
if [[ ${#item_sizes[@]} -gt 0 ]]; then
|
||||
@@ -1147,11 +1313,11 @@ clean_project_artifacts() {
|
||||
# Set global vars for selector
|
||||
export PURGE_CATEGORY_SIZES=$(
|
||||
IFS=,
|
||||
echo "${item_sizes[*]}"
|
||||
echo "${item_sizes[*]-}"
|
||||
)
|
||||
export PURGE_RECENT_CATEGORIES=$(
|
||||
IFS=,
|
||||
echo "${item_recent_flags[*]}"
|
||||
echo "${item_recent_flags[*]-}"
|
||||
)
|
||||
# Interactive selection (only if terminal is available)
|
||||
PURGE_SELECTION_RESULT=""
|
||||
@@ -1176,11 +1342,32 @@ clean_project_artifacts() {
|
||||
unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT
|
||||
return 0
|
||||
fi
|
||||
IFS=',' read -r -a selected_indices <<< "$PURGE_SELECTION_RESULT"
|
||||
local selected_total_kb=0
|
||||
local selected_unknown_count=0
|
||||
for idx in "${selected_indices[@]}"; do
|
||||
local selected_size_kb="${item_sizes[idx]:-0}"
|
||||
[[ "$selected_size_kb" =~ ^[0-9]+$ ]] || selected_size_kb=0
|
||||
selected_total_kb=$((selected_total_kb + selected_size_kb))
|
||||
if [[ "${item_size_unknown_flags[idx]:-false}" == "true" ]]; then
|
||||
selected_unknown_count=$((selected_unknown_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
if ! confirm_purge_cleanup "${#selected_indices[@]}" "$selected_total_kb" "$selected_unknown_count"; then
|
||||
echo -e "${GRAY}Purge cancelled${NC}"
|
||||
printf '\n'
|
||||
unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean selected items
|
||||
echo ""
|
||||
IFS=',' read -r -a selected_indices <<< "$PURGE_SELECTION_RESULT"
|
||||
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
||||
local cleaned_count=0
|
||||
local dry_run_mode="${MOLE_DRY_RUN:-0}"
|
||||
for idx in "${selected_indices[@]}"; do
|
||||
local item_path="${item_paths[idx]}"
|
||||
local artifact_type=$(basename "$item_path")
|
||||
@@ -1200,17 +1387,27 @@ clean_project_artifacts() {
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Cleaning $project_path/$artifact_type..."
|
||||
fi
|
||||
local removal_recorded=false
|
||||
if [[ -e "$item_path" ]]; then
|
||||
safe_remove "$item_path" true
|
||||
if [[ ! -e "$item_path" ]]; then
|
||||
local current_total=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
||||
echo "$((current_total + size_kb))" > "$stats_dir/purge_stats"
|
||||
((cleaned_count++))
|
||||
if safe_remove "$item_path" true; then
|
||||
if [[ "$dry_run_mode" == "1" || ! -e "$item_path" ]]; then
|
||||
local current_total
|
||||
current_total=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
||||
echo "$((current_total + size_kb))" > "$stats_dir/purge_stats"
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
removal_recorded=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}"
|
||||
if [[ "$removal_recorded" == "true" ]]; then
|
||||
if [[ "$dry_run_mode" == "1" ]]; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} [DRY RUN] $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}"
|
||||
else
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
# Update count
|
||||
|
||||
@@ -5,6 +5,7 @@ set -euo pipefail
|
||||
clean_deep_system() {
|
||||
stop_section_spinner
|
||||
local cache_cleaned=0
|
||||
start_section_spinner "Cleaning system caches..."
|
||||
# Optimized: Single pass for /Library/Caches (3 patterns in 1 scan)
|
||||
if sudo test -d "/Library/Caches" 2> /dev/null; then
|
||||
while IFS= read -r -d '' file; do
|
||||
@@ -20,6 +21,7 @@ clean_deep_system() {
|
||||
\( -name "*.log" -mtime "+$MOLE_LOG_AGE_DAYS" \) \
|
||||
\) -print0 2> /dev/null || true)
|
||||
fi
|
||||
stop_section_spinner
|
||||
[[ $cache_cleaned -eq 1 ]] && log_success "System caches"
|
||||
start_section_spinner "Cleaning system temporary files..."
|
||||
local tmp_cleaned=0
|
||||
@@ -84,7 +86,7 @@ clean_deep_system() {
|
||||
continue
|
||||
fi
|
||||
if safe_sudo_remove "$item"; then
|
||||
((updates_cleaned++))
|
||||
updates_cleaned=$((updates_cleaned + 1))
|
||||
fi
|
||||
done < <(find /Library/Updates -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
stop_section_spinner
|
||||
@@ -141,28 +143,33 @@ clean_deep_system() {
|
||||
debug_log "Cleaning macOS installer: $app_name, $size_human, ${age_days} days old"
|
||||
if safe_sudo_remove "$installer_app"; then
|
||||
log_success "$app_name, $size_human"
|
||||
((installer_cleaned++))
|
||||
installer_cleaned=$((installer_cleaned + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
stop_section_spinner
|
||||
[[ $installer_cleaned -gt 0 ]] && debug_log "Cleaned $installer_cleaned macOS installer(s)"
|
||||
start_section_spinner "Scanning system caches..."
|
||||
start_section_spinner "Scanning browser code signature caches..."
|
||||
local code_sign_cleaned=0
|
||||
while IFS= read -r -d '' cache_dir; do
|
||||
if safe_sudo_remove "$cache_dir"; then
|
||||
((code_sign_cleaned++))
|
||||
code_sign_cleaned=$((code_sign_cleaned + 1))
|
||||
fi
|
||||
done < <(run_with_timeout 5 command find /private/var/folders -type d -name "*.code_sign_clone" -path "*/X/*" -print0 2> /dev/null || true)
|
||||
stop_section_spinner
|
||||
[[ $code_sign_cleaned -gt 0 ]] && log_success "Browser code signature caches, $code_sign_cleaned items"
|
||||
|
||||
local diag_base="/private/var/db/diagnostics"
|
||||
start_section_spinner "Cleaning system diagnostic logs..."
|
||||
safe_sudo_find_delete "$diag_base" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "$diag_base" "*.tracev3" "30" "f" || true
|
||||
safe_sudo_find_delete "/private/var/db/DiagnosticPipeline" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
stop_section_spinner
|
||||
log_success "System diagnostic logs"
|
||||
|
||||
start_section_spinner "Cleaning power logs..."
|
||||
safe_sudo_find_delete "/private/var/db/powerlog" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
stop_section_spinner
|
||||
log_success "Power logs"
|
||||
start_section_spinner "Cleaning memory exception reports..."
|
||||
local mem_reports_dir="/private/var/db/reportmemoryexception/MemoryLimitViolations"
|
||||
@@ -171,15 +178,16 @@ clean_deep_system() {
|
||||
# Count and size old files before deletion
|
||||
local file_count=0
|
||||
local total_size_kb=0
|
||||
while IFS= read -r -d '' file; do
|
||||
((file_count++))
|
||||
local file_size
|
||||
file_size=$(sudo stat -f%z "$file" 2> /dev/null || echo "0")
|
||||
((total_size_kb += file_size / 1024))
|
||||
done < <(sudo find "$mem_reports_dir" -type f -mtime +30 -print0 2> /dev/null || true)
|
||||
local total_bytes=0
|
||||
local stats_out
|
||||
stats_out=$(sudo find "$mem_reports_dir" -type f -mtime +30 -exec stat -f "%z" {} + 2> /dev/null | awk '{c++; s+=$1} END {print c+0, s+0}' || true)
|
||||
if [[ -n "$stats_out" ]]; then
|
||||
read -r file_count total_bytes <<< "$stats_out"
|
||||
total_size_kb=$((total_bytes / 1024))
|
||||
fi
|
||||
|
||||
if [[ "$file_count" -gt 0 ]]; then
|
||||
if [[ "${DRY_RUN:-false}" != "true" ]]; then
|
||||
if [[ "${DRY_RUN:-}" != "true" ]]; then
|
||||
if safe_sudo_find_delete "$mem_reports_dir" "*" "30" "f"; then
|
||||
mem_cleaned=1
|
||||
fi
|
||||
@@ -207,6 +215,11 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
# Fast pre-check: skip entirely if Time Machine is not configured (no tmutil needed)
|
||||
if ! defaults read /Library/Preferences/com.apple.TimeMachine AutoBackup 2> /dev/null | grep -qE '^[01]$'; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Checking Time Machine configuration..."
|
||||
local spinner_active=true
|
||||
local tm_info
|
||||
@@ -287,7 +300,7 @@ clean_time_machine_failed_backups() {
|
||||
size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Incomplete backup: $backup_name${NC}, ${YELLOW}$size_human dry${NC}"
|
||||
((tm_cleaned++))
|
||||
tm_cleaned=$((tm_cleaned + 1))
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
@@ -297,10 +310,10 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete backup: $backup_name${NC}, ${GREEN}$size_human${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
tm_cleaned=$((tm_cleaned + 1))
|
||||
files_cleaned=$((files_cleaned + 1))
|
||||
total_size_cleaned=$((total_size_cleaned + size_kb))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} Could not delete: $backup_name · try manually with sudo"
|
||||
@@ -339,7 +352,7 @@ clean_time_machine_failed_backups() {
|
||||
size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Incomplete APFS backup in $bundle_name: $backup_name${NC}, ${YELLOW}$size_human dry${NC}"
|
||||
((tm_cleaned++))
|
||||
tm_cleaned=$((tm_cleaned + 1))
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
@@ -348,10 +361,10 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete APFS backup in $bundle_name: $backup_name${NC}, ${GREEN}$size_human${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
tm_cleaned=$((tm_cleaned + 1))
|
||||
files_cleaned=$((files_cleaned + 1))
|
||||
total_size_cleaned=$((total_size_cleaned + size_kb))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} Could not delete from bundle: $backup_name"
|
||||
@@ -388,6 +401,10 @@ clean_local_snapshots() {
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
# Fast pre-check: skip entirely if Time Machine is not configured (no tmutil needed)
|
||||
if ! defaults read /Library/Preferences/com.apple.TimeMachine AutoBackup 2> /dev/null | grep -qE '^[01]$'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
start_section_spinner "Checking Time Machine status..."
|
||||
local rc_running=0
|
||||
|
||||
@@ -23,7 +23,7 @@ clean_user_essentials() {
|
||||
local cleaned_count=0
|
||||
while IFS= read -r -d '' item; do
|
||||
if safe_remove "$item" true; then
|
||||
((cleaned_count++))
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
fi
|
||||
done < <(command find "$HOME/.Trash" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
if [[ $cleaned_count -gt 0 ]]; then
|
||||
@@ -76,8 +76,13 @@ _clean_mail_downloads() {
|
||||
)
|
||||
local count=0
|
||||
local cleaned_kb=0
|
||||
local spinner_active=false
|
||||
for target_path in "${mail_dirs[@]}"; do
|
||||
if [[ -d "$target_path" ]]; then
|
||||
if [[ "$spinner_active" == "false" && -t 1 ]]; then
|
||||
start_section_spinner "Cleaning old Mail attachments..."
|
||||
spinner_active=true
|
||||
fi
|
||||
local dir_size_kb=0
|
||||
dir_size_kb=$(get_path_size_kb "$target_path")
|
||||
if ! [[ "$dir_size_kb" =~ ^[0-9]+$ ]]; then
|
||||
@@ -95,13 +100,16 @@ _clean_mail_downloads() {
|
||||
local file_size_kb
|
||||
file_size_kb=$(get_path_size_kb "$file_path")
|
||||
if safe_remove "$file_path" true; then
|
||||
((count++))
|
||||
((cleaned_kb += file_size_kb))
|
||||
count=$((count + 1))
|
||||
cleaned_kb=$((cleaned_kb + file_size_kb))
|
||||
fi
|
||||
fi
|
||||
done < <(command find "$target_path" -type f -mtime +"$mail_age_days" -print0 2> /dev/null || true)
|
||||
fi
|
||||
done
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
if [[ $count -gt 0 ]]; then
|
||||
local cleaned_mb
|
||||
cleaned_mb=$(echo "$cleaned_kb" | awk '{printf "%.1f", $1/1024}' || echo "0.0")
|
||||
@@ -163,7 +171,7 @@ clean_chrome_old_versions() {
|
||||
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||
size_kb="${size_kb:-0}"
|
||||
total_size=$((total_size + size_kb))
|
||||
((cleaned_count++))
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
cleaned_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if has_sudo_session; then
|
||||
@@ -183,9 +191,9 @@ clean_chrome_old_versions() {
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Chrome old versions${NC}, ${GREEN}${cleaned_count} dirs, $size_human${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + cleaned_count))
|
||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
@@ -249,7 +257,7 @@ clean_edge_old_versions() {
|
||||
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||
size_kb="${size_kb:-0}"
|
||||
total_size=$((total_size + size_kb))
|
||||
((cleaned_count++))
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
cleaned_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if has_sudo_session; then
|
||||
@@ -269,9 +277,9 @@ clean_edge_old_versions() {
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Edge old versions${NC}, ${GREEN}${cleaned_count} dirs, $size_human${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + cleaned_count))
|
||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
@@ -316,7 +324,7 @@ clean_edge_updater_old_versions() {
|
||||
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||
size_kb="${size_kb:-0}"
|
||||
total_size=$((total_size + size_kb))
|
||||
((cleaned_count++))
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
cleaned_any=true
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$dir" true > /dev/null 2>&1 || true
|
||||
@@ -331,9 +339,9 @@ clean_edge_updater_old_versions() {
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Edge updater old versions${NC}, ${GREEN}${cleaned_count} dirs, $size_human${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + cleaned_count))
|
||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
@@ -387,6 +395,7 @@ scan_external_volumes() {
|
||||
done
|
||||
stop_section_spinner
|
||||
}
|
||||
|
||||
# Finder metadata (.DS_Store).
|
||||
clean_finder_metadata() {
|
||||
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
|
||||
@@ -411,14 +420,17 @@ clean_support_app_data() {
|
||||
safe_find_delete "$idle_assets_dir" "*" "$support_age_days" "f" || true
|
||||
fi
|
||||
|
||||
# Clean old aerial wallpaper videos (can be large, safe to remove).
|
||||
safe_clean ~/Library/Application\ Support/com.apple.wallpaper/aerials/videos/* "Aerial wallpaper videos"
|
||||
|
||||
# Do not touch Messages attachments, only preview/sticker caches.
|
||||
if pgrep -x "Messages" > /dev/null 2>&1; then
|
||||
echo -e " ${GRAY}${ICON_WARNING}${NC} Messages is running · preview cache cleanup skipped"
|
||||
return 0
|
||||
else
|
||||
safe_clean ~/Library/Messages/StickerCache/* "Messages sticker cache"
|
||||
safe_clean ~/Library/Messages/Caches/Previews/Attachments/* "Messages preview attachment cache"
|
||||
safe_clean ~/Library/Messages/Caches/Previews/StickerCache/* "Messages preview sticker cache"
|
||||
fi
|
||||
safe_clean ~/Library/Messages/StickerCache/* "Messages sticker cache"
|
||||
safe_clean ~/Library/Messages/Caches/Previews/Attachments/* "Messages preview attachment cache"
|
||||
safe_clean ~/Library/Messages/Caches/Previews/StickerCache/* "Messages preview sticker cache"
|
||||
}
|
||||
|
||||
# App caches (merged: macOS system caches + Sandboxed apps).
|
||||
@@ -472,14 +484,15 @@ clean_app_caches() {
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches${NC}, ${GREEN}$size_human${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + cleaned_count))
|
||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
|
||||
clean_group_container_caches
|
||||
}
|
||||
|
||||
# Process a single container cache directory.
|
||||
process_container_cache() {
|
||||
local container_dir="$1"
|
||||
@@ -500,9 +513,9 @@ process_container_cache() {
|
||||
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
local size
|
||||
size=$(get_path_size_kb "$cache_dir")
|
||||
((total_size += size))
|
||||
total_size=$((total_size + size))
|
||||
found_any=true
|
||||
((cleaned_count++))
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
local item
|
||||
while IFS= read -r -d '' item; do
|
||||
@@ -600,7 +613,7 @@ clean_group_container_caches() {
|
||||
item_size=$(get_path_size_kb "$item" 2> /dev/null) || item_size=0
|
||||
[[ "$item_size" =~ ^[0-9]+$ ]] || item_size=0
|
||||
candidate_changed=true
|
||||
((candidate_size_kb += item_size))
|
||||
candidate_size_kb=$((candidate_size_kb + item_size))
|
||||
done
|
||||
else
|
||||
for item in "${items_to_clean[@]}"; do
|
||||
@@ -609,14 +622,14 @@ clean_group_container_caches() {
|
||||
[[ "$item_size" =~ ^[0-9]+$ ]] || item_size=0
|
||||
if safe_remove "$item" true 2> /dev/null; then
|
||||
candidate_changed=true
|
||||
((candidate_size_kb += item_size))
|
||||
candidate_size_kb=$((candidate_size_kb + item_size))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$candidate_changed" == "true" ]]; then
|
||||
((total_size += candidate_size_kb))
|
||||
((cleaned_count++))
|
||||
total_size=$((total_size + candidate_size_kb))
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
found_any=true
|
||||
fi
|
||||
done
|
||||
@@ -632,12 +645,13 @@ clean_group_container_caches() {
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Group Containers logs/caches${NC}, ${GREEN}$size_human${NC}"
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + cleaned_count))
|
||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
|
||||
# Browser caches (Safari/Chrome/Edge/Firefox).
|
||||
clean_browsers() {
|
||||
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
|
||||
@@ -683,6 +697,7 @@ clean_browsers() {
|
||||
clean_edge_old_versions
|
||||
clean_edge_updater_old_versions
|
||||
}
|
||||
|
||||
# Cloud storage caches.
|
||||
clean_cloud_storage() {
|
||||
safe_clean ~/Library/Caches/com.dropbox.* "Dropbox cache"
|
||||
@@ -693,6 +708,7 @@ clean_cloud_storage() {
|
||||
safe_clean ~/Library/Caches/com.box.desktop "Box cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.OneDrive "OneDrive cache"
|
||||
}
|
||||
|
||||
# Office app caches.
|
||||
clean_office_applications() {
|
||||
safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache"
|
||||
@@ -704,6 +720,7 @@ clean_office_applications() {
|
||||
safe_clean ~/Library/Caches/org.mozilla.thunderbird/* "Thunderbird cache"
|
||||
safe_clean ~/Library/Caches/com.apple.mail/* "Apple Mail cache"
|
||||
}
|
||||
|
||||
# Virtualization caches.
|
||||
clean_virtualization_tools() {
|
||||
stop_section_spinner
|
||||
@@ -712,6 +729,47 @@ clean_virtualization_tools() {
|
||||
safe_clean ~/VirtualBox\ VMs/.cache "VirtualBox cache"
|
||||
safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files"
|
||||
}
|
||||
|
||||
# Estimate item size for Application Support cleanup.
|
||||
# Files use stat; directories use du with timeout to avoid long blocking scans.
|
||||
app_support_item_size_bytes() {
|
||||
local item="$1"
|
||||
local timeout_seconds="${2:-0.4}"
|
||||
|
||||
if [[ -f "$item" && ! -L "$item" ]]; then
|
||||
local file_bytes
|
||||
file_bytes=$(stat -f%z "$item" 2> /dev/null || echo "0")
|
||||
[[ "$file_bytes" =~ ^[0-9]+$ ]] || return 1
|
||||
printf '%s\n' "$file_bytes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -d "$item" && ! -L "$item" ]]; then
|
||||
local du_tmp
|
||||
du_tmp=$(mktemp)
|
||||
local du_status=0
|
||||
if run_with_timeout "$timeout_seconds" du -skP "$item" > "$du_tmp" 2> /dev/null; then
|
||||
du_status=0
|
||||
else
|
||||
du_status=$?
|
||||
fi
|
||||
|
||||
if [[ $du_status -ne 0 ]]; then
|
||||
rm -f "$du_tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local size_kb
|
||||
size_kb=$(awk 'NR==1 {print $1; exit}' "$du_tmp")
|
||||
rm -f "$du_tmp"
|
||||
[[ "$size_kb" =~ ^[0-9]+$ ]] || return 1
|
||||
printf '%s\n' "$((size_kb * 1024))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Application Support logs/caches.
|
||||
clean_application_support_logs() {
|
||||
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then
|
||||
@@ -721,14 +779,27 @@ clean_application_support_logs() {
|
||||
fi
|
||||
start_section_spinner "Scanning Application Support..."
|
||||
local total_size_bytes=0
|
||||
local total_size_partial=false
|
||||
local cleaned_count=0
|
||||
local found_any=false
|
||||
local size_timeout_seconds="${MOLE_APP_SUPPORT_ITEM_SIZE_TIMEOUT_SEC:-0.4}"
|
||||
if [[ ! "$size_timeout_seconds" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
|
||||
size_timeout_seconds=0.4
|
||||
fi
|
||||
# Enable nullglob for safe globbing.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
local app_count=0
|
||||
local total_apps
|
||||
# Temporarily disable pipefail here so that a partial find failure (e.g. TCC
|
||||
# restrictions on macOS 26+) does not propagate through the pipeline and abort
|
||||
# the whole scan via set -e.
|
||||
local pipefail_was_set=false
|
||||
if [[ -o pipefail ]]; then
|
||||
pipefail_was_set=true
|
||||
set +o pipefail
|
||||
fi
|
||||
total_apps=$(command find "$HOME/Library/Application Support" -mindepth 1 -maxdepth 1 -type d 2> /dev/null | wc -l | tr -d ' ')
|
||||
[[ "$total_apps" =~ ^[0-9]+$ ]] || total_apps=0
|
||||
local last_progress_update
|
||||
@@ -737,7 +808,7 @@ clean_application_support_logs() {
|
||||
[[ -d "$app_dir" ]] || continue
|
||||
local app_name
|
||||
app_name=$(basename "$app_dir")
|
||||
((app_count++))
|
||||
app_count=$((app_count + 1))
|
||||
update_progress_if_needed "$app_count" "$total_apps" last_progress_update 1 || true
|
||||
local app_name_lower
|
||||
app_name_lower=$(echo "$app_name" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
||||
@@ -758,22 +829,34 @@ clean_application_support_logs() {
|
||||
if [[ -d "$candidate" ]]; then
|
||||
local item_found=false
|
||||
local candidate_size_bytes=0
|
||||
local candidate_size_partial=false
|
||||
local candidate_item_count=0
|
||||
while IFS= read -r -d '' item; do
|
||||
[[ -e "$item" ]] || continue
|
||||
item_found=true
|
||||
((candidate_item_count++))
|
||||
if [[ -f "$item" && ! -L "$item" ]]; then
|
||||
local bytes
|
||||
bytes=$(stat -f%z "$item" 2> /dev/null || echo "0")
|
||||
[[ "$bytes" =~ ^[0-9]+$ ]] && ((candidate_size_bytes += bytes)) || true
|
||||
candidate_item_count=$((candidate_item_count + 1))
|
||||
if [[ ! -L "$item" && (-f "$item" || -d "$item") ]]; then
|
||||
local item_size_bytes=""
|
||||
if item_size_bytes=$(app_support_item_size_bytes "$item" "$size_timeout_seconds"); then
|
||||
if [[ "$item_size_bytes" =~ ^[0-9]+$ ]]; then
|
||||
candidate_size_bytes=$((candidate_size_bytes + item_size_bytes))
|
||||
else
|
||||
candidate_size_partial=true
|
||||
fi
|
||||
else
|
||||
candidate_size_partial=true
|
||||
fi
|
||||
fi
|
||||
if ((candidate_item_count % 250 == 0)); then
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
if [[ "$current_time" =~ ^[0-9]+$ ]] && ((current_time - last_progress_update >= 1)); then
|
||||
local app_label="$app_name"
|
||||
if [[ ${#app_label} -gt 24 ]]; then
|
||||
app_label="${app_label:0:21}..."
|
||||
fi
|
||||
stop_section_spinner
|
||||
start_section_spinner "Scanning Application Support... $app_count/$total_apps ($app_name: $candidate_item_count items)"
|
||||
start_section_spinner "Scanning Application Support... $app_count/$total_apps [$app_label, $candidate_item_count items]"
|
||||
last_progress_update=$current_time
|
||||
fi
|
||||
fi
|
||||
@@ -782,8 +865,9 @@ clean_application_support_logs() {
|
||||
fi
|
||||
done < <(command find "$candidate" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
if [[ "$item_found" == "true" ]]; then
|
||||
((total_size_bytes += candidate_size_bytes))
|
||||
((cleaned_count++))
|
||||
total_size_bytes=$((total_size_bytes + candidate_size_bytes))
|
||||
[[ "$candidate_size_partial" == "true" ]] && total_size_partial=true
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
found_any=true
|
||||
fi
|
||||
fi
|
||||
@@ -800,22 +884,34 @@ clean_application_support_logs() {
|
||||
if [[ -d "$candidate" ]]; then
|
||||
local item_found=false
|
||||
local candidate_size_bytes=0
|
||||
local candidate_size_partial=false
|
||||
local candidate_item_count=0
|
||||
while IFS= read -r -d '' item; do
|
||||
[[ -e "$item" ]] || continue
|
||||
item_found=true
|
||||
((candidate_item_count++))
|
||||
if [[ -f "$item" && ! -L "$item" ]]; then
|
||||
local bytes
|
||||
bytes=$(stat -f%z "$item" 2> /dev/null || echo "0")
|
||||
[[ "$bytes" =~ ^[0-9]+$ ]] && ((candidate_size_bytes += bytes)) || true
|
||||
candidate_item_count=$((candidate_item_count + 1))
|
||||
if [[ ! -L "$item" && (-f "$item" || -d "$item") ]]; then
|
||||
local item_size_bytes=""
|
||||
if item_size_bytes=$(app_support_item_size_bytes "$item" "$size_timeout_seconds"); then
|
||||
if [[ "$item_size_bytes" =~ ^[0-9]+$ ]]; then
|
||||
candidate_size_bytes=$((candidate_size_bytes + item_size_bytes))
|
||||
else
|
||||
candidate_size_partial=true
|
||||
fi
|
||||
else
|
||||
candidate_size_partial=true
|
||||
fi
|
||||
fi
|
||||
if ((candidate_item_count % 250 == 0)); then
|
||||
local current_time
|
||||
current_time=$(get_epoch_seconds)
|
||||
if [[ "$current_time" =~ ^[0-9]+$ ]] && ((current_time - last_progress_update >= 1)); then
|
||||
local container_label="$container"
|
||||
if [[ ${#container_label} -gt 24 ]]; then
|
||||
container_label="${container_label:0:21}..."
|
||||
fi
|
||||
stop_section_spinner
|
||||
start_section_spinner "Scanning Application Support... group container ($container: $candidate_item_count items)"
|
||||
start_section_spinner "Scanning Application Support... group [$container_label, $candidate_item_count items]"
|
||||
last_progress_update=$current_time
|
||||
fi
|
||||
fi
|
||||
@@ -824,13 +920,18 @@ clean_application_support_logs() {
|
||||
fi
|
||||
done < <(command find "$candidate" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
if [[ "$item_found" == "true" ]]; then
|
||||
((total_size_bytes += candidate_size_bytes))
|
||||
((cleaned_count++))
|
||||
total_size_bytes=$((total_size_bytes + candidate_size_bytes))
|
||||
[[ "$candidate_size_partial" == "true" ]] && total_size_partial=true
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
found_any=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Restore pipefail if it was previously set
|
||||
if [[ "$pipefail_was_set" == "true" ]]; then
|
||||
set -o pipefail
|
||||
fi
|
||||
eval "$_ng_state"
|
||||
stop_section_spinner
|
||||
if [[ "$found_any" == "true" ]]; then
|
||||
@@ -838,13 +939,21 @@ clean_application_support_logs() {
|
||||
size_human=$(bytes_to_human "$total_size_bytes")
|
||||
local total_size_kb=$(((total_size_bytes + 1023) / 1024))
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Application Support logs/caches${NC}, ${YELLOW}$size_human dry${NC}"
|
||||
if [[ "$total_size_partial" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Application Support logs/caches${NC}, ${YELLOW}at least $size_human dry${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Application Support logs/caches${NC}, ${YELLOW}$size_human dry${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Application Support logs/caches${NC}, ${GREEN}$size_human${NC}"
|
||||
if [[ "$total_size_partial" == "true" ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Application Support logs/caches${NC}, ${GREEN}at least $size_human${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Application Support logs/caches${NC}, ${GREEN}$size_human${NC}"
|
||||
fi
|
||||
fi
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += total_size_kb))
|
||||
((total_items++))
|
||||
files_cleaned=$((files_cleaned + cleaned_count))
|
||||
total_size_cleaned=$((total_size_cleaned + total_size_kb))
|
||||
total_items=$((total_items + 1))
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
@@ -922,7 +1031,8 @@ check_large_file_candidates() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${SYSTEM_CLEAN:-false}" != "true" ]] && command -v tmutil > /dev/null 2>&1; then
|
||||
if [[ "${SYSTEM_CLEAN:-false}" != "true" ]] && command -v tmutil > /dev/null 2>&1 &&
|
||||
defaults read /Library/Preferences/com.apple.TimeMachine AutoBackup 2> /dev/null | grep -qE '^[01]$'; then
|
||||
local snapshot_list snapshot_count
|
||||
snapshot_list=$(run_with_timeout 3 tmutil listlocalsnapshots / 2> /dev/null || true)
|
||||
if [[ -n "$snapshot_list" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user