diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 572eb69..03a0bf2 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -108,15 +108,14 @@ scan_applications() { find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' ' ) - # Check if cache is valid + # Check if cache is valid unless explicitly disabled if [[ -f "$cache_file" && -f "$cache_meta" ]]; then local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0") # Cache is valid if: age < TTL AND app count matches if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then - # Only show cache info in debug mode - [[ -n "${MOLE_DEBUG:-}" ]] && echo "Using cached app list (${cache_age}s old, $current_app_count apps) ✓" >&2 + # Silent - cache hit, no need to show progress echo "$cache_file" return 0 fi @@ -124,7 +123,6 @@ scan_applications() { local temp_file=$(create_temp_file) - echo "" >&2 # Add space before scanning output without breaking stdout return # Pre-cache current epoch to avoid repeated calls local current_epoch=$(date "+%s") @@ -219,6 +217,11 @@ scan_applications() { local total_apps=${#app_data_tuples[@]} local max_parallel=10 # Process 10 apps in parallel local pids=() + local inline_loading=false + if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then + inline_loading=true + printf "\033[H" >&2 # Position cursor at top of screen + fi # Process app metadata extraction function process_app_metadata() { @@ -296,7 +299,11 @@ scan_applications() { # Update progress with spinner local spinner_char="${spinner_chars:$((spinner_idx % 4)):1}" - echo -ne "\r\033[K ${spinner_char} Scanning applications... $app_count/$total_apps" >&2 + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2K${spinner_char} Scanning applications... %d/%d" "$app_count" "$total_apps" >&2 + else + echo -ne "\r\033[K${spinner_char} Scanning applications... $app_count/$total_apps" >&2 + fi ((spinner_idx++)) # Wait if we've hit max parallel limit @@ -311,15 +318,22 @@ scan_applications() { wait "$pid" 2>/dev/null done - echo -e "\r\033[K ✓ Found $app_count applications" >&2 - echo "" >&2 - # Check if we found any applications if [[ ! -s "$temp_file" ]]; then + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2K" >&2 + else + echo -ne "\r\033[K" >&2 + fi + echo "No applications found to uninstall" >&2 rm -f "$temp_file" return 1 fi + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2K" >&2 + fi + # Sort by last used (oldest first) and cache the result sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file"; return 1; } rm -f "$temp_file" @@ -388,13 +402,13 @@ uninstall_applications() { echo "" - # Check if app is running - if pgrep -f "$app_name" >/dev/null 2>&1; then + # Check if app is running (use app path for precise matching) + if pgrep -f "$app_path" >/dev/null 2>&1; then echo -e "${YELLOW}⚠ $app_name is currently running${NC}" read -p " Force quit $app_name? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then - pkill -f "$app_name" 2>/dev/null || true + pkill -f "$app_path" 2>/dev/null || true sleep 2 else echo -e " ${BLUE}○${NC} Skipped $app_name" @@ -509,6 +523,10 @@ uninstall_applications() { # Cleanup function - restore cursor and clean up cleanup() { # Restore cursor using common function + if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then + leave_alt_screen + unset MOLE_ALT_SCREEN_ACTIVE + fi show_cursor exit "${1:-0}" } @@ -518,30 +536,78 @@ trap cleanup EXIT INT TERM # Main function main() { + local use_inline_loading=false + if [[ -t 1 && -t 2 ]]; then + use_inline_loading=true + fi + # Hide cursor during operation hide_cursor + if [[ $use_inline_loading == true ]]; then + enter_alt_screen + export MOLE_ALT_SCREEN_ACTIVE=1 + export MOLE_INLINE_LOADING=1 + export MOLE_MANAGED_ALT_SCREEN=1 + printf "\033[2J\033[H" >&2 + else + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN MOLE_ALT_SCREEN_ACTIVE + fi + # Scan applications - local apps_file=$(scan_applications) + local apps_file="" + if ! apps_file=$(scan_applications); then + if [[ $use_inline_loading == true ]]; then + printf "\033[2J\033[H" >&2 + leave_alt_screen + unset MOLE_ALT_SCREEN_ACTIVE + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN + fi + return 1 + fi + + if [[ $use_inline_loading == true ]]; then + printf "\033[2J\033[H" >&2 + fi if [[ ! -f "$apps_file" ]]; then - echo "" - log_error "Failed to scan applications" + # Error message already shown by scan_applications + if [[ $use_inline_loading == true ]]; then + leave_alt_screen + unset MOLE_ALT_SCREEN_ACTIVE + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN + fi return 1 fi # Load applications if ! load_applications "$apps_file"; then + if [[ $use_inline_loading == true ]]; then + leave_alt_screen + unset MOLE_ALT_SCREEN_ACTIVE + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN + fi rm -f "$apps_file" return 1 fi # Interactive selection using paginated menu if ! select_apps_for_uninstall; then + if [[ $use_inline_loading == true ]]; then + leave_alt_screen + unset MOLE_ALT_SCREEN_ACTIVE + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN + fi rm -f "$apps_file" return 0 fi + if [[ $use_inline_loading == true ]]; then + leave_alt_screen + unset MOLE_ALT_SCREEN_ACTIVE + unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN + fi + # Restore cursor and show a concise summary before confirmation show_cursor clear diff --git a/lib/app_selector.sh b/lib/app_selector.sh index 7f12058..55d3ff0 100755 --- a/lib/app_selector.sh +++ b/lib/app_selector.sh @@ -37,10 +37,8 @@ select_apps_for_uninstall() { menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")") done - # Clear screen before menu (alternate screen preserves main screen) - clear_screen - # Use paginated menu - result will be stored in MOLE_SELECTION_RESULT + # Note: paginated_multi_select enters alternate screen and handles clearing MOLE_SELECTION_RESULT="" paginated_multi_select "Select Apps to Remove" "${menu_options[@]}" local exit_code=$? diff --git a/lib/batch_uninstall.sh b/lib/batch_uninstall.sh index 6b75501..6cd6cdc 100755 --- a/lib/batch_uninstall.sh +++ b/lib/batch_uninstall.sh @@ -24,6 +24,7 @@ batch_uninstall_applications() { local -a sudo_apps=() local total_estimated_size=0 local -a app_details=() + local -a dock_cleanup_paths=() echo "" # Silent analysis without spinner output (avoid visual flicker) @@ -31,8 +32,8 @@ batch_uninstall_applications() { [[ -z "$selected_app" ]] && continue IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" - # Check if app is running - if pgrep -f "$app_name" >/dev/null 2>&1; then + # Check if app is running (use app path for precise matching) + if pgrep -f "$app_path" >/dev/null 2>&1; then running_apps+=("$app_name") fi @@ -49,14 +50,46 @@ batch_uninstall_applications() { ((total_estimated_size += total_kb)) # Store details for later use - # Base64 encode related_files to handle multi-line data safely - local encoded_files=$(echo "$related_files" | base64) + # Base64 encode related_files to handle multi-line data safely (single line) + local encoded_files + encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n') app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files") done # Format size display (convert KB to bytes for bytes_to_human()) local size_display=$(bytes_to_human "$((total_estimated_size * 1024))") + # Display detailed file list for each app before confirmation + echo -e "${PURPLE}Files to be removed:${NC}" + echo "" + for detail in "${app_details[@]}"; do + IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail" + local related_files=$(printf '%s' "$encoded_files" | base64 -d) + local app_size_display=$(bytes_to_human "$((total_kb * 1024))") + + echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name} ${GRAY}(${app_size_display})${NC}" + echo -e " ${GREEN}✓${NC} $(echo "$app_path" | sed "s|$HOME|~|")" + + # Show related files (limit to 5 most important ones for brevity) + local file_count=0 + local max_files=5 + while IFS= read -r file; do + if [[ -n "$file" && -e "$file" ]]; then + if [[ $file_count -lt $max_files ]]; then + echo -e " ${GREEN}✓${NC} $(echo "$file" | sed "s|$HOME|~|")" + fi + ((file_count++)) + fi + done <<< "$related_files" + + # Show count of remaining files if truncated + if [[ $file_count -gt $max_files ]]; then + local remaining=$((file_count - max_files)) + echo -e " ${GRAY} ... and ${remaining} more files${NC}" + fi + echo "" + done + # Show summary and get batch confirmation first (before asking for password) local app_total=${#selected_apps[@]} local app_text="app" @@ -100,12 +133,7 @@ batch_uninstall_applications() { if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi # Force quit running apps first (batch) - if [[ ${#running_apps[@]} -gt 0 ]]; then - pkill -f "${running_apps[0]}" 2>/dev/null || true - for app_name in "${running_apps[@]:1}"; do pkill -f "$app_name" 2>/dev/null || true; done - sleep 2 - if pgrep -f "${running_apps[0]}" >/dev/null 2>&1; then sleep 1; fi - fi + # Note: Apps are already killed in the individual uninstall loop below with app_path for precise matching # Perform uninstallations (silent mode, show results at end) if [[ -t 1 ]]; then stop_inline_spinner; fi @@ -114,11 +142,11 @@ batch_uninstall_applications() { local -a success_items=() for detail in "${app_details[@]}"; do IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail" - local related_files=$(echo "$encoded_files" | base64 -d) + local related_files=$(printf '%s' "$encoded_files" | base64 -d) local reason="" local needs_sudo=false [[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]] && needs_sudo=true - if ! force_kill_app "$app_name"; then + if ! force_kill_app "$app_name" "$app_path"; then reason="still running" fi if [[ -z "$reason" ]]; then @@ -139,6 +167,7 @@ batch_uninstall_applications() { ((files_cleaned++)) ((total_items++)) success_items+=("$app_name") + dock_cleanup_paths+=("$app_path") else ((failed_count++)) failed_items+=("$app_name:$reason") @@ -148,7 +177,6 @@ batch_uninstall_applications() { # Summary local freed_display=$(bytes_to_human "$((total_size_freed * 1024))") local bar="================================================================================" - echo "" echo "$bar" if [[ $success_count -gt 0 ]]; then local success_list="${success_items[*]}" @@ -179,6 +207,10 @@ batch_uninstall_applications() { fi echo "$bar" + if [[ ${#dock_cleanup_paths[@]} -gt 0 ]]; then + remove_apps_from_dock "${dock_cleanup_paths[@]}" + fi + # Clean up sudo keepalive if it was started if [[ -n "${sudo_keepalive_pid:-}" ]]; then kill "$sudo_keepalive_pid" 2>/dev/null || true diff --git a/lib/common.sh b/lib/common.sh index c484084..47140f3 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -268,14 +268,38 @@ bytes_to_human() { fi if ((bytes >= 1073741824)); then # >= 1GB - echo "$bytes" | awk '{printf "%.2fGB", $1/1073741824}' - elif ((bytes >= 1048576)); then # >= 1MB - echo "$bytes" | awk '{printf "%.1fMB", $1/1048576}' - elif ((bytes >= 1024)); then # >= 1KB - echo "$bytes" | awk '{printf "%.0fKB", $1/1024}' - else - echo "${bytes}B" + local divisor=1073741824 + local whole=$((bytes / divisor)) + local remainder=$((bytes % divisor)) + local frac=$(( (remainder * 100 + divisor / 2) / divisor )) # Two decimals, rounded + if ((frac >= 100)); then + frac=0 + ((whole++)) + fi + printf "%d.%02dGB\n" "$whole" "$frac" + return 0 fi + + if ((bytes >= 1048576)); then # >= 1MB + local divisor=1048576 + local whole=$((bytes / divisor)) + local remainder=$((bytes % divisor)) + local frac=$(( (remainder * 10 + divisor / 2) / divisor )) # One decimal, rounded + if ((frac >= 10)); then + frac=0 + ((whole++)) + fi + printf "%d.%01dMB\n" "$whole" "$frac" + return 0 + fi + + if ((bytes >= 1024)); then # >= 1KB + local rounded_kb=$(((bytes + 512) / 1024)) # Nearest integer KB + printf "%dKB\n" "$rounded_kb" + return 0 + fi + + printf "%dB\n" "$bytes" } # Calculate directory size in bytes @@ -726,17 +750,130 @@ mktemp_dir() { local d; d=$(mktemp -d) || return 1; register_temp_dir "$d"; ech # Uninstall helper abstractions # ========================================================================= force_kill_app() { - # Args: app_name; tries graceful then force kill; returns 0 if stopped, 1 otherwise - local app="$1" - if pgrep -f "$app" >/dev/null 2>&1; then - pkill -f "$app" 2>/dev/null || true + # Args: app_name [app_path]; tries graceful then force kill; returns 0 if stopped, 1 otherwise + local app_name="$1" + local app_path="${2:-}" + + # Use app path for precise matching if provided + local match_pattern="$app_name" + if [[ -n "$app_path" && -e "$app_path" ]]; then + # Use the app bundle path for more precise matching + match_pattern="$app_path" + fi + + if pgrep -f "$match_pattern" >/dev/null 2>&1; then + pkill -f "$match_pattern" 2>/dev/null || true sleep 1 fi - if pgrep -f "$app" >/dev/null 2>&1; then - pkill -9 -f "$app" 2>/dev/null || true + if pgrep -f "$match_pattern" >/dev/null 2>&1; then + pkill -9 -f "$match_pattern" 2>/dev/null || true sleep 1 fi - pgrep -f "$app" >/dev/null 2>&1 && return 1 || return 0 + pgrep -f "$match_pattern" >/dev/null 2>&1 && return 1 || return 0 +} + +# Remove application icons from the Dock (best effort) +remove_apps_from_dock() { + if [[ $# -eq 0 ]]; then + return 0 + fi + + local plist="$HOME/Library/Preferences/com.apple.dock.plist" + [[ -f "$plist" ]] || return 0 + + if ! command -v python3 >/dev/null 2>&1; then + return 0 + fi + + # Execute Python helper to prune dock entries for the given app paths. + # Exit status 2 means entries were removed. + local target_count=$# + + python3 - "$@" <<'PY' +import os +import plistlib +import subprocess +import sys +import urllib.parse + +plist_path = os.path.expanduser('~/Library/Preferences/com.apple.dock.plist') +if not os.path.exists(plist_path): + sys.exit(0) + +def normalise(path): + if not path: + return '' + return os.path.normpath(os.path.realpath(path.rstrip('/'))) + +targets = {normalise(arg) for arg in sys.argv[1:] if arg} +targets = {t for t in targets if t} +if not targets: + sys.exit(0) + +with open(plist_path, 'rb') as fh: + try: + data = plistlib.load(fh) + except Exception: + sys.exit(0) + +apps = data.get('persistent-apps') +if not isinstance(apps, list): + sys.exit(0) + +changed = False +filtered = [] +for item in apps: + try: + url = item['tile-data']['file-data']['_CFURLString'] + except (KeyError, TypeError): + filtered.append(item) + continue + + if not isinstance(url, str): + filtered.append(item) + continue + + parsed = urllib.parse.urlparse(url) + path = urllib.parse.unquote(parsed.path or '') + if not path: + filtered.append(item) + continue + + candidate = normalise(path) + if any(candidate == t or candidate.startswith(t + os.sep) for t in targets): + changed = True + continue + + filtered.append(item) + +if not changed: + sys.exit(0) + +data['persistent-apps'] = filtered +with open(plist_path, 'wb') as fh: + try: + plistlib.dump(data, fh, fmt=plistlib.FMT_BINARY) + except Exception: + plistlib.dump(data, fh) + +# Restart Dock to apply changes (ignore errors) +try: + subprocess.run(['killall', 'Dock'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) +except Exception: + pass + +sys.exit(2) +PY + local python_status=$? + if [[ $python_status -eq 2 ]]; then + if [[ $target_count -gt 1 ]]; then + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icons from Dock" + else + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icon from Dock" + fi + return 0 + fi + return $python_status } map_uninstall_reason() { diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh index 25cc247..4e8e940 100755 --- a/lib/paginated_menu.sh +++ b/lib/paginated_menu.sh @@ -12,6 +12,10 @@ paginated_multi_select() { local title="$1" shift local -a items=("$@") + local external_alt_screen=false + if [[ "${MOLE_MANAGED_ALT_SCREEN:-}" == "1" || "${MOLE_MANAGED_ALT_SCREEN:-}" == "true" ]]; then + external_alt_screen=true + fi # Validation if [[ ${#items[@]} -eq 0 ]]; then @@ -55,11 +59,14 @@ paginated_multi_select() { else stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true fi - leave_alt_screen + if [[ "${external_alt_screen:-false}" == false ]]; then + leave_alt_screen + fi } # Cleanup function cleanup() { + trap - EXIT INT TERM restore_terminal } @@ -74,9 +81,13 @@ paginated_multi_select() { # Setup terminal - preserve interrupt character stty -echo -icanon intr ^C 2>/dev/null || true - enter_alt_screen - # Clear screen once on entry to alt screen - printf "\033[2J\033[H" >&2 + if [[ $external_alt_screen == false ]]; then + enter_alt_screen + # Clear screen once on entry to alt screen + printf "\033[2J\033[H" >&2 + else + printf "\033[H" >&2 + fi hide_cursor # Helper functions @@ -84,11 +95,11 @@ paginated_multi_select() { render_item() { local idx=$1 is_current=$2 - local checkbox="☐" - [[ ${selected[idx]} == true ]] && checkbox="☑" + local checkbox="[ ]" + [[ ${selected[idx]} == true ]] && checkbox="[x]" if [[ $is_current == true ]]; then - printf "\r\033[2K\033[7m▶ %s %s\033[0m\n" "$checkbox" "${items[idx]}" >&2 + printf "\r\033[2K${BLUE}> %s %s${NC}\n" "$checkbox" "${items[idx]}" >&2 else printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2 fi @@ -168,7 +179,10 @@ EOF local key=$(read_key) case "$key" in - "QUIT") cleanup; return 1 ;; + "QUIT") + cleanup + return 1 + ;; "UP") if [[ $cursor_pos -gt 0 ]]; then ((cursor_pos--)) diff --git a/mole b/mole index f4087bf..c89b44a 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/common.sh" # Version info -VERSION="1.7.3" +VERSION="1.7.4" MOLE_TAGLINE="can dig deep to clean your Mac." # Get latest version from remote repository @@ -400,28 +400,53 @@ remove_mole() { exit 0 } -# Display main menu options +# Display main menu options with minimal refresh to avoid flicker show_main_menu() { local selected="${1:-1}" - local full_draw="${2:-true}" + local _full_draw="${2:-true}" # Kept for compatibility (unused) + local banner="${MAIN_MENU_BANNER:-}" + local update_message="${MAIN_MENU_UPDATE_MESSAGE:-}" - # Full redraw each time (prevents ghost menu items) - clear_screen - echo "" - show_brand_banner - show_update_notification - echo "" + # Fallback if globals missing (should not happen) + if [[ -z "$banner" ]]; then + banner="$(show_brand_banner)" + MAIN_MENU_BANNER="$banner" + fi - show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)" - show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)" - show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)" - show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)" - show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)" + printf '\033[H' # Move cursor to home + + local line="" + # Leading spacer + printf '\r\033[2K\n' + + # Brand banner + while IFS= read -r line || [[ -n "$line" ]]; do + printf '\r\033[2K%s\n' "$line" + done <<< "$banner" + + # Update notification block (if present) + if [[ -n "$update_message" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + printf '\r\033[2K%s\n' "$line" + done <<< "$update_message" + fi + + # Spacer before menu options + printf '\r\033[2K\n' + + printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)")" if [[ -t 0 ]]; then - echo "" - echo -e " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit" + printf '\r\033[2K\n' + printf '\r\033[2K%s\n' " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit" fi + + # Clear any remaining content below without full screen wipe + printf '\033[J' } # Interactive main menu loop @@ -439,6 +464,18 @@ interactive_main_menu() { fi local current_option=1 local first_draw=true + local brand_banner="" + local msg_cache="$HOME/.cache/mole/update_message" + local update_message="" + + brand_banner="$(show_brand_banner)" + MAIN_MENU_BANNER="$brand_banner" + + if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then + update_message="$(cat "$msg_cache" 2>/dev/null || echo "")" + fi + MAIN_MENU_UPDATE_MESSAGE="$update_message" + cleanup_and_exit() { show_cursor echo ""