1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 12:41:46 +00:00

perf: improve cleanup UI responsiveness and reduce visual flicker

- Speed up spinner animation from 100ms to 50ms for smoother visuals
- Fix spinner flicker by deferring stop until output is ready
- Remove unnecessary 'Preparing...' spinner at section start
- Hide whitelist-protected items from output (Trash, Finder metadata)
- Add spinner feedback for system diagnostic log cleanup
- Remove redundant stop_section_spinner calls in cleanup modules

The cleanup process now feels significantly faster and more polished,
with continuous visual feedback and no jarring gaps between operations.
This commit is contained in:
Tw93
2026-01-17 10:12:23 +08:00
parent b9072c2389
commit e6fc0613d5
4 changed files with 216 additions and 228 deletions

View File

@@ -69,10 +69,10 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
fi
case "$line" in
/ | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*)
WHITELIST_WARNINGS+=("Protected system path: $line")
continue
;;
/ | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*)
WHITELIST_WARNINGS+=("Protected system path: $line")
continue
;;
esac
duplicate="false"
@@ -86,7 +86,7 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
fi
[[ "$duplicate" == "true" ]] && continue
WHITELIST_PATTERNS+=("$line")
done < "$HOME/.config/mole/whitelist"
done <"$HOME/.config/mole/whitelist"
else
WHITELIST_PATTERNS=("${DEFAULT_WHITELIST_PATTERNS[@]}")
fi
@@ -140,7 +140,7 @@ cleanup() {
fi
CLEANUP_DONE=true
stop_inline_spinner 2> /dev/null || true
stop_inline_spinner 2>/dev/null || true
if [[ -t 1 ]]; then
printf "\r\033[K" >&2 || true
@@ -164,14 +164,10 @@ start_section() {
echo ""
echo -e "${PURPLE_BOLD}${ICON_ARROW} $1${NC}"
if [[ -t 1 ]]; then
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Preparing..."
fi
if [[ "$DRY_RUN" == "true" ]]; then
ensure_user_file "$EXPORT_LIST_FILE"
echo "" >> "$EXPORT_LIST_FILE"
echo "=== $1 ===" >> "$EXPORT_LIST_FILE"
echo "" >>"$EXPORT_LIST_FILE"
echo "=== $1 ===" >>"$EXPORT_LIST_FILE"
fi
}
@@ -224,7 +220,7 @@ normalize_paths_for_cleanup() {
done
fi
[[ "$is_child" == "true" ]] || result_paths+=("$path")
done <<< "$sorted_paths"
done <<<"$sorted_paths"
if [[ ${#result_paths[@]} -gt 0 ]]; then
printf '%s\n' "${result_paths[@]}"
@@ -236,9 +232,9 @@ get_cleanup_path_size_kb() {
local path="$1"
if [[ -f "$path" && ! -L "$path" ]]; then
if command -v stat > /dev/null 2>&1; then
if command -v stat >/dev/null 2>&1; then
local bytes
bytes=$(stat -f%z "$path" 2> /dev/null || echo "0")
bytes=$(stat -f%z "$path" 2>/dev/null || echo "0")
if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then
echo $(((bytes + 1023) / 1024))
return 0
@@ -247,9 +243,9 @@ get_cleanup_path_size_kb() {
fi
if [[ -L "$path" ]]; then
if command -v stat > /dev/null 2>&1; then
if command -v stat >/dev/null 2>&1; then
local bytes
bytes=$(stat -f%z "$path" 2> /dev/null || echo "0")
bytes=$(stat -f%z "$path" 2>/dev/null || echo "0")
if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then
echo $(((bytes + 1023) / 1024))
else
@@ -308,9 +304,6 @@ safe_clean() {
return 0
fi
# Always stop spinner before outputting results
stop_section_spinner
local description
local -a targets
@@ -361,6 +354,7 @@ safe_clean() {
local show_scan_feedback=false
if [[ ${#targets[@]} -gt 20 && -t 1 ]]; then
show_scan_feedback=true
stop_section_spinner
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning ${#targets[@]} items..."
fi
@@ -467,9 +461,9 @@ safe_clean() {
[[ ! "$size" =~ ^[0-9]+$ ]] && size=0
if [[ "$size" -gt 0 ]]; then
echo "$size 1" > "$temp_dir/result_${idx}"
echo "$size 1" >"$temp_dir/result_${idx}"
else
echo "0 0" > "$temp_dir/result_${idx}"
echo "0 0" >"$temp_dir/result_${idx}"
fi
((idx++))
@@ -494,17 +488,17 @@ safe_clean() {
[[ ! "$size" =~ ^[0-9]+$ ]] && size=0
local tmp_file="$temp_dir/result_${idx}.$$"
if [[ "$size" -gt 0 ]]; then
echo "$size 1" > "$tmp_file"
echo "$size 1" >"$tmp_file"
else
echo "0 0" > "$tmp_file"
echo "0 0" >"$tmp_file"
fi
mv "$tmp_file" "$temp_dir/result_${idx}" 2> /dev/null || true
mv "$tmp_file" "$temp_dir/result_${idx}" 2>/dev/null || true
) &
pids+=($!)
((idx++))
if ((${#pids[@]} >= MOLE_MAX_PARALLEL_JOBS)); then
wait "${pids[0]}" 2> /dev/null || true
wait "${pids[0]}" 2>/dev/null || true
pids=("${pids[@]:1}")
((completed++))
@@ -517,7 +511,7 @@ safe_clean() {
if [[ ${#pids[@]} -gt 0 ]]; then
for pid in "${pids[@]}"; do
wait "$pid" 2> /dev/null || true
wait "$pid" 2>/dev/null || true
((completed++))
if [[ "$show_spinner" == "true" && -t 1 ]]; then
@@ -533,11 +527,11 @@ safe_clean() {
for path in "${existing_paths[@]}"; do
local result_file="$temp_dir/result_${idx}"
if [[ -f "$result_file" ]]; then
read -r size count < "$result_file" 2> /dev/null || true
read -r size count <"$result_file" 2>/dev/null || true
local removed=0
if [[ "$DRY_RUN" != "true" ]]; then
if [[ -L "$path" ]]; then
rm "$path" 2> /dev/null && removed=1
rm "$path" 2>/dev/null && removed=1
else
if safe_remove "$path" true; then
removed=1
@@ -574,7 +568,7 @@ safe_clean() {
local removed=0
if [[ "$DRY_RUN" != "true" ]]; then
if [[ -L "$path" ]]; then
rm "$path" 2> /dev/null && removed=1
rm "$path" 2>/dev/null && removed=1
else
if safe_remove "$path" true; then
removed=1
@@ -614,6 +608,9 @@ safe_clean() {
fi
if [[ $removed_any -eq 1 ]]; then
# Stop spinner before output
stop_section_spinner
local size_human=$(bytes_to_human "$((total_size_kb * 1024))")
local label="$description"
@@ -632,9 +629,9 @@ safe_clean() {
local size=0
if [[ -n "${temp_dir:-}" && -f "$temp_dir/result_${idx}" ]]; then
read -r size count < "$temp_dir/result_${idx}" 2> /dev/null || true
read -r size count <"$temp_dir/result_${idx}" 2>/dev/null || true
else
size=$(get_cleanup_path_size_kb "$path" 2> /dev/null || echo "0")
size=$(get_cleanup_path_size_kb "$path" 2>/dev/null || echo "0")
fi
[[ "$size" == "0" || -z "$size" ]] && {
@@ -642,7 +639,7 @@ safe_clean() {
continue
}
echo "$(dirname "$path")|$size|$path" >> "$paths_temp"
echo "$(dirname "$path")|$size|$path" >>"$paths_temp"
((idx++))
done
fi
@@ -673,9 +670,9 @@ safe_clean() {
' | while IFS='|' read -r display_path total_size child_count; do
local size_human=$(bytes_to_human "$((total_size * 1024))")
if [[ $child_count -gt 1 ]]; then
echo "$display_path # $size_human ($child_count items)" >> "$EXPORT_LIST_FILE"
echo "$display_path # $size_human ($child_count items)" >>"$EXPORT_LIST_FILE"
else
echo "$display_path # $size_human" >> "$EXPORT_LIST_FILE"
echo "$display_path # $size_human" >>"$EXPORT_LIST_FILE"
fi
done
@@ -711,7 +708,7 @@ start_cleanup() {
SYSTEM_CLEAN=false
ensure_user_file "$EXPORT_LIST_FILE"
cat > "$EXPORT_LIST_FILE" << EOF
cat >"$EXPORT_LIST_FILE" <<EOF
# Mole Cleanup Preview - $(date '+%Y-%m-%d %H:%M:%S')
#
# How to protect files:
@@ -1001,7 +998,7 @@ perform_cleanup() {
echo "# Potential cleanup: ${freed_gb}GB"
echo "# Items: $files_cleaned"
echo "# Categories: $total_items"
} >> "$EXPORT_LIST_FILE"
} >>"$EXPORT_LIST_FILE"
summary_details+=("Detailed file list: ${GRAY}$EXPORT_LIST_FILE${NC}")
summary_details+=("Use ${GRAY}mo clean --whitelist${NC} to add protection rules")
@@ -1050,17 +1047,17 @@ perform_cleanup() {
main() {
for arg in "$@"; do
case "$arg" in
"--debug")
export MO_DEBUG=1
;;
"--dry-run" | "-n")
DRY_RUN=true
;;
"--whitelist")
source "$SCRIPT_DIR/../lib/manage/whitelist.sh"
manage_whitelist "clean"
exit 0
;;
"--debug")
export MO_DEBUG=1
;;
"--dry-run" | "-n")
DRY_RUN=true
;;
"--whitelist")
source "$SCRIPT_DIR/../lib/manage/whitelist.sh"
manage_whitelist "clean"
exit 0
;;
esac
done

View File

@@ -27,14 +27,14 @@ clean_deep_system() {
continue
fi
local item_flags
item_flags=$($STAT_BSD -f%Sf "$item" 2> /dev/null || echo "")
item_flags=$($STAT_BSD -f%Sf "$item" 2>/dev/null || echo "")
if [[ "$item_flags" == *"restricted"* ]]; then
continue
fi
if safe_sudo_remove "$item"; then
((updates_cleaned++))
fi
done < <(find /Library/Updates -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
done < <(find /Library/Updates -mindepth 1 -maxdepth 1 -print0 2>/dev/null || true)
[[ $updates_cleaned -gt 0 ]] && log_success "System library updates"
fi
fi
@@ -76,28 +76,32 @@ clean_deep_system() {
last_update_time=$current_time
fi
fi
done < <(run_with_timeout 5 command find /private/var/folders -type d -name "*.code_sign_clone" -path "*/X/*" -print0 2> /dev/null || true)
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)"
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
safe_sudo_find_delete "/private/var/db/DiagnosticPipeline" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
log_success "System diagnostic logs"
safe_sudo_find_delete "/private/var/db/powerlog" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
log_success "Power logs"
safe_sudo_find_delete "/private/var/db/reportmemoryexception/MemoryLimitViolations" "*" "30" "f" || true
log_success "Memory exception reports"
start_section_spinner "Cleaning diagnostic trace logs..."
local diag_logs_cleaned=0
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*.tracev3" "30" "f" && diag_logs_cleaned=1 || true
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*.tracev3" "30" "f" && diag_logs_cleaned=1 || true
start_section_spinner "Cleaning system diagnostic logs..."
local diag_cleaned=0
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*" "$MOLE_LOG_AGE_DAYS" "f" && diag_cleaned=1 || true
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*" "$MOLE_LOG_AGE_DAYS" "f" && diag_cleaned=1 || true
safe_sudo_find_delete "/private/var/db/DiagnosticPipeline" "*" "$MOLE_LOG_AGE_DAYS" "f" && diag_cleaned=1 || true
safe_sudo_find_delete "/private/var/db/powerlog" "*" "$MOLE_LOG_AGE_DAYS" "f" && diag_cleaned=1 || true
safe_sudo_find_delete "/private/var/db/reportmemoryexception/MemoryLimitViolations" "*" "30" "f" && diag_cleaned=1 || true
stop_section_spinner
[[ $diag_logs_cleaned -eq 1 ]] && log_success "System diagnostic trace logs"
[[ $diag_cleaned -eq 1 ]] && log_success "System diagnostic logs"
start_section_spinner "Cleaning diagnostic trace logs..."
local trace_cleaned=0
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*.tracev3" "30" "f" && trace_cleaned=1 || true
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*.tracev3" "30" "f" && trace_cleaned=1 || true
stop_section_spinner
[[ $trace_cleaned -eq 1 ]] && log_success "System diagnostic trace logs"
}
# Incomplete Time Machine backups.
clean_time_machine_failed_backups() {
local tm_cleaned=0
if ! command -v tmutil > /dev/null 2>&1; then
if ! command -v tmutil >/dev/null 2>&1; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
return 0
fi
@@ -151,9 +155,9 @@ clean_time_machine_failed_backups() {
fi
for volume in "${backup_volumes[@]}"; do
local fs_type
fs_type=$(run_with_timeout 1 command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}' || echo "unknown")
fs_type=$(run_with_timeout 1 command df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}' || echo "unknown")
case "$fs_type" in
nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;;
nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;;
esac
local backupdb_dir="$volume/Backups.backupdb"
if [[ -d "$backupdb_dir" ]]; then
@@ -181,11 +185,11 @@ clean_time_machine_failed_backups() {
note_activity
continue
fi
if ! command -v tmutil > /dev/null 2>&1; then
if ! command -v tmutil >/dev/null 2>&1; then
echo -e " ${YELLOW}!${NC} tmutil not available, skipping: $backup_name"
continue
fi
if tmutil delete "$inprogress_file" 2> /dev/null; then
if tmutil delete "$inprogress_file" 2>/dev/null; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete backup: $backup_name ${GREEN}($size_human)${NC}"
((tm_cleaned++))
((files_cleaned++))
@@ -195,14 +199,14 @@ clean_time_machine_failed_backups() {
else
echo -e " ${YELLOW}!${NC} Could not delete: $backup_name · try manually with sudo"
fi
done < <(run_with_timeout 15 find "$backupdb_dir" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
done < <(run_with_timeout 15 find "$backupdb_dir" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2>/dev/null || true)
fi
# APFS bundles.
for bundle in "$volume"/*.backupbundle "$volume"/*.sparsebundle; do
[[ -e "$bundle" ]] || continue
[[ -d "$bundle" ]] || continue
local bundle_name=$(basename "$bundle")
local mounted_path=$(hdiutil info 2> /dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
local mounted_path=$(hdiutil info 2>/dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then
while IFS= read -r inprogress_file; do
[[ -d "$inprogress_file" ]] || continue
@@ -227,10 +231,10 @@ clean_time_machine_failed_backups() {
note_activity
continue
fi
if ! command -v tmutil > /dev/null 2>&1; then
if ! command -v tmutil >/dev/null 2>&1; then
continue
fi
if tmutil delete "$inprogress_file" 2> /dev/null; then
if tmutil delete "$inprogress_file" 2>/dev/null; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}"
((tm_cleaned++))
((files_cleaned++))
@@ -240,7 +244,7 @@ clean_time_machine_failed_backups() {
else
echo -e " ${YELLOW}!${NC} Could not delete from bundle: $backup_name"
fi
done < <(run_with_timeout 15 find "$mounted_path" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
done < <(run_with_timeout 15 find "$mounted_path" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2>/dev/null || true)
fi
done
done
@@ -256,20 +260,20 @@ clean_time_machine_failed_backups() {
# Returns 2 if status cannot be determined
tm_is_running() {
local st
st="$(tmutil status 2> /dev/null)" || return 2
st="$(tmutil status 2>/dev/null)" || return 2
# If we can't find a Running field at all, treat as unknown.
if ! grep -qE '(^|[[:space:]])("Running"|Running)[[:space:]]*=' <<< "$st"; then
if ! grep -qE '(^|[[:space:]])("Running"|Running)[[:space:]]*=' <<<"$st"; then
return 2
fi
# Match: Running = 1; OR "Running" = 1 (with or without trailing ;)
grep -qE '(^|[[:space:]])("Running"|Running)[[:space:]]*=[[:space:]]*1([[:space:]]*;|$)' <<< "$st"
grep -qE '(^|[[:space:]])("Running"|Running)[[:space:]]*=[[:space:]]*1([[:space:]]*;|$)' <<<"$st"
}
# Local APFS snapshots (keep the most recent).
clean_local_snapshots() {
if ! command -v tmutil > /dev/null 2>&1; then
if ! command -v tmutil >/dev/null 2>&1; then
return 0
fi
@@ -288,7 +292,7 @@ clean_local_snapshots() {
start_section_spinner "Checking local snapshots..."
local snapshot_list
snapshot_list=$(tmutil listlocalsnapshots / 2> /dev/null)
snapshot_list=$(tmutil listlocalsnapshots / 2>/dev/null)
stop_section_spinner
[[ -z "$snapshot_list" ]] && return 0
local cleaned_count=0
@@ -301,14 +305,14 @@ clean_local_snapshots() {
local snap_name="${BASH_REMATCH[0]}"
snapshots+=("$snap_name")
local date_str="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]} ${BASH_REMATCH[4]:0:2}:${BASH_REMATCH[4]:2:2}:${BASH_REMATCH[4]:4:2}"
local snap_ts=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%s" 2> /dev/null || echo "0")
local snap_ts=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%s" 2>/dev/null || echo "0")
[[ "$snap_ts" == "0" ]] && continue
if [[ "$snap_ts" -gt "$newest_ts" ]]; then
newest_ts="$snap_ts"
newest_name="$snap_name"
fi
fi
done <<< "$snapshot_list"
done <<<"$snapshot_list"
[[ ${#snapshots[@]} -eq 0 ]] && return 0
[[ -z "$newest_name" ]] && return 0
@@ -327,7 +331,7 @@ clean_local_snapshots() {
echo -e " ${GRAY}The most recent snapshot will be kept.${NC}"
echo -ne " ${PURPLE}${ICON_ARROW}${NC} Remove all local snapshots except the most recent one? ${GREEN}Enter${NC} continue, ${GRAY}Space${NC} skip: "
local choice
if type read_key > /dev/null 2>&1; then
if type read_key >/dev/null 2>&1; then
choice=$(read_key)
else
IFS= read -r -s -n 1 choice || choice=""
@@ -352,7 +356,7 @@ clean_local_snapshots() {
((cleaned_count++))
note_activity
else
if sudo tmutil deletelocalsnapshots "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}-${BASH_REMATCH[4]}" > /dev/null 2>&1; then
if sudo tmutil deletelocalsnapshots "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}-${BASH_REMATCH[4]}" >/dev/null 2>&1; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed snapshot: $snap_name"
((cleaned_count++))
note_activity

View File

@@ -7,10 +7,7 @@ clean_user_essentials() {
stop_section_spinner
safe_clean ~/Library/Logs/* "User app logs"
if is_path_whitelisted "$HOME/.Trash"; then
note_activity
echo -e " ${GREEN}${ICON_EMPTY}${NC} Trash · whitelist protected"
else
if ! is_path_whitelisted "$HOME/.Trash"; then
safe_clean ~/.Trash/* "Trash"
fi
}
@@ -23,7 +20,7 @@ clean_chrome_old_versions() {
)
# Match the exact Chrome process name to avoid false positives
if pgrep -x "Google Chrome" > /dev/null 2>&1; then
if pgrep -x "Google Chrome" >/dev/null 2>&1; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Google Chrome running · old versions cleanup skipped"
return 0
fi
@@ -42,7 +39,7 @@ clean_chrome_old_versions() {
[[ -L "$current_link" ]] || continue
local current_version
current_version=$(readlink "$current_link" 2> /dev/null || true)
current_version=$(readlink "$current_link" 2>/dev/null || true)
current_version="${current_version##*/}"
[[ -n "$current_version" ]] || continue
@@ -72,9 +69,9 @@ clean_chrome_old_versions() {
cleaned_any=true
if [[ "$DRY_RUN" != "true" ]]; then
if has_sudo_session; then
safe_sudo_remove "$dir" > /dev/null 2>&1 || true
safe_sudo_remove "$dir" >/dev/null 2>&1 || true
else
safe_remove "$dir" true > /dev/null 2>&1 || true
safe_remove "$dir" true >/dev/null 2>&1 || true
fi
fi
done
@@ -103,7 +100,7 @@ clean_edge_old_versions() {
)
# Match the exact Edge process name to avoid false positives (e.g., Microsoft Teams)
if pgrep -x "Microsoft Edge" > /dev/null 2>&1; then
if pgrep -x "Microsoft Edge" >/dev/null 2>&1; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · old versions cleanup skipped"
return 0
fi
@@ -122,7 +119,7 @@ clean_edge_old_versions() {
[[ -L "$current_link" ]] || continue
local current_version
current_version=$(readlink "$current_link" 2> /dev/null || true)
current_version=$(readlink "$current_link" 2>/dev/null || true)
current_version="${current_version##*/}"
[[ -n "$current_version" ]] || continue
@@ -152,9 +149,9 @@ clean_edge_old_versions() {
cleaned_any=true
if [[ "$DRY_RUN" != "true" ]]; then
if has_sudo_session; then
safe_sudo_remove "$dir" > /dev/null 2>&1 || true
safe_sudo_remove "$dir" >/dev/null 2>&1 || true
else
safe_remove "$dir" true > /dev/null 2>&1 || true
safe_remove "$dir" true >/dev/null 2>&1 || true
fi
fi
done
@@ -180,7 +177,7 @@ clean_edge_updater_old_versions() {
local updater_dir="$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-stable"
[[ -d "$updater_dir" ]] || return 0
if pgrep -x "Microsoft Edge" > /dev/null 2>&1; then
if pgrep -x "Microsoft Edge" >/dev/null 2>&1; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · updater cleanup skipped"
return 0
fi
@@ -218,7 +215,7 @@ clean_edge_updater_old_versions() {
((cleaned_count++))
cleaned_any=true
if [[ "$DRY_RUN" != "true" ]]; then
safe_remove "$dir" true > /dev/null 2>&1 || true
safe_remove "$dir" true >/dev/null 2>&1 || true
fi
done
@@ -245,20 +242,20 @@ scan_external_volumes() {
[[ -d "$volume" && -w "$volume" && ! -L "$volume" ]] || continue
[[ "$volume" == "/" || "$volume" == "/Volumes/Macintosh HD" ]] && continue
local protocol=""
protocol=$(run_with_timeout 1 command diskutil info "$volume" 2> /dev/null | grep -i "Protocol:" | awk '{print $2}' || echo "")
protocol=$(run_with_timeout 1 command diskutil info "$volume" 2>/dev/null | grep -i "Protocol:" | awk '{print $2}' || echo "")
case "$protocol" in
SMB | NFS | AFP | CIFS | WebDAV)
network_volumes+=("$volume")
continue
;;
SMB | NFS | AFP | CIFS | WebDAV)
network_volumes+=("$volume")
continue
;;
esac
local fs_type=""
fs_type=$(run_with_timeout 1 command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}' || echo "")
fs_type=$(run_with_timeout 1 command df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}' || echo "")
case "$fs_type" in
nfs | smbfs | afpfs | cifs | webdav)
network_volumes+=("$volume")
continue
;;
nfs | smbfs | afpfs | cifs | webdav)
network_volumes+=("$volume")
continue
;;
esac
candidate_volumes+=("$volume")
done
@@ -278,7 +275,7 @@ scan_external_volumes() {
if [[ -d "$volume_trash" && "$DRY_RUN" != "true" ]] && ! is_path_whitelisted "$volume_trash"; then
while IFS= read -r -d '' item; do
safe_remove "$item" true || true
done < <(command find "$volume_trash" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
done < <(command find "$volume_trash" -mindepth 1 -maxdepth 1 -print0 2>/dev/null || true)
fi
if [[ "$PROTECT_FINDER_METADATA" != "true" ]]; then
clean_ds_store_tree "$volume" "$(basename "$volume") volume (.DS_Store)"
@@ -288,17 +285,13 @@ scan_external_volumes() {
}
# Finder metadata (.DS_Store).
clean_finder_metadata() {
stop_section_spinner
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
note_activity
echo -e " ${GREEN}${ICON_EMPTY}${NC} Finder metadata · whitelist protected"
return
fi
clean_ds_store_tree "$HOME" "Home directory (.DS_Store)"
}
# macOS system caches and user-level leftovers.
clean_macos_system_caches() {
stop_section_spinner
# safe_clean already checks protected paths.
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states" || true
safe_clean ~/Library/Caches/com.apple.photoanalysisd "Photo analysis cache" || true
@@ -318,7 +311,6 @@ clean_macos_system_caches() {
safe_clean ~/Library/Application\ Support/AddressBook/Sources/*/Photos.cache "Address Book photo cache" || true
}
clean_recent_items() {
stop_section_spinner
local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist"
local -a recent_lists=(
"$shared_dir/com.apple.LSSharedFileList.RecentApplications.sfl2"
@@ -338,7 +330,6 @@ clean_recent_items() {
safe_clean ~/Library/Preferences/com.apple.recentitems.plist "Recent items preferences" || true
}
clean_mail_downloads() {
stop_section_spinner
local mail_age_days=${MOLE_MAIL_AGE_DAYS:-}
if ! [[ "$mail_age_days" =~ ^[0-9]+$ ]]; then
mail_age_days=30
@@ -371,7 +362,7 @@ clean_mail_downloads() {
((cleaned_kb += file_size_kb))
fi
fi
done < <(command find "$target_path" -type f -mtime +"$mail_age_days" -print0 2> /dev/null || true)
done < <(command find "$target_path" -type f -mtime +"$mail_age_days" -print0 2>/dev/null || true)
fi
done
if [[ $count -gt 0 ]]; then
@@ -429,7 +420,7 @@ process_container_cache() {
local cache_dir="$container_dir/Data/Library/Caches"
[[ -d "$cache_dir" ]] || return 0
# Fast non-empty check.
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q .; then
local size=$(get_path_size_kb "$cache_dir")
((total_size += size))
found_any=true
@@ -449,7 +440,6 @@ process_container_cache() {
}
# Browser caches (Safari/Chrome/Edge/Firefox).
clean_browsers() {
stop_section_spinner
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
# Chrome/Chromium.
safe_clean ~/Library/Caches/Google/Chrome/* "Chrome cache"
@@ -461,7 +451,7 @@ clean_browsers() {
safe_clean ~/Library/Caches/company.thebrowser.dia/* "Dia cache"
safe_clean ~/Library/Caches/BraveSoftware/Brave-Browser/* "Brave cache"
local firefox_running=false
if pgrep -x "Firefox" > /dev/null 2>&1; then
if pgrep -x "Firefox" >/dev/null 2>&1; then
firefox_running=true
fi
if [[ "$firefox_running" == "true" ]]; then
@@ -485,7 +475,6 @@ clean_browsers() {
}
# Cloud storage caches.
clean_cloud_storage() {
stop_section_spinner
safe_clean ~/Library/Caches/com.dropbox.* "Dropbox cache"
safe_clean ~/Library/Caches/com.getdropbox.dropbox "Dropbox cache"
safe_clean ~/Library/Caches/com.google.GoogleDrive "Google Drive cache"
@@ -496,7 +485,6 @@ clean_cloud_storage() {
}
# Office app caches.
clean_office_applications() {
stop_section_spinner
safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache"
safe_clean ~/Library/Caches/com.microsoft.Excel "Microsoft Excel cache"
safe_clean ~/Library/Caches/com.microsoft.Powerpoint "Microsoft PowerPoint cache"
@@ -516,8 +504,7 @@ clean_virtualization_tools() {
}
# Application Support logs/caches.
clean_application_support_logs() {
stop_section_spinner
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" >/dev/null 2>&1; then
note_activity
echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Application Support"
return 0
@@ -549,7 +536,7 @@ clean_application_support_logs() {
local -a start_candidates=("$app_dir/log" "$app_dir/logs" "$app_dir/activitylog" "$app_dir/Cache/Cache_Data" "$app_dir/Crashpad/completed")
for candidate in "${start_candidates[@]}"; do
if [[ -d "$candidate" ]]; then
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q .; then
local size=$(get_path_size_kb "$candidate")
((total_size += size))
((cleaned_count++))
@@ -557,7 +544,7 @@ clean_application_support_logs() {
if [[ "$DRY_RUN" != "true" ]]; then
for item in "$candidate"/*; do
[[ -e "$item" ]] || continue
safe_remove "$item" true > /dev/null 2>&1 || true
safe_remove "$item" true >/dev/null 2>&1 || true
done
fi
fi
@@ -573,7 +560,7 @@ clean_application_support_logs() {
local -a gc_candidates=("$container_path/Logs" "$container_path/Library/Logs")
for candidate in "${gc_candidates[@]}"; do
if [[ -d "$candidate" ]]; then
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q .; then
local size=$(get_path_size_kb "$candidate")
((total_size += size))
((cleaned_count++))
@@ -581,7 +568,7 @@ clean_application_support_logs() {
if [[ "$DRY_RUN" != "true" ]]; then
for item in "$candidate"/*; do
[[ -e "$item" ]] || continue
safe_remove "$item" true > /dev/null 2>&1 || true
safe_remove "$item" true >/dev/null 2>&1 || true
done
fi
fi
@@ -610,7 +597,7 @@ check_ios_device_backups() {
if [[ -d "$backup_dir" ]]; then
local backup_kb=$(get_path_size_kb "$backup_dir")
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then
local backup_human=$(command du -sh "$backup_dir" 2> /dev/null | awk '{print $1}')
local backup_human=$(command du -sh "$backup_dir" 2>/dev/null | awk '{print $1}')
if [[ -n "$backup_human" ]]; then
note_activity
echo -e " Found ${GREEN}${backup_human}${NC} iOS backups"

View File

@@ -168,47 +168,47 @@ read_key() {
return 0
}
case "$key" in
$'\n' | $'\r') echo "ENTER" ;;
$'\x7f' | $'\x08') echo "DELETE" ;;
$'\x1b')
# Check if this is an escape sequence (arrow keys) or ESC key
if IFS= read -r -s -n 1 -t 0.1 rest 2> /dev/null; then
if [[ "$rest" == "[" ]]; then
if IFS= read -r -s -n 1 -t 0.1 rest2 2> /dev/null; then
case "$rest2" in
"A") echo "UP" ;;
"B") echo "DOWN" ;;
"C") echo "RIGHT" ;;
"D") echo "LEFT" ;;
"3")
IFS= read -r -s -n 1 -t 0.1 rest3 2> /dev/null
[[ "$rest3" == "~" ]] && echo "DELETE" || echo "OTHER"
;;
*) echo "OTHER" ;;
esac
else echo "QUIT"; fi
elif [[ "$rest" == "O" ]]; then
if IFS= read -r -s -n 1 -t 0.1 rest2 2> /dev/null; then
case "$rest2" in
"A") echo "UP" ;;
"B") echo "DOWN" ;;
"C") echo "RIGHT" ;;
"D") echo "LEFT" ;;
*) echo "OTHER" ;;
esac
else echo "OTHER"; fi
else
# Not an escape sequence, it's ESC key
echo "QUIT"
fi
$'\n' | $'\r') echo "ENTER" ;;
$'\x7f' | $'\x08') echo "DELETE" ;;
$'\x1b')
# Check if this is an escape sequence (arrow keys) or ESC key
if IFS= read -r -s -n 1 -t 0.1 rest 2>/dev/null; then
if [[ "$rest" == "[" ]]; then
if IFS= read -r -s -n 1 -t 0.1 rest2 2>/dev/null; then
case "$rest2" in
"A") echo "UP" ;;
"B") echo "DOWN" ;;
"C") echo "RIGHT" ;;
"D") echo "LEFT" ;;
"3")
IFS= read -r -s -n 1 -t 0.1 rest3 2>/dev/null
[[ "$rest3" == "~" ]] && echo "DELETE" || echo "OTHER"
;;
*) echo "OTHER" ;;
esac
else echo "QUIT"; fi
elif [[ "$rest" == "O" ]]; then
if IFS= read -r -s -n 1 -t 0.1 rest2 2>/dev/null; then
case "$rest2" in
"A") echo "UP" ;;
"B") echo "DOWN" ;;
"C") echo "RIGHT" ;;
"D") echo "LEFT" ;;
*) echo "OTHER" ;;
esac
else echo "OTHER"; fi
else
# No following characters, it's ESC key
# Not an escape sequence, it's ESC key
echo "QUIT"
fi
;;
' ') echo "SPACE" ;; # Allow space in filter mode for selection
[[:print:]]) echo "CHAR:$key" ;;
*) echo "OTHER" ;;
else
# No following characters, it's ESC key
echo "QUIT"
fi
;;
' ') echo "SPACE" ;; # Allow space in filter mode for selection
[[:print:]]) echo "CHAR:$key" ;;
*) echo "OTHER" ;;
esac
return 0
fi
@@ -218,53 +218,53 @@ read_key() {
return 0
}
case "$key" in
$'\n' | $'\r') echo "ENTER" ;;
' ') echo "SPACE" ;;
'/') echo "FILTER" ;;
'q' | 'Q') echo "QUIT" ;;
'R') echo "RETRY" ;;
'm' | 'M') echo "MORE" ;;
'u' | 'U') echo "UPDATE" ;;
't' | 'T') echo "TOUCHID" ;;
'j' | 'J') echo "DOWN" ;;
'k' | 'K') echo "UP" ;;
'h' | 'H') echo "LEFT" ;;
'l' | 'L') echo "RIGHT" ;;
$'\x03') echo "QUIT" ;;
$'\x7f' | $'\x08') echo "DELETE" ;;
$'\x1b')
if IFS= read -r -s -n 1 -t 1 rest 2> /dev/null; then
if [[ "$rest" == "[" ]]; then
if IFS= read -r -s -n 1 -t 1 rest2 2> /dev/null; then
case "$rest2" in
"A") echo "UP" ;; "B") echo "DOWN" ;;
"C") echo "RIGHT" ;; "D") echo "LEFT" ;;
"3")
IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null
[[ "$rest3" == "~" ]] && echo "DELETE" || echo "OTHER"
;;
*) echo "OTHER" ;;
esac
else echo "QUIT"; fi
elif [[ "$rest" == "O" ]]; then
if IFS= read -r -s -n 1 -t 1 rest2 2> /dev/null; then
case "$rest2" in
"A") echo "UP" ;; "B") echo "DOWN" ;;
"C") echo "RIGHT" ;; "D") echo "LEFT" ;;
*) echo "OTHER" ;;
esac
else echo "OTHER"; fi
$'\n' | $'\r') echo "ENTER" ;;
' ') echo "SPACE" ;;
'/') echo "FILTER" ;;
'q' | 'Q') echo "QUIT" ;;
'R') echo "RETRY" ;;
'm' | 'M') echo "MORE" ;;
'u' | 'U') echo "UPDATE" ;;
't' | 'T') echo "TOUCHID" ;;
'j' | 'J') echo "DOWN" ;;
'k' | 'K') echo "UP" ;;
'h' | 'H') echo "LEFT" ;;
'l' | 'L') echo "RIGHT" ;;
$'\x03') echo "QUIT" ;;
$'\x7f' | $'\x08') echo "DELETE" ;;
$'\x1b')
if IFS= read -r -s -n 1 -t 1 rest 2>/dev/null; then
if [[ "$rest" == "[" ]]; then
if IFS= read -r -s -n 1 -t 1 rest2 2>/dev/null; then
case "$rest2" in
"A") echo "UP" ;; "B") echo "DOWN" ;;
"C") echo "RIGHT" ;; "D") echo "LEFT" ;;
"3")
IFS= read -r -s -n 1 -t 1 rest3 2>/dev/null
[[ "$rest3" == "~" ]] && echo "DELETE" || echo "OTHER"
;;
*) echo "OTHER" ;;
esac
else echo "QUIT"; fi
elif [[ "$rest" == "O" ]]; then
if IFS= read -r -s -n 1 -t 1 rest2 2>/dev/null; then
case "$rest2" in
"A") echo "UP" ;; "B") echo "DOWN" ;;
"C") echo "RIGHT" ;; "D") echo "LEFT" ;;
*) echo "OTHER" ;;
esac
else echo "OTHER"; fi
else echo "QUIT"; fi
;;
[[:print:]]) echo "CHAR:$key" ;;
*) echo "OTHER" ;;
else echo "OTHER"; fi
else echo "QUIT"; fi
;;
[[:print:]]) echo "CHAR:$key" ;;
*) echo "OTHER" ;;
esac
}
drain_pending_input() {
local drained=0
while IFS= read -r -s -n 1 -t 0.01 _ 2> /dev/null; do
while IFS= read -r -s -n 1 -t 0.01 _ 2>/dev/null; do
((drained++))
[[ $drained -gt 100 ]] && break
done
@@ -288,7 +288,7 @@ INLINE_SPINNER_PID=""
INLINE_SPINNER_STOP_FILE=""
start_inline_spinner() {
stop_inline_spinner 2> /dev/null || true
stop_inline_spinner 2>/dev/null || true
local message="$1"
if [[ -t 1 ]]; then
@@ -308,15 +308,15 @@ start_inline_spinner() {
# Output to stderr to avoid interfering with stdout
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}%s${NC} %s" "$c" "$message" >&2 || break
((i++))
sleep 0.1
sleep 0.05
done
# Clean up stop file before exiting
rm -f "$stop_file" 2> /dev/null || true
rm -f "$stop_file" 2>/dev/null || true
exit 0
) &
INLINE_SPINNER_PID=$!
disown 2> /dev/null || true
disown 2>/dev/null || true
else
echo -n " ${BLUE}|${NC} $message" >&2 || true
fi
@@ -326,25 +326,25 @@ stop_inline_spinner() {
if [[ -n "$INLINE_SPINNER_PID" ]]; then
# Cooperative stop: create stop file to signal spinner to exit
if [[ -n "$INLINE_SPINNER_STOP_FILE" ]]; then
touch "$INLINE_SPINNER_STOP_FILE" 2> /dev/null || true
touch "$INLINE_SPINNER_STOP_FILE" 2>/dev/null || true
fi
# Wait briefly for cooperative exit
local wait_count=0
while kill -0 "$INLINE_SPINNER_PID" 2> /dev/null && [[ $wait_count -lt 5 ]]; do
sleep 0.05 2> /dev/null || true
while kill -0 "$INLINE_SPINNER_PID" 2>/dev/null && [[ $wait_count -lt 5 ]]; do
sleep 0.05 2>/dev/null || true
((wait_count++))
done
# Only use SIGKILL as last resort if process is stuck
if kill -0 "$INLINE_SPINNER_PID" 2> /dev/null; then
kill -KILL "$INLINE_SPINNER_PID" 2> /dev/null || true
if kill -0 "$INLINE_SPINNER_PID" 2>/dev/null; then
kill -KILL "$INLINE_SPINNER_PID" 2>/dev/null || true
fi
wait "$INLINE_SPINNER_PID" 2> /dev/null || true
wait "$INLINE_SPINNER_PID" 2>/dev/null || true
# Cleanup
rm -f "$INLINE_SPINNER_STOP_FILE" 2> /dev/null || true
rm -f "$INLINE_SPINNER_STOP_FILE" 2>/dev/null || true
INLINE_SPINNER_PID=""
INLINE_SPINNER_STOP_FILE=""
@@ -361,8 +361,8 @@ with_spinner() {
start_inline_spinner "$msg"
local exit_code=0
if [[ -n "${MOLE_TIMEOUT_BIN:-}" ]]; then
"$MOLE_TIMEOUT_BIN" "$timeout" "$@" > /dev/null 2>&1 || exit_code=$?
else "$@" > /dev/null 2>&1 || exit_code=$?; fi
"$MOLE_TIMEOUT_BIN" "$timeout" "$@" >/dev/null 2>&1 || exit_code=$?
else "$@" >/dev/null 2>&1 || exit_code=$?; fi
stop_inline_spinner "$msg"
return $exit_code
}
@@ -379,14 +379,14 @@ format_last_used_summary() {
local value="$1"
case "$value" in
"" | "Unknown")
echo "Unknown"
return 0
;;
"Never" | "Recent" | "Today" | "Yesterday" | "This year" | "Old")
echo "$value"
return 0
;;
"" | "Unknown")
echo "Unknown"
return 0
;;
"Never" | "Recent" | "Today" | "Yesterday" | "This year" | "Old")
echo "$value"
return 0
;;
esac
if [[ $value =~ ^([0-9]+)[[:space:]]+days?\ ago$ ]]; then
@@ -444,7 +444,7 @@ has_full_disk_access() {
if [[ -e "$test_path" ]]; then
tested_count=$((tested_count + 1))
# Try to stat the ACTUAL protected path - this requires FDA
if stat "$test_path" > /dev/null 2>&1; then
if stat "$test_path" >/dev/null 2>&1; then
accessible_count=$((accessible_count + 1))
fi
fi