diff --git a/lib/core/base.sh b/lib/core/base.sh index d097cff..be82a09 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -30,16 +30,15 @@ readonly NC="${ESC}[0m" readonly ICON_CONFIRM="◎" readonly ICON_ADMIN="⚙" readonly ICON_SUCCESS="✓" -readonly ICON_ERROR="☻" +readonly ICON_ERROR="☹︎" +readonly ICON_WARNING="☺︎" readonly ICON_EMPTY="○" readonly ICON_SOLID="●" readonly ICON_LIST="•" readonly ICON_ARROW="➤" -readonly ICON_WARNING="☻" +readonly ICON_DRY_RUN="→" readonly ICON_NAV_UP="↑" readonly ICON_NAV_DOWN="↓" -readonly ICON_NAV_LEFT="←" -readonly ICON_NAV_RIGHT="→" # ============================================================================ # Global Configuration Constants @@ -53,6 +52,8 @@ readonly MOLE_LOG_AGE_DAYS=7 # Log retention (days) readonly MOLE_CRASH_REPORT_AGE_DAYS=7 # Crash report retention (days) readonly MOLE_SAVED_STATE_AGE_DAYS=7 # Saved state retention (days) readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # TM backup safety window (hours) +readonly MOLE_MAX_DS_STORE_FILES=500 # Max .DS_Store files to clean per scan +readonly MOLE_MAX_ORPHAN_ITERATIONS=100 # Max iterations for orphaned app data scan # ============================================================================ # Seasonal Functions @@ -566,3 +567,268 @@ note_activity() { SECTION_ACTIVITY=1 fi } + +# Start a section spinner with optional message +# Usage: start_section_spinner "message" +start_section_spinner() { + local message="${1:-Scanning...}" + stop_inline_spinner 2> /dev/null || true + if [[ -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "$message" + fi +} + +# Stop spinner and clear the line +# Usage: stop_section_spinner +stop_section_spinner() { + stop_inline_spinner 2> /dev/null || true + if [[ -t 1 ]]; then + echo -ne "\r\033[K" >&2 + fi +} + +# Safe terminal line clearing with terminal type detection +# Usage: safe_clear_lines [tty_device] +# Returns: 0 on success, 1 if terminal doesn't support ANSI +safe_clear_lines() { + local lines="${1:-1}" + local tty_device="${2:-/dev/tty}" + + # Use centralized ANSI support check (defined below) + # Note: This forward reference works because functions are parsed before execution + is_ansi_supported 2>/dev/null || return 1 + + # Clear lines one by one (more reliable than multi-line sequences) + local i + for ((i=0; i "$tty_device" 2>/dev/null || return 1 + done + + return 0 +} + +# Safe single line clear with fallback +# Usage: safe_clear_line [tty_device] +safe_clear_line() { + local tty_device="${1:-/dev/tty}" + + # Use centralized ANSI support check + is_ansi_supported 2>/dev/null || return 1 + + printf "\r\033[K" > "$tty_device" 2>/dev/null || return 1 + return 0 +} + +# Update progress spinner if enough time has elapsed +# Usage: update_progress_if_needed [interval] +# Example: update_progress_if_needed "$completed" "$total" last_progress_update 2 +# Returns: 0 if updated, 1 if skipped +update_progress_if_needed() { + local completed="$1" + local total="$2" + local last_update_var="$3" # Name of variable holding last update time + local interval="${4:-2}" # Default: update every 2 seconds + + # Get current time + local current_time=$(date +%s) + + # Get last update time from variable + local last_time + eval "last_time=\${$last_update_var:-0}" + + # Check if enough time has elapsed + if [[ $((current_time - last_time)) -ge $interval ]]; then + # Update the spinner with progress + stop_section_spinner + start_section_spinner "Scanning items... ($completed/$total)" + + # Update the last_update_time variable + eval "$last_update_var=$current_time" + return 0 + fi + + return 1 +} + +# ============================================================================ +# Spinner Stack Management (prevents nesting issues) +# ============================================================================ + +# Global spinner stack +declare -a MOLE_SPINNER_STACK=() + +# Push current spinner state onto stack +# Usage: push_spinner_state +push_spinner_state() { + local current_state="" + + # Save current spinner PID if running + if [[ -n "${MOLE_SPINNER_PID:-}" ]] && kill -0 "$MOLE_SPINNER_PID" 2>/dev/null; then + current_state="running:$MOLE_SPINNER_PID" + else + current_state="stopped" + fi + + MOLE_SPINNER_STACK+=("$current_state") + debug_log "Pushed spinner state: $current_state (stack depth: ${#MOLE_SPINNER_STACK[@]})" +} + +# Pop and restore spinner state from stack +# Usage: pop_spinner_state +pop_spinner_state() { + if [[ ${#MOLE_SPINNER_STACK[@]} -eq 0 ]]; then + debug_log "Warning: Attempted to pop from empty spinner stack" + return 1 + fi + + # Stack depth safety check + if [[ ${#MOLE_SPINNER_STACK[@]} -gt 10 ]]; then + debug_log "Warning: Spinner stack depth excessive (${#MOLE_SPINNER_STACK[@]}), possible leak" + fi + + local last_idx=$((${#MOLE_SPINNER_STACK[@]} - 1)) + local state="${MOLE_SPINNER_STACK[$last_idx]}" + + # Remove from stack (Bash 3.2 compatible way) + # Instead of unset, rebuild array without last element + local -a new_stack=() + local i + for ((i=0; i +safe_start_spinner() { + local message="${1:-Working...}" + + # Push current state + push_spinner_state + + # Stop any existing spinner + stop_section_spinner 2>/dev/null || true + + # Start new spinner + start_section_spinner "$message" +} + +# Safe spinner stop with stack management +# Usage: safe_stop_spinner +safe_stop_spinner() { + # Stop current spinner + stop_section_spinner 2>/dev/null || true + + # Pop previous state + pop_spinner_state || true +} + +# ============================================================================ +# Terminal Compatibility Checks +# ============================================================================ + +# Check if terminal supports ANSI escape codes +# Usage: is_ansi_supported +# Returns: 0 if supported, 1 if not +is_ansi_supported() { + # Check if running in interactive terminal + [[ -t 1 ]] || return 1 + + # Check TERM variable + [[ -n "${TERM:-}" ]] || return 1 + + # Check for known ANSI-compatible terminals + case "$TERM" in + xterm*|vt100|vt220|screen*|tmux*|ansi|linux|rxvt*|konsole*) + return 0 + ;; + dumb|unknown) + return 1 + ;; + *) + # Check terminfo database if available + if command -v tput >/dev/null 2>&1; then + # Test if terminal supports colors (good proxy for ANSI support) + local colors=$(tput colors 2>/dev/null || echo "0") + [[ "$colors" -ge 8 ]] && return 0 + fi + return 1 + ;; + esac +} + +# Get terminal capability info +# Usage: get_terminal_info +get_terminal_info() { + local info="Terminal: ${TERM:-unknown}" + + if is_ansi_supported; then + info+=" (ANSI supported)" + + if command -v tput >/dev/null 2>&1; then + local cols=$(tput cols 2>/dev/null || echo "?") + local lines=$(tput lines 2>/dev/null || echo "?") + local colors=$(tput colors 2>/dev/null || echo "?") + info+=" ${cols}x${lines}, ${colors} colors" + fi + else + info+=" (ANSI not supported)" + fi + + echo "$info" +} + +# Validate terminal environment before running +# Usage: validate_terminal_environment +# Returns: 0 if OK, 1 with warning if issues detected +validate_terminal_environment() { + local warnings=0 + + # Check if TERM is set + if [[ -z "${TERM:-}" ]]; then + log_warning "TERM environment variable not set" + ((warnings++)) + fi + + # Check if running in a known problematic terminal + case "${TERM:-}" in + dumb) + log_warning "Running in 'dumb' terminal - limited functionality" + ((warnings++)) + ;; + unknown) + log_warning "Terminal type unknown - may have display issues" + ((warnings++)) + ;; + esac + + # Check terminal size if available + if command -v tput >/dev/null 2>&1; then + local cols=$(tput cols 2>/dev/null || echo "80") + if [[ "$cols" -lt 60 ]]; then + log_warning "Terminal width ($cols cols) is narrow - output may wrap" + ((warnings++)) + fi + fi + + # Report compatibility + if [[ $warnings -eq 0 ]]; then + debug_log "Terminal environment validated: $(get_terminal_info)" + return 0 + else + debug_log "Terminal compatibility warnings: $warnings" + return 1 + fi +}