diff --git a/bin/check.sh b/bin/check.sh index 8225d60..24e4594 100755 --- a/bin/check.sh +++ b/bin/check.sh @@ -16,13 +16,20 @@ source "$SCRIPT_DIR/lib/manage/autofix.sh" source "$SCRIPT_DIR/lib/check/all.sh" cleanup_all() { + stop_inline_spinner 2> /dev/null || true stop_sudo_session cleanup_temp_files } +handle_interrupt() { + cleanup_all + exit 130 +} + main() { # Register unified cleanup handler - trap cleanup_all EXIT INT TERM + trap cleanup_all EXIT + trap handle_interrupt INT TERM if [[ -t 1 ]]; then clear diff --git a/bin/optimize.sh b/bin/optimize.sh index 9c9f687..49d6ceb 100755 --- a/bin/optimize.sh +++ b/bin/optimize.sh @@ -319,10 +319,16 @@ perform_security_fixes() { } cleanup_all() { + stop_inline_spinner 2> /dev/null || true stop_sudo_session cleanup_temp_files } +handle_interrupt() { + cleanup_all + exit 130 +} + main() { local health_json for arg in "$@"; do @@ -340,7 +346,8 @@ main() { esac done - trap cleanup_all EXIT INT TERM + trap cleanup_all EXIT + trap handle_interrupt INT TERM if [[ -t 1 ]]; then clear diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 8b1b2ca..8befe5c 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -77,6 +77,23 @@ scan_applications() { "/Applications" "$HOME/Applications" ) + local vol_app_dir + local nullglob_was_set=0 + shopt -q nullglob && nullglob_was_set=1 + shopt -s nullglob + for vol_app_dir in /Volumes/*/Applications; do + [[ -d "$vol_app_dir" && -r "$vol_app_dir" ]] || continue + if [[ -d "/Applications" && "$vol_app_dir" -ef "/Applications" ]]; then + continue + fi + if [[ -d "$HOME/Applications" && "$vol_app_dir" -ef "$HOME/Applications" ]]; then + continue + fi + app_dirs+=("$vol_app_dir") + done + if [[ $nullglob_was_set -eq 0 ]]; then + shopt -u nullglob + fi for app_dir in "${app_dirs[@]}"; do if [[ ! -d "$app_dir" ]]; then continue; fi diff --git a/bin/uninstall_lib.sh b/bin/uninstall_lib.sh index 43035db..badd927 100755 --- a/bin/uninstall_lib.sh +++ b/bin/uninstall_lib.sh @@ -111,40 +111,61 @@ scan_applications() { # First pass: quickly collect all valid app paths and bundle IDs (NO mdls calls) local -a app_data_tuples=() - while IFS= read -r -d '' app_path; do - if [[ ! -e "$app_path" ]]; then continue; fi - - local app_name - app_name=$(basename "$app_path" .app) - - # Skip nested apps (e.g. inside Wrapper/ or Frameworks/ of another app) - # Check if parent path component ends in .app (e.g. /Foo.app/Bar.app or /Foo.app/Contents/Bar.app) - # This prevents false positives like /Old.apps/Target.app - local parent_dir - parent_dir=$(dirname "$app_path") - if [[ "$parent_dir" == *".app" || "$parent_dir" == *".app/"* ]]; then - continue - fi - - # Get bundle ID only (fast, no mdls calls in first pass) - 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 - - # Skip system critical apps (input methods, system components) - if should_protect_from_uninstall "$bundle_id"; then - continue - fi - - # Store tuple: app_path|app_name|bundle_id (display_name will be resolved in parallel later) - app_data_tuples+=("${app_path}|${app_name}|${bundle_id}") - done < <( - # Scan both system and user application directories - # Using maxdepth 3 to find apps in subdirectories (e.g., Adobe apps in /Applications/Adobe X/) - command find /Applications -name "*.app" -maxdepth 3 -print0 2> /dev/null - command find ~/Applications -name "*.app" -maxdepth 3 -print0 2> /dev/null + local -a app_dirs=( + "/Applications" + "$HOME/Applications" ) + local vol_app_dir + local nullglob_was_set=0 + shopt -q nullglob && nullglob_was_set=1 + shopt -s nullglob + for vol_app_dir in /Volumes/*/Applications; do + [[ -d "$vol_app_dir" && -r "$vol_app_dir" ]] || continue + if [[ -d "/Applications" && "$vol_app_dir" -ef "/Applications" ]]; then + continue + fi + if [[ -d "$HOME/Applications" && "$vol_app_dir" -ef "$HOME/Applications" ]]; then + continue + fi + app_dirs+=("$vol_app_dir") + done + if [[ $nullglob_was_set -eq 0 ]]; then + shopt -u nullglob + fi + + for app_dir in "${app_dirs[@]}"; do + if [[ ! -d "$app_dir" ]]; then continue; fi + + while IFS= read -r -d '' app_path; do + if [[ ! -e "$app_path" ]]; then continue; fi + + local app_name + app_name=$(basename "$app_path" .app) + + # Skip nested apps (e.g. inside Wrapper/ or Frameworks/ of another app) + # Check if parent path component ends in .app (e.g. /Foo.app/Bar.app or /Foo.app/Contents/Bar.app) + # This prevents false positives like /Old.apps/Target.app + local parent_dir + parent_dir=$(dirname "$app_path") + if [[ "$parent_dir" == *".app" || "$parent_dir" == *".app/"* ]]; then + continue + fi + + # Get bundle ID only (fast, no mdls calls in first pass) + 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 + + # Skip system critical apps (input methods, system components) + if should_protect_from_uninstall "$bundle_id"; then + continue + fi + + # Store tuple: app_path|app_name|bundle_id (display_name will be resolved in parallel later) + app_data_tuples+=("${app_path}|${app_name}|${bundle_id}") + done < <(command find "$app_dir" -name "*.app" -maxdepth 3 -print0 2> /dev/null) + done # Second pass: process each app with parallel size calculation local app_count=0 diff --git a/lib/optimize/tasks.sh b/lib/optimize/tasks.sh index 9ee0746..2fe2ddc 100644 --- a/lib/optimize/tasks.sh +++ b/lib/optimize/tasks.sh @@ -174,8 +174,18 @@ opt_saved_state_cleanup() { # Removed: opt_local_snapshots - Deletes user Time Machine recovery points, breaks backup continuity opt_fix_broken_configs() { + local spinner_started="false" + if [[ -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking preferences..." + spinner_started="true" + fi + local broken_prefs=$(fix_broken_preferences) + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi + if [[ $broken_prefs -gt 0 ]]; then opt_msg "Repaired $broken_prefs corrupted preference files" else @@ -324,7 +334,7 @@ opt_sqlite_vacuum() { fi if [[ $skipped -gt 0 ]]; then - echo -e " ${GRAY}Already optimal for $skipped databases (size or integrity limits)${NC}" + opt_msg "Already optimal for $skipped databases" fi if [[ $timed_out -gt 0 ]]; then @@ -520,8 +530,17 @@ opt_disk_permissions_repair() { # 1. Checks if default audio output is Bluetooth (precise) # 2. Falls back to Bluetooth + media app detection (compatibility) opt_bluetooth_reset() { + local spinner_started="false" + if [[ -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking Bluetooth..." + spinner_started="true" + fi + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then if has_bluetooth_hid_connected; then + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi opt_msg "Bluetooth already optimal" return 0 fi @@ -557,6 +576,9 @@ opt_bluetooth_reset() { fi if [[ "$bt_audio_active" == "true" ]]; then + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi opt_msg "Bluetooth already optimal" return 0 fi @@ -567,12 +589,21 @@ opt_bluetooth_reset() { if pgrep -x bluetoothd > /dev/null 2>&1; then sudo pkill -KILL bluetoothd > /dev/null 2>&1 || true fi + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi opt_msg "Bluetooth module restarted" opt_msg "Connectivity issues resolved" else + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi opt_msg "Bluetooth already optimal" fi else + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi opt_msg "Bluetooth module restarted" opt_msg "Connectivity issues resolved" fi diff --git a/lib/ui/menu_paginated.sh b/lib/ui/menu_paginated.sh index 3ebbe94..1238b1d 100755 --- a/lib/ui/menu_paginated.sh +++ b/lib/ui/menu_paginated.sh @@ -235,11 +235,13 @@ paginated_multi_select() { local cols="${COLUMNS:-}" [[ -z "$cols" ]] && cols=$(tput cols 2> /dev/null || echo 80) + [[ "$cols" =~ ^[0-9]+$ ]] || cols=80 _strip_ansi_len() { local text="$1" local stripped - stripped=$(printf "%s" "$text" | LC_ALL=C awk '{gsub(/\033\[[0-9;]*[A-Za-z]/,""); print}') + stripped=$(printf "%s" "$text" | LC_ALL=C awk '{gsub(/\033\[[0-9;]*[A-Za-z]/,""); print}' || true) + [[ -z "$stripped" ]] && stripped="$text" printf "%d" "${#stripped}" } @@ -251,7 +253,10 @@ paginated_multi_select() { else candidate="$line${sep}${s}" fi - if (($(_strip_ansi_len "$candidate") > cols)); then + local candidate_len + candidate_len=$(_strip_ansi_len "$candidate") + [[ -z "$candidate_len" ]] && candidate_len=0 + if ((candidate_len > cols)); then printf "%s%s\n" "$clear_line" "$line" >&2 line="$s" else @@ -526,6 +531,7 @@ paginated_multi_select() { # Normal: show full controls with dynamic reduction local term_width="${COLUMNS:-}" [[ -z "$term_width" ]] && term_width=$(tput cols 2> /dev/null || echo 80) + [[ "$term_width" =~ ^[0-9]+$ ]] || term_width=80 # Level 0: Full controls local -a _segs=("$nav" "$space_select" "$enter" "$refresh" "$search" "$sort_ctrl" "$order_ctrl" "$exit")