From 27205c653d414aba9fa6be308f07e4c4cda559f2 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 11:01:15 +0800 Subject: [PATCH] feat: Boost UI performance with pure bash string width calculation and truncation, and add visual feedback for cache hits in uninstall scripts. --- bin/uninstall.sh | 6 ++ bin/uninstall_lib.sh | 6 ++ lib/core/ui.sh | 152 +++++++++++++++++------------------------ lib/ui/app_selector.sh | 10 +-- 4 files changed, 80 insertions(+), 94 deletions(-) diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 4794a49..ef07cc3 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -83,6 +83,12 @@ scan_applications() { [[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file if [[ $cache_age -lt $cache_ttl ]]; then # Cache hit - return immediately + # Show brief flash of cache usage if in interactive mode + if [[ -t 2 ]]; then + echo -e "${GREEN}Loading from cache...${NC}" >&2 + # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) + sleep 0.3 + fi echo "$cache_file" return 0 fi diff --git a/bin/uninstall_lib.sh b/bin/uninstall_lib.sh index 6ec88f2..7cc1926 100755 --- a/bin/uninstall_lib.sh +++ b/bin/uninstall_lib.sh @@ -83,6 +83,12 @@ scan_applications() { [[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file if [[ $cache_age -lt $cache_ttl ]]; then # Cache hit - return immediately + # Show brief flash of cache usage if in interactive mode + if [[ -t 2 ]]; then + echo -e "${GREEN}Loading from cache...${NC}" >&2 + # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) + sleep 0.3 + fi echo "$cache_file" return 0 fi diff --git a/lib/core/ui.sh b/lib/core/ui.sh index 9908444..c367a1f 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -24,76 +24,50 @@ show_cursor() { [[ -t 1 ]] && printf '\033[?25h' >&2 || true; } get_display_width() { local str="$1" - # Check Python availability once and cache the result - # Use Python for accurate width calculation if available (cached check) - if [[ -z "${MOLE_PYTHON_AVAILABLE:-}" ]]; then - if command -v python3 > /dev/null 2>&1; then - export MOLE_PYTHON_AVAILABLE=1 - else - export MOLE_PYTHON_AVAILABLE=0 - fi - fi + # Optimized pure bash implementation without forks + local width - if [[ "${MOLE_PYTHON_AVAILABLE:-0}" == "1" ]]; then - python3 -c " -import sys -import unicodedata - -s = sys.argv[1] -width = 0 -for char in s: - # East Asian Width property - ea_width = unicodedata.east_asian_width(char) - if ea_width in ('F', 'W'): # Fullwidth or Wide - width += 2 - else: - width += 1 -print(width) -" "$str" 2> /dev/null && return - fi - - # Fallback: Use wc with UTF-8 locale temporarily - local saved_lc_all="${LC_ALL:-}" - local saved_lang="${LANG:-}" + # Save current locale + local old_lc="${LC_ALL:-}" + # Get Char Count (UTF-8) + # We must export ensuring it applies to the expansion (though just assignment often works in newer bash, export is safer for all subshells/cmds) export LC_ALL=en_US.UTF-8 - export LANG=en_US.UTF-8 + local char_count=${#str} - local char_count byte_count width - char_count=$(printf '%s' "$str" | wc -m 2> /dev/null | tr -d ' ') - byte_count=$(printf '%s' "$str" | wc -c 2> /dev/null | tr -d ' ') + # Get Byte Count (C) + export LC_ALL=C + local byte_count=${#str} - # Restore locale - if [[ -n "$saved_lc_all" ]]; then - export LC_ALL="$saved_lc_all" + # Restore Locale immediately + if [[ -n "$old_lc" ]]; then + export LC_ALL="$old_lc" else unset LC_ALL fi - if [[ -n "$saved_lang" ]]; then - export LANG="$saved_lang" - else - unset LANG + + if [[ $byte_count -eq $char_count ]]; then + echo "$char_count" + return fi - # Estimate: if byte_count > char_count, we have multibyte chars - # Rough approximation: each multibyte char (CJK) is ~3 bytes and width 2 - # ASCII chars are 1 byte and width 1 - if [[ $byte_count -gt $char_count ]]; then - local multibyte_chars=$((byte_count - char_count)) - # Assume most multibyte chars are 2 bytes extra (3 bytes total for UTF-8 CJK) - local cjk_chars=$((multibyte_chars / 2)) - local ascii_chars=$((char_count - cjk_chars)) - width=$((ascii_chars + cjk_chars * 2)) - else - width=$char_count - fi + # CJK Heuristic: + # Most CJK chars are 3 bytes in UTF-8 and width 2. + # ASCII chars are 1 byte and width 1. + # Width ~= CharCount + (ByteCount - CharCount) / 2 + # "δΈ­" (1 char, 3 bytes) -> 1 + (2)/2 = 2. + # "A" (1 char, 1 byte) -> 1 + 0 = 1. + # This is an approximation but very fast and sufficient for App names. + # Integer arithmetic in bash automatically handles floor. + local extra_bytes=$((byte_count - char_count)) + local padding=$((extra_bytes / 2)) + width=$((char_count + padding)) echo "$width" } # Truncate string by display width (handles CJK correctly) # Args: $1 - string, $2 - max display width -# Returns: truncated string with "..." if needed truncate_by_display_width() { local str="$1" local max_width="$2" @@ -105,45 +79,48 @@ truncate_by_display_width() { return fi - # Use Python for accurate truncation if available (use cached check) - if [[ "${MOLE_PYTHON_AVAILABLE:-0}" == "1" ]]; then - python3 -c " -import sys -import unicodedata -s = sys.argv[1] -max_w = int(sys.argv[2]) -result = '' -width = 0 + # Fallback: Use pure bash character iteration + # Since we need to know the width of *each* character to truncate at the right spot, + # we cannot just use the total width formula on the whole string. + # However, iterating char-by-char and calling the optimized get_display_width function + # is now much faster because it doesn't fork 'wc'. -for char in s: - ea_width = unicodedata.east_asian_width(char) - char_width = 2 if ea_width in ('F', 'W') else 1 - - if width + char_width + 3 > max_w: # +3 for '...' - break - - result += char - width += char_width - -print(result + '...') -" "$str" "$max_width" 2> /dev/null && return - fi - - # Fallback: Use UTF-8 locale for proper string handling - local saved_lc_all="${LC_ALL:-}" - local saved_lang="${LANG:-}" + # CRITICAL: Switch to UTF-8 for correct character iteration + local old_lc="${LC_ALL:-}" export LC_ALL=en_US.UTF-8 - export LANG=en_US.UTF-8 local truncated="" local width=0 local i=0 local char char_width + local strlen=${#str} # Re-calculate in UTF-8 - while [[ $i -lt ${#str} ]]; do + # Optimization: If total width <= max_width, return original string (checked above) + + while [[ $i -lt $strlen ]]; do char="${str:$i:1}" - char_width=$(get_display_width "$char") + + # Inlined width calculation for minimal overhead to avoid recursion overhead + # We are already in UTF-8, so ${#char} is char length (1). + # We need byte length for the heuristic. + # But switching locale inside loop is disastrous for perf. + # Logic: If char is ASCII (1 byte), width 1. + # If char is wide (3 bytes), width 2. + # How to detect byte size without switching locale? + # printf %s "$char" | wc -c ? Slow. + # Check against ASCII range? + # Fast ASCII check: if [[ "$char" < $'\x7f' ]]; then ... + + if [[ "$char" =~ [[:ascii:]] ]]; then + char_width=1 + else + # Assume wide for non-ascii in this context (simplified) + # Or use LC_ALL=C inside? No. + # Most non-ASCII in filenames are either CJK (width 2) or heavy symbols. + # Let's assume 2 for simplicity in this fast loop as we know we are usually dealing with CJK. + char_width=2 + fi if ((width + char_width + 3 > max_width)); then break @@ -155,16 +132,11 @@ print(result + '...') done # Restore locale - if [[ -n "$saved_lc_all" ]]; then - export LC_ALL="$saved_lc_all" + if [[ -n "$old_lc" ]]; then + export LC_ALL="$old_lc" else unset LC_ALL fi - if [[ -n "$saved_lang" ]]; then - export LANG="$saved_lang" - else - unset LANG - fi echo "${truncated}..." } diff --git a/lib/ui/app_selector.sh b/lib/ui/app_selector.sh index 7d402d4..49f8638 100755 --- a/lib/ui/app_selector.sh +++ b/lib/ui/app_selector.sh @@ -18,7 +18,8 @@ format_app_display() { [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size" # Calculate available width for app name based on terminal width - local terminal_width=$(tput cols 2> /dev/null || echo 80) + # use passed width or calculate it (but calculation is slow in loops) + local terminal_width="${4:-$(tput cols 2> /dev/null || echo 80)}" local fixed_width=28 local available_width=$((terminal_width - fixed_width)) @@ -58,7 +59,8 @@ select_apps_for_uninstall() { # Build menu options # Show loading for large lists (formatting can be slow due to width calculations) local app_count=${#apps_data[@]} - if [[ $app_count -gt 30 ]]; then + local terminal_width=$(tput cols 2> /dev/null || echo 80) + if [[ $app_count -gt 100 ]]; then if [[ -t 2 ]]; then printf "\rPreparing %d applications... " "$app_count" >&2 fi @@ -72,7 +74,7 @@ select_apps_for_uninstall() { for app_data in "${apps_data[@]}"; do # Keep extended field 7 (size_kb) if present IFS='|' read -r epoch _ display_name _ size last_used size_kb <<< "$app_data" - menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")") + menu_options+=("$(format_app_display "$display_name" "$size" "$last_used" "$terminal_width")") # Build csv lists (avoid trailing commas) if [[ $idx -eq 0 ]]; then epochs_csv="${epoch:-0}" @@ -85,7 +87,7 @@ select_apps_for_uninstall() { done # Clear loading message - if [[ $app_count -gt 30 ]]; then + if [[ $app_count -gt 100 ]]; then if [[ -t 2 ]]; then printf "\r\033[K" >&2 fi