diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 43304e1..ba2f2aa 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -545,7 +545,44 @@ main() { local -a summary_rows=() local max_name_display_width=0 local max_size_width=0 - local name_trunc_limit=30 + local max_last_width=0 + # First pass: get actual max widths for all columns + for selected_app in "${selected_apps[@]}"; do + IFS='|' read -r _ _ app_name _ size last_used _ <<< "$selected_app" + 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" + [[ ${#size_display} -gt $max_size_width ]] && max_size_width=${#size_display} + local last_display=$(format_last_used_summary "$last_used") + [[ ${#last_display} -gt $max_last_width ]] && max_last_width=${#last_display} + done + ((max_size_width < 5)) && max_size_width=5 + ((max_last_width < 5)) && max_last_width=5 + + # Calculate name width: use actual max, but constrain by terminal width + # Fixed elements: "99. " (4) + " " (2) + " | Last: " (11) = 17 + local term_width=$(tput cols 2>/dev/null || echo 100) + local available_for_name=$((term_width - 17 - max_size_width - max_last_width)) + + # Dynamic minimum for better spacing on wide terminals + local min_name_width=24 + if [[ $term_width -ge 120 ]]; then + min_name_width=50 + elif [[ $term_width -ge 100 ]]; then + min_name_width=42 + elif [[ $term_width -ge 80 ]]; then + min_name_width=30 + fi + + # Constrain name width: dynamic min, max min(actual_max, available, 60) + local name_trunc_limit=$max_name_display_width + [[ $name_trunc_limit -lt $min_name_width ]] && name_trunc_limit=$min_name_width + [[ $name_trunc_limit -gt $available_for_name ]] && name_trunc_limit=$available_for_name + [[ $name_trunc_limit -gt 60 ]] && name_trunc_limit=60 + + # Reset for second pass + max_name_display_width=0 for selected_app in "${selected_apps[@]}"; do IFS='|' read -r epoch app_path app_name bundle_id size last_used size_kb <<< "$selected_app" @@ -554,17 +591,15 @@ main() { local display_name display_name=$(truncate_by_display_width "$app_name" "$name_trunc_limit") - # Get actual display width + # Track actual max width after truncation local current_width current_width=$(get_display_width "$display_name") - [[ $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" fi - [[ ${#size_display} -gt $max_size_width ]] && max_size_width=${#size_display} local last_display last_display=$(format_last_used_summary "$last_used") @@ -573,7 +608,6 @@ main() { done ((max_name_display_width < 16)) && max_name_display_width=16 - ((max_size_width < 5)) && max_size_width=5 local index=1 for row in "${summary_rows[@]}"; do diff --git a/bin/uninstall_lib.sh b/bin/uninstall_lib.sh index 9fcffd2..8531c54 100755 --- a/bin/uninstall_lib.sh +++ b/bin/uninstall_lib.sh @@ -545,7 +545,43 @@ main() { local -a summary_rows=() local max_name_width=0 local max_size_width=0 - local name_trunc_limit=30 + local max_last_width=0 + # First pass: get actual max widths for all columns + for selected_app in "${selected_apps[@]}"; do + IFS='|' read -r _ _ app_name _ size last_used _ <<< "$selected_app" + [[ ${#app_name} -gt $max_name_width ]] && max_name_width=${#app_name} + local size_display="$size" + [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]] && size_display="Unknown" + [[ ${#size_display} -gt $max_size_width ]] && max_size_width=${#size_display} + local last_display=$(format_last_used_summary "$last_used") + [[ ${#last_display} -gt $max_last_width ]] && max_last_width=${#last_display} + done + ((max_size_width < 5)) && max_size_width=5 + ((max_last_width < 5)) && max_last_width=5 + + # Calculate name width: use actual max, but constrain by terminal width + # Fixed elements: "99. " (4) + " " (2) + " | Last: " (11) = 17 + local term_width=$(tput cols 2>/dev/null || echo 100) + local available_for_name=$((term_width - 17 - max_size_width - max_last_width)) + + # Dynamic minimum for better spacing on wide terminals + local min_name_width=24 + if [[ $term_width -ge 120 ]]; then + min_name_width=50 + elif [[ $term_width -ge 100 ]]; then + min_name_width=42 + elif [[ $term_width -ge 80 ]]; then + min_name_width=30 + fi + + # Constrain name width: dynamic min, max min(actual_max, available, 60) + local name_trunc_limit=$max_name_width + [[ $name_trunc_limit -lt $min_name_width ]] && name_trunc_limit=$min_name_width + [[ $name_trunc_limit -gt $available_for_name ]] && name_trunc_limit=$available_for_name + [[ $name_trunc_limit -gt 60 ]] && name_trunc_limit=60 + + # Reset for second pass + max_name_width=0 for selected_app in "${selected_apps[@]}"; do IFS='|' read -r epoch app_path app_name bundle_id size last_used size_kb <<< "$selected_app" @@ -560,7 +596,6 @@ main() { if [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]]; then size_display="Unknown" fi - [[ ${#size_display} -gt $max_size_width ]] && max_size_width=${#size_display} local last_display last_display=$(format_last_used_summary "$last_used") @@ -569,7 +604,6 @@ main() { done ((max_name_width < 16)) && max_name_width=16 - ((max_size_width < 5)) && max_size_width=5 local index=1 for row in "${summary_rows[@]}"; do diff --git a/cmd/analyze/format.go b/cmd/analyze/format.go index 0c7a41e..6554781 100644 --- a/cmd/analyze/format.go +++ b/cmd/analyze/format.go @@ -161,9 +161,28 @@ func displayWidth(s string) int { return width } +// calculateNameWidth computes the optimal name column width based on terminal width. +// Fixed elements: prefix(3) + num(3) + bar(24) + percent(7) + sep(5) + icon(3) + size(12) + hint(4) = 61 +func calculateNameWidth(termWidth int) int { + const fixedWidth = 61 + available := termWidth - fixedWidth + + // Constrain to reasonable bounds + if available < 24 { + return 24 // Minimum for readability + } + if available > 60 { + return 60 // Maximum to avoid overly wide columns + } + return available +} + func trimName(name string) string { + return trimNameWithWidth(name, 45) // Default width for backward compatibility +} + +func trimNameWithWidth(name string, maxWidth int) string { const ( - maxWidth = 28 ellipsis = "..." ellipsisWidth = 3 ) diff --git a/cmd/analyze/view.go b/cmd/analyze/view.go index e76bb75..bd52a8b 100644 --- a/cmd/analyze/view.go +++ b/cmd/analyze/view.go @@ -119,11 +119,12 @@ func (m model) View() string { maxLargeSize = file.Size } } + nameWidth := calculateNameWidth(m.width) for idx := start; idx < end; idx++ { file := m.largeFiles[idx] shortPath := displayPath(file.Path) - shortPath = truncateMiddle(shortPath, 35) - paddedPath := padName(shortPath, 35) + shortPath = truncateMiddle(shortPath, nameWidth) + paddedPath := padName(shortPath, nameWidth) entryPrefix := " " nameColor := "" sizeColor := colorGray @@ -152,6 +153,7 @@ func (m model) View() string { } } totalSize := m.totalSize + nameWidth := calculateNameWidth(m.width) for idx, entry := range m.entries { icon := "📁" sizeVal := entry.Size @@ -188,8 +190,8 @@ func (m model) View() string { } } entryPrefix := " " - name := trimName(entry.Name) - paddedName := padName(name, 28) + name := trimNameWithWidth(entry.Name, nameWidth) + paddedName := padName(name, nameWidth) nameSegment := fmt.Sprintf("%s %s", icon, paddedName) numColor := "" percentColor := "" @@ -237,6 +239,7 @@ func (m model) View() string { } viewport := calculateViewport(m.height, false) + nameWidth := calculateNameWidth(m.width) start := m.offset if start < 0 { start = 0 @@ -253,8 +256,8 @@ func (m model) View() string { icon = "📁" } size := humanizeBytes(entry.Size) - name := trimName(entry.Name) - paddedName := padName(name, 28) + name := trimNameWithWidth(entry.Name, nameWidth) + paddedName := padName(name, nameWidth) // Calculate percentage percent := float64(entry.Size) / float64(m.totalSize) * 100 diff --git a/lib/ui/app_selector.sh b/lib/ui/app_selector.sh index 89fe63a..d73be87 100755 --- a/lib/ui/app_selector.sh +++ b/lib/ui/app_selector.sh @@ -18,14 +18,33 @@ format_app_display() { [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size" # Calculate available width for app name based on terminal width - # use passed width or calculate it (but calculation is slow in loops) + # Accept pre-calculated max_name_width (5th param) to avoid recalculation in loops local terminal_width="${4:-$(tput cols 2> /dev/null || echo 80)}" - local fixed_width=28 - local available_width=$((terminal_width - fixed_width)) + local max_name_width="${5:-}" + local available_width - # Set reasonable bounds for name width: 24-35 display width - [[ $available_width -lt 24 ]] && available_width=24 - [[ $available_width -gt 35 ]] && available_width=35 + if [[ -n "$max_name_width" ]]; then + # Use pre-calculated width from caller + available_width=$max_name_width + else + # Fallback: calculate it (slower, but works for standalone calls) + # Fixed elements: " ○ " (4) + " " (1) + size (9) + " | " (3) + max_last (7) = 24 + local fixed_width=24 + available_width=$((terminal_width - fixed_width)) + + # Dynamic minimum for better spacing on wide terminals + local min_width=18 + if [[ $terminal_width -ge 120 ]]; then + min_width=48 + elif [[ $terminal_width -ge 100 ]]; then + min_width=38 + elif [[ $terminal_width -ge 80 ]]; then + min_width=25 + fi + + [[ $available_width -lt $min_width ]] && available_width=$min_width + [[ $available_width -gt 60 ]] && available_width=60 + fi # Truncate long names if needed (based on display width, not char count) local truncated_name @@ -66,6 +85,31 @@ select_apps_for_uninstall() { fi fi + # Pre-scan to get actual max name width + local max_name_width=0 + for app_data in "${apps_data[@]}"; do + IFS='|' read -r _ _ display_name _ _ _ _ <<< "$app_data" + local name_width=$(get_display_width "$display_name") + [[ $name_width -gt $max_name_width ]] && max_name_width=$name_width + done + # Constrain based on terminal width: fixed=24, min varies by terminal width, max=60 + local fixed_width=24 + local available=$((terminal_width - fixed_width)) + + # Dynamic minimum: wider terminals get larger minimum for better spacing + local min_width=18 + if [[ $terminal_width -ge 120 ]]; then + min_width=48 # Wide terminals: very generous spacing + elif [[ $terminal_width -ge 100 ]]; then + min_width=38 # Medium-wide terminals: generous spacing + elif [[ $terminal_width -ge 80 ]]; then + min_width=25 # Standard terminals + fi + + [[ $max_name_width -lt $min_width ]] && max_name_width=$min_width + [[ $available -lt $max_name_width ]] && max_name_width=$available + [[ $max_name_width -gt 60 ]] && max_name_width=60 + local -a menu_options=() # Prepare metadata (comma-separated) for sorting/filtering inside the menu local epochs_csv="" @@ -74,7 +118,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" "$terminal_width")") + menu_options+=("$(format_app_display "$display_name" "$size" "$last_used" "$terminal_width" "$max_name_width")") # Build csv lists (avoid trailing commas) if [[ $idx -eq 0 ]]; then epochs_csv="${epoch:-0}" diff --git a/lib/ui/menu_paginated.sh b/lib/ui/menu_paginated.sh index fbae704..383e109 100755 --- a/lib/ui/menu_paginated.sh +++ b/lib/ui/menu_paginated.sh @@ -505,17 +505,75 @@ paginated_multi_select() { ) _print_wrapped_controls "$sep" "${_segs_all[@]}" else - # Normal: show full controls - local -a _segs_all=( + # Normal: show full controls (without O ↑/↓ to save space) + # Dynamically reduce controls if they won't fit + local term_width="${COLUMNS:-}" + [[ -z "$term_width" ]] && term_width=$(tput cols 2>/dev/null || echo 80) + + # Helper to calculate display length without ANSI codes + _calc_len() { + local text="$1" + local stripped + stripped=$(printf "%s" "$text" | LC_ALL=C awk '{gsub(/\033\[[0-9;]*[A-Za-z]/,""); print}') + printf "%d" "${#stripped}" + } + + # Build full controls and calculate total width + local -a _segs_full=( "${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}" "${GRAY}Space Select${NC}" "${GRAY}Enter${NC}" "${GRAY}R Refresh${NC}" "${GRAY}${filter_text}${NC}" "${GRAY}S ${sort_status}${NC}" - "${GRAY}O ${reverse_arrow}${NC}" "${GRAY}Q Exit${NC}" ) + + # Calculate total width with separators + local total_len=0 seg_count=${#_segs_full[@]} + local i + for i in "${!_segs_full[@]}"; do + total_len=$((total_len + $(_calc_len "${_segs_full[i]}"))) + # Add separator width (3 chars: " | ") + [[ $i -lt $((seg_count - 1)) ]] && total_len=$((total_len + 3)) + done + + # Multi-level fallback: progressively remove items if too wide + if [[ $total_len -gt $term_width ]]; then + # Level 1: Remove "R Refresh" (least critical) + local -a _segs_reduced=( + "${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}" + "${GRAY}Space Select${NC}" + "${GRAY}Enter${NC}" + "${GRAY}${filter_text}${NC}" + "${GRAY}S ${sort_status}${NC}" + "${GRAY}Q Exit${NC}" + ) + + # Recalculate length + total_len=0 + seg_count=${#_segs_reduced[@]} + for i in "${!_segs_reduced[@]}"; do + total_len=$((total_len + $(_calc_len "${_segs_reduced[i]}"))) + [[ $i -lt $((seg_count - 1)) ]] && total_len=$((total_len + 3)) + done + + # Level 2: If still too wide, also remove "S ${sort_status}" + if [[ $total_len -gt $term_width ]]; then + local -a _segs_all=( + "${GRAY}${ICON_NAV_UP}${ICON_NAV_DOWN}${NC}" + "${GRAY}Space Select${NC}" + "${GRAY}Enter${NC}" + "${GRAY}${filter_text}${NC}" + "${GRAY}Q Exit${NC}" + ) + else + local -a _segs_all=("${_segs_reduced[@]}") + fi + else + local -a _segs_all=("${_segs_full[@]}") + fi + _print_wrapped_controls "$sep" "${_segs_all[@]}" fi else