From e6fc0613d5ac16fbc090784dfac1bd455bb28e09 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sat, 17 Jan 2026 10:12:23 +0800 Subject: [PATCH] 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. --- bin/clean.sh | 93 ++++++++++---------- lib/clean/system.sh | 74 ++++++++-------- lib/clean/user.sh | 75 +++++++--------- lib/core/ui.sh | 202 ++++++++++++++++++++++---------------------- 4 files changed, 216 insertions(+), 228 deletions(-) diff --git a/bin/clean.sh b/bin/clean.sh index 2de8f28..3a33ed6 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -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" <> "$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 diff --git a/lib/clean/system.sh b/lib/clean/system.sh index 27d6ab0..aa1ce4a 100644 --- a/lib/clean/system.sh +++ b/lib/clean/system.sh @@ -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 diff --git a/lib/clean/user.sh b/lib/clean/user.sh index a951685..7c82b5c 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -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" diff --git a/lib/core/ui.sh b/lib/core/ui.sh index d27f231..257b385 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -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