From f3b288a21b264e278a9a659c849262c8186f53c1 Mon Sep 17 00:00:00 2001 From: tw93 Date: Sat, 7 Feb 2026 10:23:35 +0800 Subject: [PATCH] feat(uninstall): implement metadata caching for faster app scanning --- bin/uninstall.sh | 683 +++++++++++++++++++++++++++++++---------------- 1 file changed, 459 insertions(+), 224 deletions(-) diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 437f66d..8b9fcf9 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -5,6 +5,10 @@ set -euo pipefail +# Preserve user's locale for app display name lookup. +readonly MOLE_UNINSTALL_USER_LC_ALL="${LC_ALL:-}" +readonly MOLE_UNINSTALL_USER_LANG="${LANG:-}" + # Fix locale issues on non-English systems. export LC_ALL=C export LANG=C @@ -27,60 +31,341 @@ total_items=0 files_cleaned=0 total_size_cleaned=0 -# Scan applications and collect information. -scan_applications() { - # Cache app scan (24h TTL). - local cache_dir="$HOME/.cache/mole" - local cache_file="$cache_dir/app_scan_cache" - local cache_ttl=86400 # 24 hours - local force_rescan="${1:-false}" +readonly MOLE_UNINSTALL_META_CACHE_DIR="$HOME/.cache/mole" +readonly MOLE_UNINSTALL_META_CACHE_FILE="$MOLE_UNINSTALL_META_CACHE_DIR/uninstall_app_metadata_v1" +readonly MOLE_UNINSTALL_META_CACHE_LOCK="${MOLE_UNINSTALL_META_CACHE_FILE}.lock" +readonly MOLE_UNINSTALL_META_REFRESH_TTL=604800 # 7 days +readonly MOLE_UNINSTALL_SCAN_SPINNER_DELAY_SEC="0.15" - ensure_user_dir "$cache_dir" +uninstall_relative_time_from_epoch() { + local value_epoch="${1:-0}" + local now_epoch="${2:-0}" - if [[ $force_rescan == false && -f "$cache_file" ]]; then - local cache_age=$(($(get_epoch_seconds) - $(get_file_mtime "$cache_file"))) - [[ $cache_age -eq $(get_epoch_seconds) ]] && cache_age=86401 + if [[ ! "$value_epoch" =~ ^[0-9]+$ || $value_epoch -le 0 ]]; then + echo "..." + return 0 + fi - if [[ $cache_age -lt $cache_ttl ]]; then - if [[ -t 2 ]]; then - echo -e "${GREEN}Loading from cache...${NC}" >&2 - sleep 0.3 - fi - echo "$cache_file" - return 0 + local days_ago=$(((now_epoch - value_epoch) / 86400)) + if [[ $days_ago -lt 0 ]]; then + days_ago=0 + fi + + if [[ $days_ago -eq 0 ]]; then + echo "Today" + elif [[ $days_ago -eq 1 ]]; then + echo "Yesterday" + elif [[ $days_ago -lt 7 ]]; then + echo "${days_ago} days ago" + elif [[ $days_ago -lt 30 ]]; then + local weeks_ago=$((days_ago / 7)) + [[ $weeks_ago -eq 1 ]] && echo "1 week ago" || echo "${weeks_ago} weeks ago" + elif [[ $days_ago -lt 365 ]]; then + local months_ago=$((days_ago / 30)) + [[ $months_ago -eq 1 ]] && echo "1 month ago" || echo "${months_ago} months ago" + else + local years_ago=$((days_ago / 365)) + [[ $years_ago -eq 1 ]] && echo "1 year ago" || echo "${years_ago} years ago" + fi +} + +uninstall_resolve_display_name() { + local app_path="$1" + local app_name="$2" + local display_name="$app_name" + + if [[ -f "$app_path/Contents/Info.plist" ]]; then + local md_display_name + if [[ -n "$MOLE_UNINSTALL_USER_LC_ALL" ]]; then + md_display_name=$(run_with_timeout 0.04 env LC_ALL="$MOLE_UNINSTALL_USER_LC_ALL" LANG="$MOLE_UNINSTALL_USER_LANG" mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "") + elif [[ -n "$MOLE_UNINSTALL_USER_LANG" ]]; then + md_display_name=$(run_with_timeout 0.04 env LANG="$MOLE_UNINSTALL_USER_LANG" mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "") + else + md_display_name=$(run_with_timeout 0.04 mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "") + fi + + local bundle_display_name + bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null || echo "") + local bundle_name + bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null || echo "") + + if [[ "$md_display_name" == /* ]]; then + md_display_name="" + fi + md_display_name="${md_display_name//|/-}" + md_display_name="${md_display_name//[$'\t\r\n']/}" + + bundle_display_name="${bundle_display_name//|/-}" + bundle_display_name="${bundle_display_name//[$'\t\r\n']/}" + + bundle_name="${bundle_name//|/-}" + bundle_name="${bundle_name//[$'\t\r\n']/}" + + if [[ -n "$md_display_name" && "$md_display_name" != "(null)" && "$md_display_name" != "$app_name" ]]; then + display_name="$md_display_name" + elif [[ -n "$bundle_display_name" && "$bundle_display_name" != "(null)" ]]; then + display_name="$bundle_display_name" + elif [[ -n "$bundle_name" && "$bundle_name" != "(null)" ]]; then + display_name="$bundle_name" fi fi - local inline_loading=false - if [[ -t 1 && -t 2 ]]; then - inline_loading=true - printf "\033[2J\033[H" >&2 # Clear screen for inline loading + if [[ "$display_name" == /* ]]; then + display_name="$app_name" + fi + display_name="${display_name%.app}" + display_name="${display_name//|/-}" + display_name="${display_name//[$'\t\r\n']/}" + echo "$display_name" +} + +uninstall_acquire_metadata_lock() { + local lock_dir="$1" + local attempts=0 + + while ! mkdir "$lock_dir" 2> /dev/null; do + ((attempts++)) + if [[ $attempts -ge 40 ]]; then + return 1 + fi + + # Clean stale lock if older than 5 minutes. + if [[ -d "$lock_dir" ]]; then + local lock_mtime + lock_mtime=$(get_file_mtime "$lock_dir") + # Skip stale detection if mtime lookup failed (returns 0). + if [[ "$lock_mtime" =~ ^[0-9]+$ && $lock_mtime -gt 0 ]]; then + local lock_age + lock_age=$(($(get_epoch_seconds) - lock_mtime)) + if [[ "$lock_age" =~ ^-?[0-9]+$ && $lock_age -gt 300 ]]; then + rmdir "$lock_dir" 2> /dev/null || true + fi + fi + fi + + sleep 0.1 2> /dev/null || sleep 1 + done + + return 0 +} + +uninstall_release_metadata_lock() { + local lock_dir="$1" + [[ -d "$lock_dir" ]] && rmdir "$lock_dir" 2> /dev/null || true +} + +start_uninstall_metadata_refresh() { + local refresh_file="$1" + [[ ! -s "$refresh_file" ]] && { + rm -f "$refresh_file" 2> /dev/null || true + return 0 + } + + ( + _refresh_debug() { + if [[ "${MO_DEBUG:-}" == "1" ]]; then + local ts + ts=$(date "+%Y-%m-%d %H:%M:%S" 2> /dev/null || echo "?") + echo "[$ts] DEBUG: [metadata-refresh] $*" >> "${HOME}/.config/mole/mole_debug_session.log" 2> /dev/null || true + fi + } + + ensure_user_dir "$MOLE_UNINSTALL_META_CACHE_DIR" + ensure_user_file "$MOLE_UNINSTALL_META_CACHE_FILE" + if [[ ! -r "$MOLE_UNINSTALL_META_CACHE_FILE" ]]; then + if ! : > "$MOLE_UNINSTALL_META_CACHE_FILE" 2> /dev/null; then + _refresh_debug "Cannot create cache file, aborting" + exit 0 + fi + fi + if [[ ! -w "$MOLE_UNINSTALL_META_CACHE_FILE" ]]; then + _refresh_debug "Cache file not writable, aborting" + exit 0 + fi + + local updates_file + updates_file=$(mktemp 2> /dev/null) || { + _refresh_debug "mktemp failed, aborting" + exit 0 + } + local now_epoch + now_epoch=$(get_epoch_seconds) + local max_parallel + max_parallel=$(get_optimal_parallel_jobs "io") + if [[ ! "$max_parallel" =~ ^[0-9]+$ || $max_parallel -lt 1 ]]; then + max_parallel=1 + elif [[ $max_parallel -gt 4 ]]; then + max_parallel=4 + fi + local -a worker_pids=() + local worker_idx=0 + + while IFS='|' read -r app_path app_mtime bundle_id display_name; do + [[ -n "$app_path" && -d "$app_path" ]] || continue + ((worker_idx++)) + local worker_output="${updates_file}.${worker_idx}" + + ( + local last_used_epoch=0 + local metadata_date + metadata_date=$(run_with_timeout 0.2 mdls -name kMDItemLastUsedDate -raw "$app_path" 2> /dev/null || echo "") + if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then + last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2> /dev/null || echo "0") + fi + + if [[ ! "$last_used_epoch" =~ ^[0-9]+$ || $last_used_epoch -le 0 ]]; then + last_used_epoch=0 + fi + + local size_kb + size_kb=$(get_path_size_kb "$app_path") + [[ "$size_kb" =~ ^[0-9]+$ ]] || size_kb=0 + + printf "%s|%s|%s|%s|%s|%s|%s\n" "$app_path" "${app_mtime:-0}" "$size_kb" "${last_used_epoch:-0}" "$now_epoch" "$bundle_id" "$display_name" > "$worker_output" + ) & + worker_pids+=($!) + + if ((${#worker_pids[@]} >= max_parallel)); then + wait "${worker_pids[0]}" 2> /dev/null || true + worker_pids=("${worker_pids[@]:1}") + fi + done < "$refresh_file" + + local worker_pid + for worker_pid in "${worker_pids[@]}"; do + wait "$worker_pid" 2> /dev/null || true + done + + local worker_output + for worker_output in "${updates_file}".*; do + [[ -f "$worker_output" ]] || continue + cat "$worker_output" >> "$updates_file" + rm -f "$worker_output" + done + + if [[ ! -s "$updates_file" ]]; then + rm -f "$updates_file" + exit 0 + fi + + if ! uninstall_acquire_metadata_lock "$MOLE_UNINSTALL_META_CACHE_LOCK"; then + _refresh_debug "Failed to acquire lock, aborting merge" + rm -f "$updates_file" + exit 0 + fi + + local merged_file + merged_file=$(mktemp 2> /dev/null) || { + _refresh_debug "mktemp for merge failed, aborting" + uninstall_release_metadata_lock "$MOLE_UNINSTALL_META_CACHE_LOCK" + rm -f "$updates_file" + exit 0 + } + + awk -F'|' ' + NR == FNR { updates[$1] = $0; next } + !($1 in updates) { print } + END { + for (path in updates) { + print updates[path] + } + } + ' "$updates_file" "$MOLE_UNINSTALL_META_CACHE_FILE" > "$merged_file" + + mv "$merged_file" "$MOLE_UNINSTALL_META_CACHE_FILE" 2> /dev/null || { + cp "$merged_file" "$MOLE_UNINSTALL_META_CACHE_FILE" 2> /dev/null || true + rm -f "$merged_file" + } + + uninstall_release_metadata_lock "$MOLE_UNINSTALL_META_CACHE_LOCK" + rm -f "$updates_file" + rm -f "$refresh_file" 2> /dev/null || true + ) > /dev/null 2>&1 & + +} + +# Scan applications and collect information. +scan_applications() { + local temp_file scan_raw_file merged_file refresh_file cache_snapshot_file + temp_file=$(create_temp_file) + scan_raw_file="${temp_file}.scan" + merged_file="${temp_file}.merged" + refresh_file="${temp_file}.refresh" + cache_snapshot_file="${temp_file}.cache" + : > "$scan_raw_file" + : > "$refresh_file" + : > "$cache_snapshot_file" + + ensure_user_dir "$MOLE_UNINSTALL_META_CACHE_DIR" + ensure_user_file "$MOLE_UNINSTALL_META_CACHE_FILE" + local cache_source="$MOLE_UNINSTALL_META_CACHE_FILE" + local cache_source_is_temp=false + if [[ ! -r "$cache_source" ]]; then + cache_source=$(create_temp_file) + : > "$cache_source" + cache_source_is_temp=true fi - local temp_file - temp_file=$(create_temp_file) + # Fast lookup cache for unchanged apps: path+mtime -> bundle_id/display_name. + local -a cache_paths=() + local -a cache_mtimes=() + local -a cache_bundle_ids=() + local -a cache_display_names=() + local cache_path cache_mtime _cache_size _cache_epoch _cache_updated cache_bundle cache_display + while IFS='|' read -r cache_path cache_mtime _cache_size _cache_epoch _cache_updated cache_bundle cache_display; do + [[ -n "$cache_path" ]] || continue + cache_paths+=("$cache_path") + cache_mtimes+=("${cache_mtime:-0}") + cache_bundle_ids+=("${cache_bundle:-}") + cache_display_names+=("${cache_display:-}") + done < "$cache_source" + + lookup_cached_identity() { + local target_path="$1" + local target_mtime="$2" + local idx + for ((idx = 0; idx < ${#cache_paths[@]}; idx++)); do + if [[ "${cache_paths[idx]}" == "$target_path" ]]; then + if [[ "${cache_mtimes[idx]:-0}" == "${target_mtime:-0}" ]]; then + echo "${cache_bundle_ids[idx]:-}|${cache_display_names[idx]:-}" + else + echo "|" + fi + return 0 + fi + done + echo "|" + } # Local spinner_pid for cleanup local spinner_pid="" + local spinner_shown_file="${temp_file}.spinner_shown" + local previous_int_trap="" + previous_int_trap=$(trap -p INT || true) + + restore_scan_int_trap() { + if [[ -n "$previous_int_trap" ]]; then + eval "$previous_int_trap" + else + trap - INT + fi + } # Trap to handle Ctrl+C during scan - local scan_interrupted=false # shellcheck disable=SC2329 # Function invoked indirectly via trap trap_scan_cleanup() { - scan_interrupted=true if [[ -n "$spinner_pid" ]]; then kill -TERM "$spinner_pid" 2> /dev/null || true wait "$spinner_pid" 2> /dev/null || true fi - printf "\r\033[K" >&2 - rm -f "$temp_file" "${temp_file}.sorted" "${temp_file}.progress" 2> /dev/null || true + if [[ -f "$spinner_shown_file" ]]; then + printf "\r\033[K" >&2 + fi + rm -f "$temp_file" "$scan_raw_file" "$merged_file" "$refresh_file" "$cache_snapshot_file" "${temp_file}.sorted" "${temp_file}.progress" "$spinner_shown_file" 2> /dev/null || true exit 130 } trap trap_scan_cleanup INT - local current_epoch - current_epoch=$(get_epoch_seconds) - # Pass 1: collect app paths and bundle IDs (no mdls). local -a app_data_tuples=() local -a app_dirs=( @@ -141,27 +426,27 @@ scan_applications() { fi fi - local bundle_id="unknown" - if [[ -f "$app_path/Contents/Info.plist" ]]; then - bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown") - fi + local app_mtime + app_mtime=$(get_file_mtime "$app_path") - if should_protect_from_uninstall "$bundle_id"; then - continue - fi + local cached_identity cached_bundle_id cached_display_name + cached_identity=$(lookup_cached_identity "$app_path" "$app_mtime") + IFS='|' read -r cached_bundle_id cached_display_name <<< "$cached_identity" - # Store tuple for pass 2 (metadata + size). - app_data_tuples+=("${app_path}|${app_name}|${bundle_id}") + # Store tuple for pass 2 (bundle + display resolution, then cache merge). + app_data_tuples+=("${app_path}|${app_name}|${app_mtime}|${cached_bundle_id}|${cached_display_name}") done < <(command find "$app_dir" -name "*.app" -maxdepth 3 -print0 2> /dev/null) done if [[ ${#app_data_tuples[@]} -eq 0 ]]; then - rm -f "$temp_file" + rm -f "$temp_file" "$scan_raw_file" "$merged_file" "$refresh_file" "$cache_snapshot_file" "${temp_file}.sorted" "${temp_file}.progress" "$spinner_shown_file" 2> /dev/null || true + [[ $cache_source_is_temp == true ]] && rm -f "$cache_source" 2> /dev/null || true + restore_scan_int_trap printf "\r\033[K" >&2 echo "No applications found to uninstall." >&2 return 1 fi - # Pass 2: metadata + size in parallel (mdls is slow). + # Pass 2: resolve display names in parallel. local app_count=0 local total_apps=${#app_data_tuples[@]} local max_parallel @@ -176,97 +461,33 @@ scan_applications() { process_app_metadata() { local app_data_tuple="$1" local output_file="$2" - local current_epoch="$3" - IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple" + IFS='|' read -r app_path app_name app_mtime cached_bundle_id cached_display_name <<< "$app_data_tuple" - # Display name priority: mdls display name → bundle display → bundle name → folder. - local display_name="$app_name" - if [[ -f "$app_path/Contents/Info.plist" ]]; then - local md_display_name - md_display_name=$(run_with_timeout 0.05 mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "") - - local bundle_display_name - bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null) - local bundle_name - bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null) - - if [[ "$md_display_name" == /* ]]; then md_display_name=""; fi - md_display_name="${md_display_name//|/-}" - md_display_name="${md_display_name//[$'\t\r\n']/}" - - bundle_display_name="${bundle_display_name//|/-}" - bundle_display_name="${bundle_display_name//[$'\t\r\n']/}" - - bundle_name="${bundle_name//|/-}" - bundle_name="${bundle_name//[$'\t\r\n']/}" - - if [[ -n "$md_display_name" && "$md_display_name" != "(null)" && "$md_display_name" != "$app_name" ]]; then - display_name="$md_display_name" - elif [[ -n "$bundle_display_name" && "$bundle_display_name" != "(null)" ]]; then - display_name="$bundle_display_name" - elif [[ -n "$bundle_name" && "$bundle_name" != "(null)" ]]; then - display_name="$bundle_name" + local bundle_id="${cached_bundle_id:-}" + if [[ -z "$bundle_id" ]]; then + bundle_id="unknown" + if [[ -f "$app_path/Contents/Info.plist" ]]; then + bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown") fi fi - if [[ "$display_name" == /* ]]; then - display_name="$app_name" + if should_protect_from_uninstall "$bundle_id"; then + return 0 fi + + local display_name="${cached_display_name:-}" + if [[ -z "$display_name" ]]; then + display_name=$(uninstall_resolve_display_name "$app_path" "$app_name") + fi + + display_name="${display_name%.app}" display_name="${display_name//|/-}" display_name="${display_name//[$'\t\r\n']/}" - # App size (KB → human). - local app_size="N/A" - local app_size_kb="0" - if [[ -d "$app_path" ]]; then - app_size_kb=$(get_path_size_kb "$app_path") - app_size=$(bytes_to_human "$((app_size_kb * 1024))") - fi - - # Last used: mdls (fast timeout) → mtime. - local last_used="Never" - local last_used_epoch=0 - - if [[ -d "$app_path" ]]; then - local metadata_date - metadata_date=$(run_with_timeout 0.1 mdls -name kMDItemLastUsedDate -raw "$app_path" 2> /dev/null || echo "") - - if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then - last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2> /dev/null || echo "0") - fi - - if [[ "$last_used_epoch" -eq 0 ]]; then - last_used_epoch=$(get_file_mtime "$app_path") - fi - - if [[ $last_used_epoch -gt 0 ]]; then - local days_ago=$(((current_epoch - last_used_epoch) / 86400)) - - if [[ $days_ago -eq 0 ]]; then - last_used="Today" - elif [[ $days_ago -eq 1 ]]; then - last_used="Yesterday" - elif [[ $days_ago -lt 7 ]]; then - last_used="${days_ago} days ago" - elif [[ $days_ago -lt 30 ]]; then - local weeks_ago=$((days_ago / 7)) - [[ $weeks_ago -eq 1 ]] && last_used="1 week ago" || last_used="${weeks_ago} weeks ago" - elif [[ $days_ago -lt 365 ]]; then - local months_ago=$((days_ago / 30)) - [[ $months_ago -eq 1 ]] && last_used="1 month ago" || last_used="${months_ago} months ago" - else - local years_ago=$((days_ago / 365)) - [[ $years_ago -eq 1 ]] && last_used="1 year ago" || last_used="${years_ago} years ago" - fi - fi - fi - - echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}|${app_size_kb}" >> "$output_file" + echo "${app_path}|${display_name}|${bundle_id}|${app_mtime}" >> "$output_file" } - export -f process_app_metadata - local progress_file="${temp_file}.progress" echo "0" > "$progress_file" @@ -274,16 +495,15 @@ scan_applications() { # shellcheck disable=SC2329 # Function invoked indirectly via trap cleanup_spinner() { exit 0; } trap cleanup_spinner TERM INT EXIT + sleep "$MOLE_UNINSTALL_SCAN_SPINNER_DELAY_SEC" 2> /dev/null || sleep 1 + [[ -f "$progress_file" ]] || exit 0 local spinner_chars="|/-\\" local i=0 + : > "$spinner_shown_file" while true; do local completed=$(cat "$progress_file" 2> /dev/null || echo 0) local c="${spinner_chars:$((i % 4)):1}" - if [[ $inline_loading == true ]]; then - printf "\033[H\033[2K%s Scanning applications... %d/%d\n" "$c" "$completed" "$total_apps" >&2 - else - printf "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 - fi + printf "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 ((i++)) sleep 0.1 2> /dev/null || sleep 1 done @@ -292,7 +512,7 @@ scan_applications() { for app_data_tuple in "${app_data_tuples[@]}"; do ((app_count++)) - process_app_metadata "$app_data_tuple" "$temp_file" "$current_epoch" & + process_app_metadata "$app_data_tuple" "$scan_raw_file" & pids+=($!) echo "$app_count" > "$progress_file" @@ -310,47 +530,127 @@ scan_applications() { kill -TERM "$spinner_pid" 2> /dev/null || true wait "$spinner_pid" 2> /dev/null || true fi - if [[ $inline_loading == true ]]; then - printf "\033[H\033[2K" >&2 - else + if [[ -f "$spinner_shown_file" ]]; then echo -ne "\r\033[K" >&2 fi - rm -f "$progress_file" + rm -f "$progress_file" "$spinner_shown_file" - if [[ ! -s "$temp_file" ]]; then + if [[ ! -s "$scan_raw_file" ]]; then echo "No applications found to uninstall" >&2 - rm -f "$temp_file" + rm -f "$temp_file" "$scan_raw_file" "$merged_file" "$refresh_file" "$cache_snapshot_file" "${temp_file}.sorted" "${temp_file}.progress" "$spinner_shown_file" 2> /dev/null || true + [[ $cache_source_is_temp == true ]] && rm -f "$cache_source" 2> /dev/null || true + restore_scan_int_trap return 1 fi if [[ $total_apps -gt 50 ]]; then - if [[ $inline_loading == true ]]; then - printf "\033[H\033[2KProcessing %d applications...\n" "$total_apps" >&2 + printf "\rProcessing %d applications... " "$total_apps" >&2 + fi + + awk -F'|' ' + NR == FNR { + cache_mtime[$1] = $2 + cache_size[$1] = $3 + cache_epoch[$1] = $4 + cache_updated[$1] = $5 + cache_bundle[$1] = $6 + cache_display[$1] = $7 + next + } + { + print $0 "|" cache_mtime[$1] "|" cache_size[$1] "|" cache_epoch[$1] "|" cache_updated[$1] "|" cache_bundle[$1] "|" cache_display[$1] + } + ' "$cache_source" "$scan_raw_file" > "$merged_file" + if [[ ! -s "$merged_file" && -s "$scan_raw_file" ]]; then + awk '{print $0 "||||||"}' "$scan_raw_file" > "$merged_file" + fi + + local current_epoch + current_epoch=$(get_epoch_seconds) + + while IFS='|' read -r app_path display_name bundle_id app_mtime cached_mtime cached_size_kb cached_epoch cached_updated_epoch cached_bundle_id cached_display_name; do + [[ -n "$app_path" && -e "$app_path" ]] || continue + + local cache_match=false + if [[ -n "$cached_mtime" && -n "$app_mtime" && "$cached_mtime" == "$app_mtime" ]]; then + cache_match=true + fi + + local final_epoch=0 + if [[ "$cached_epoch" =~ ^[0-9]+$ && $cached_epoch -gt 0 ]]; then + final_epoch="$cached_epoch" + fi + + local final_size_kb=0 + local final_size="..." + if [[ "$cached_size_kb" =~ ^[0-9]+$ && $cached_size_kb -gt 0 ]]; then + final_size_kb="$cached_size_kb" + final_size=$(bytes_to_human "$((cached_size_kb * 1024))") + fi + + local final_last_used + final_last_used=$(uninstall_relative_time_from_epoch "$final_epoch" "$current_epoch") + + local needs_refresh=false + if [[ $cache_match == false ]]; then + needs_refresh=true + elif [[ ! "$cached_size_kb" =~ ^[0-9]+$ || $cached_size_kb -le 0 ]]; then + needs_refresh=true + elif [[ ! "$cached_epoch" =~ ^[0-9]+$ || $cached_epoch -le 0 ]]; then + needs_refresh=true + elif [[ ! "$cached_updated_epoch" =~ ^[0-9]+$ ]]; then + needs_refresh=true + elif [[ -z "$cached_bundle_id" || -z "$cached_display_name" ]]; then + needs_refresh=true else - printf "\rProcessing %d applications... " "$total_apps" >&2 + local cache_age=$((current_epoch - cached_updated_epoch)) + if [[ $cache_age -gt $MOLE_UNINSTALL_META_REFRESH_TTL ]]; then + needs_refresh=true + fi + fi + + if [[ $needs_refresh == true ]]; then + printf "%s|%s|%s|%s\n" "$app_path" "${app_mtime:-0}" "$bundle_id" "$display_name" >> "$refresh_file" + fi + + local persist_updated_epoch=0 + if [[ "$cached_updated_epoch" =~ ^[0-9]+$ && $cached_updated_epoch -gt 0 ]]; then + persist_updated_epoch="$cached_updated_epoch" + fi + printf "%s|%s|%s|%s|%s|%s|%s\n" "$app_path" "${app_mtime:-0}" "${final_size_kb:-0}" "${final_epoch:-0}" "${persist_updated_epoch:-0}" "$bundle_id" "$display_name" >> "$cache_snapshot_file" + + echo "${final_epoch}|${app_path}|${display_name}|${bundle_id}|${final_size}|${final_last_used}|${final_size_kb}" >> "$temp_file" + done < "$merged_file" + + if [[ -s "$cache_snapshot_file" ]]; then + if uninstall_acquire_metadata_lock "$MOLE_UNINSTALL_META_CACHE_LOCK"; then + mv "$cache_snapshot_file" "$MOLE_UNINSTALL_META_CACHE_FILE" 2> /dev/null || { + cp "$cache_snapshot_file" "$MOLE_UNINSTALL_META_CACHE_FILE" 2> /dev/null || true + rm -f "$cache_snapshot_file" + } + uninstall_release_metadata_lock "$MOLE_UNINSTALL_META_CACHE_LOCK" fi fi sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { - rm -f "$temp_file" + rm -f "$temp_file" "$scan_raw_file" "$merged_file" "$refresh_file" "$cache_snapshot_file" + [[ $cache_source_is_temp == true ]] && rm -f "$cache_source" 2> /dev/null || true + restore_scan_int_trap return 1 } - rm -f "$temp_file" + rm -f "$temp_file" "$scan_raw_file" "$merged_file" "$cache_snapshot_file" + [[ $cache_source_is_temp == true ]] && rm -f "$cache_source" 2> /dev/null || true - if [[ $total_apps -gt 50 ]]; then - if [[ $inline_loading == true ]]; then - printf "\033[H\033[2K" >&2 - else - printf "\r\033[K" >&2 - fi - fi + [[ $total_apps -gt 50 ]] && printf "\r\033[K" >&2 - ensure_user_file "$cache_file" - cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true + start_uninstall_metadata_refresh "$refresh_file" if [[ -f "${temp_file}.sorted" ]]; then + restore_scan_int_trap echo "${temp_file}.sorted" + return 0 else + restore_scan_int_trap return 1 fi } @@ -405,7 +705,6 @@ main() { export MOLE_CURRENT_COMMAND="uninstall" log_operation_session_start "uninstall" - local force_rescan=false # Global flags for arg in "$@"; do case "$arg" in @@ -415,69 +714,22 @@ main() { esac done - local use_inline_loading=false - if [[ -t 1 && -t 2 ]]; then - use_inline_loading=true - fi - hide_cursor while true; do - local needs_scanning=true - local cache_file="$HOME/.cache/mole/app_scan_cache" - if [[ $force_rescan == false && -f "$cache_file" ]]; then - local cache_age=$(($(get_epoch_seconds) - $(get_file_mtime "$cache_file"))) - [[ $cache_age -eq $(get_epoch_seconds) ]] && cache_age=86401 - [[ $cache_age -lt 86400 ]] && needs_scanning=false - fi - - if [[ $needs_scanning == true && $use_inline_loading == true ]]; then - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" != "1" ]]; then - enter_alt_screen - export MOLE_ALT_SCREEN_ACTIVE=1 - export MOLE_INLINE_LOADING=1 - export MOLE_MANAGED_ALT_SCREEN=1 - fi - printf "\033[2J\033[H" >&2 - else - unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN MOLE_ALT_SCREEN_ACTIVE - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - leave_alt_screen - unset MOLE_ALT_SCREEN_ACTIVE - fi - fi + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN local apps_file="" - if ! apps_file=$(scan_applications "$force_rescan"); then - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - printf "\033[2J\033[H" >&2 - leave_alt_screen - unset MOLE_ALT_SCREEN_ACTIVE - unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN - fi + if ! apps_file=$(scan_applications); then return 1 fi - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - printf "\033[2J\033[H" >&2 - fi - if [[ ! -f "$apps_file" ]]; then - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - leave_alt_screen - unset MOLE_ALT_SCREEN_ACTIVE - unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN - fi return 1 fi if ! load_applications "$apps_file"; then - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - leave_alt_screen - unset MOLE_ALT_SCREEN_ACTIVE - unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN - fi - [[ "$apps_file" != "$cache_file" ]] && rm -f "$apps_file" + rm -f "$apps_file" return 1 fi @@ -487,38 +739,21 @@ main() { set -e if [[ $exit_code -ne 0 ]]; then - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - leave_alt_screen - unset MOLE_ALT_SCREEN_ACTIVE - unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN - fi show_cursor clear_screen printf '\033[2J\033[H' >&2 - # Only delete temp files, never the permanent cache - [[ "$apps_file" != "$cache_file" ]] && rm -f "$apps_file" - - if [[ $exit_code -eq 10 ]]; then - force_rescan=true - continue - fi + rm -f "$apps_file" return 0 fi - if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then - leave_alt_screen - unset MOLE_ALT_SCREEN_ACTIVE - unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN - fi - show_cursor clear_screen printf '\033[2J\033[H' >&2 local selection_count=${#selected_apps[@]} if [[ $selection_count -eq 0 ]]; then echo "No apps selected" - [[ "$apps_file" != "$cache_file" ]] && rm -f "$apps_file" + rm -f "$apps_file" continue fi echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} apps:" @@ -531,9 +766,10 @@ main() { local name_width=$(get_display_width "$app_name") [[ $name_width -gt $max_name_display_width ]] && max_name_display_width=$name_width local size_display="$size" - [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]] && size_display="Unknown" + [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" || "$size_display" == "Unknown" ]] && size_display="..." [[ ${#size_display} -gt $max_size_width ]] && max_size_width=${#size_display} local last_display=$(format_last_used_summary "$last_used") + [[ -z "$last_display" || "$last_display" == "Unknown" || "$last_display" == "Never" ]] && last_display="..." [[ ${#last_display} -gt $max_last_width ]] && max_last_width=${#last_display} done ((max_size_width < 5)) && max_size_width=5 @@ -570,12 +806,13 @@ main() { [[ $current_width -gt $max_name_display_width ]] && max_name_display_width=$current_width local size_display="$size" - if [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]]; then - size_display="Unknown" + if [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" || "$size_display" == "Unknown" ]]; then + size_display="..." fi local last_display last_display=$(format_last_used_summary "$last_used") + [[ -z "$last_display" || "$last_display" == "Unknown" || "$last_display" == "Never" ]] && last_display="..." summary_rows+=("$display_name|$size_display|$last_display") done @@ -597,8 +834,7 @@ main() { batch_uninstall_applications - # Only delete temp files, never the permanent cache - [[ "$apps_file" != "$cache_file" ]] && rm -f "$apps_file" + rm -f "$apps_file" echo -e "${GRAY}Press Enter to return to application list, any other key to exit...${NC}" local key @@ -612,7 +848,6 @@ main() { return 0 fi - force_rescan=false done }