1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 14:26:46 +00:00

feat: localize app names based on system language and improve UI display width calculation for CJK characters with loading indicator

This commit is contained in:
Tw93
2025-12-17 10:36:33 +08:00
parent 89febf8cf2
commit b843cde0fd
3 changed files with 174 additions and 168 deletions

View File

@@ -88,7 +88,13 @@ scan_applications() {
fi
fi
# Cache miss - show scanning feedback below
# Cache miss - prepare for scanning
local inline_loading=false
if [[ -t 1 && -t 2 ]]; then
inline_loading=true
# Clear screen for inline loading
printf "\033[2J\033[H" >&2
fi
local temp_file
temp_file=$(create_temp_file)
@@ -97,11 +103,7 @@ scan_applications() {
local current_epoch
current_epoch=$(date "+%s")
# Spinner for scanning feedback (simple ASCII for compatibility)
local spinner_chars="|/-\\"
local spinner_idx=0
# First pass: quickly collect all valid app paths and bundle IDs
# 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
@@ -118,78 +120,19 @@ scan_applications() {
continue
fi
# Try to get English name from bundle info, fallback to folder name
# Get bundle ID only (fast, no mdls calls in first pass)
local bundle_id="unknown"
local display_name="$app_name"
if [[ -f "$app_path/Contents/Info.plist" ]]; then
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown")
# Try to get English name from bundle info
local bundle_executable
bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null)
# Smart display name selection - prefer descriptive names over generic ones
local candidates=()
# Get all potential names
local bundle_display_name
bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null)
local bundle_name
bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null)
# Check if executable name is generic/technical (should be avoided)
local is_generic_executable=false
if [[ -n "$bundle_executable" ]]; then
case "$bundle_executable" in
"pake" | "Electron" | "electron" | "nwjs" | "node" | "helper" | "main" | "app" | "binary")
is_generic_executable=true
;;
esac
fi
# Priority order for name selection:
# 1. App folder name (if ASCII and descriptive) - often the most complete name
if [[ "$app_name" =~ ^[A-Za-z0-9\ ._-]+$ && ${#app_name} -gt 3 ]]; then
candidates+=("$app_name")
fi
# 2. CFBundleDisplayName (if meaningful and ASCII)
if [[ -n "$bundle_display_name" && "$bundle_display_name" =~ ^[A-Za-z0-9\ ._-]+$ ]]; then
candidates+=("$bundle_display_name")
fi
# 3. CFBundleName (if meaningful and ASCII)
if [[ -n "$bundle_name" && "$bundle_name" =~ ^[A-Za-z0-9\ ._-]+$ && "$bundle_name" != "$bundle_display_name" ]]; then
candidates+=("$bundle_name")
fi
# 4. CFBundleExecutable (only if not generic and ASCII)
if [[ -n "$bundle_executable" && "$bundle_executable" =~ ^[A-Za-z0-9._-]+$ && "$is_generic_executable" == false ]]; then
candidates+=("$bundle_executable")
fi
# 5. Fallback to non-ASCII names if no ASCII found
if [[ ${#candidates[@]} -eq 0 ]]; then
[[ -n "$bundle_display_name" ]] && candidates+=("$bundle_display_name")
[[ -n "$bundle_name" && "$bundle_name" != "$bundle_display_name" ]] && candidates+=("$bundle_name")
candidates+=("$app_name")
fi
# Select the first (best) candidate
display_name="${candidates[0]:-$app_name}"
# Apply brand name mapping from common.sh
display_name="$(get_brand_name "$display_name")"
fi
# Skip system critical apps (input methods, system components)
# Note: Paid apps like CleanMyMac, 1Password are NOT protected here - users can uninstall them
if should_protect_from_uninstall "$bundle_id"; then
continue
fi
# Store tuple: app_path|app_name|bundle_id|display_name
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
# 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/)
@@ -209,11 +152,7 @@ scan_applications() {
max_parallel=32
fi
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
# inline_loading variable already set above (line ~92)
# Process app metadata extraction function
process_app_metadata() {
@@ -221,7 +160,35 @@ scan_applications() {
local output_file="$2"
local current_epoch="$3"
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple"
# Get localized display name (moved from first pass for better performance)
local display_name="$app_name"
if [[ -f "$app_path/Contents/Info.plist" ]]; then
# Try to get localized name from system metadata (best for i18n)
local md_display_name
md_display_name=$(run_with_timeout 0.05 mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "")
# Get bundle names
local bundle_display_name
bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null)
local bundle_name
bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null)
# Priority order for name selection (prefer localized names):
# 1. System metadata display name (kMDItemDisplayName) - respects system language
# 2. CFBundleDisplayName - usually localized
# 3. CFBundleName - fallback
# 4. App folder name - last resort
if [[ -n "$md_display_name" && "$md_display_name" != "(null)" && "$md_display_name" != "$app_name" ]]; then
display_name="$md_display_name"
elif [[ -n "$bundle_display_name" && "$bundle_display_name" != "(null)" ]]; then
display_name="$bundle_display_name"
elif [[ -n "$bundle_name" && "$bundle_name" != "(null)" ]]; then
display_name="$bundle_name"
fi
fi
# Parallel size calculation
local app_size="N/A"
@@ -293,9 +260,9 @@ scan_applications() {
local completed=$(cat "$progress_file" 2> /dev/null || echo 0)
local c="${spinner_chars:$((i % 4)):1}"
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2
printf "\033[H\033[2K%s Scanning applications... %d/%d\n" "$c" "$completed" "$total_apps" >&2
else
echo -ne "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2
printf "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2
fi
((i++))
sleep 0.1 2> /dev/null || sleep 1
@@ -346,12 +313,30 @@ scan_applications() {
fi
# Sort by last used (oldest first) and cache the result
# Show brief processing message for large app lists
if [[ $total_apps -gt 50 ]]; then
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2KProcessing %d applications...\n" "$total_apps" >&2
else
printf "\rProcessing %d applications... " "$total_apps" >&2
fi
fi
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || {
rm -f "$temp_file"
return 1
}
rm -f "$temp_file"
# Clear processing message
if [[ $total_apps -gt 50 ]]; then
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K" >&2
else
printf "\r\033[K" >&2
fi
fi
# Save to cache (simplified - no metadata)
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true
@@ -555,18 +540,22 @@ main() {
# Show selected apps with clean alignment
echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):"
local -a summary_rows=()
local max_name_width=0
local max_name_display_width=0
local max_size_width=0
local name_trunc_limit=30
for selected_app in "${selected_apps[@]}"; do
IFS='|' read -r epoch app_path app_name bundle_id size last_used size_kb <<< "$selected_app"
local display_name="$app_name"
if [[ ${#display_name} -gt $name_trunc_limit ]]; then
display_name="${display_name:0:$((name_trunc_limit - 3))}..."
fi
[[ ${#display_name} -gt $max_name_width ]] && max_name_width=${#display_name}
# Truncate by display width if needed
local display_name
display_name=$(truncate_by_display_width "$app_name" "$name_trunc_limit")
# Get actual display width
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
@@ -580,13 +569,20 @@ main() {
summary_rows+=("$display_name|$size_display|$last_display")
done
((max_name_width < 16)) && max_name_width=16
((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
IFS='|' read -r name_cell size_cell last_cell <<< "$row"
printf "%d. %-*s %*s | Last: %s\n" "$index" "$max_name_width" "$name_cell" "$max_size_width" "$size_cell" "$last_cell"
# Calculate printf width based on actual display width
local name_display_width
name_display_width=$(get_display_width "$name_cell")
local name_char_count=${#name_cell}
local padding_needed=$((max_name_display_width - name_display_width))
local printf_name_width=$((name_char_count + padding_needed))
printf "%d. %-*s %*s | Last: %s\n" "$index" "$printf_name_width" "$name_cell" "$max_size_width" "$size_cell" "$last_cell"
((index++))
done

View File

@@ -88,7 +88,13 @@ scan_applications() {
fi
fi
# Cache miss - show scanning feedback below
# Cache miss - prepare for scanning
local inline_loading=false
if [[ -t 1 && -t 2 ]]; then
inline_loading=true
# Clear screen for inline loading
printf "\033[2J\033[H" >&2
fi
local temp_file
temp_file=$(create_temp_file)
@@ -97,11 +103,7 @@ scan_applications() {
local current_epoch
current_epoch=$(date "+%s")
# Spinner for scanning feedback (simple ASCII for compatibility)
local spinner_chars="|/-\\"
local spinner_idx=0
# First pass: quickly collect all valid app paths and bundle IDs
# 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
@@ -118,78 +120,19 @@ scan_applications() {
continue
fi
# Try to get English name from bundle info, fallback to folder name
# Get bundle ID only (fast, no mdls calls in first pass)
local bundle_id="unknown"
local display_name="$app_name"
if [[ -f "$app_path/Contents/Info.plist" ]]; then
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown")
# Try to get English name from bundle info
local bundle_executable
bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null)
# Smart display name selection - prefer descriptive names over generic ones
local candidates=()
# Get all potential names
local bundle_display_name
bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null)
local bundle_name
bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null)
# Check if executable name is generic/technical (should be avoided)
local is_generic_executable=false
if [[ -n "$bundle_executable" ]]; then
case "$bundle_executable" in
"pake" | "Electron" | "electron" | "nwjs" | "node" | "helper" | "main" | "app" | "binary")
is_generic_executable=true
;;
esac
fi
# Priority order for name selection:
# 1. App folder name (if ASCII and descriptive) - often the most complete name
if [[ "$app_name" =~ ^[A-Za-z0-9\ ._-]+$ && ${#app_name} -gt 3 ]]; then
candidates+=("$app_name")
fi
# 2. CFBundleDisplayName (if meaningful and ASCII)
if [[ -n "$bundle_display_name" && "$bundle_display_name" =~ ^[A-Za-z0-9\ ._-]+$ ]]; then
candidates+=("$bundle_display_name")
fi
# 3. CFBundleName (if meaningful and ASCII)
if [[ -n "$bundle_name" && "$bundle_name" =~ ^[A-Za-z0-9\ ._-]+$ && "$bundle_name" != "$bundle_display_name" ]]; then
candidates+=("$bundle_name")
fi
# 4. CFBundleExecutable (only if not generic and ASCII)
if [[ -n "$bundle_executable" && "$bundle_executable" =~ ^[A-Za-z0-9._-]+$ && "$is_generic_executable" == false ]]; then
candidates+=("$bundle_executable")
fi
# 5. Fallback to non-ASCII names if no ASCII found
if [[ ${#candidates[@]} -eq 0 ]]; then
[[ -n "$bundle_display_name" ]] && candidates+=("$bundle_display_name")
[[ -n "$bundle_name" && "$bundle_name" != "$bundle_display_name" ]] && candidates+=("$bundle_name")
candidates+=("$app_name")
fi
# Select the first (best) candidate
display_name="${candidates[0]:-$app_name}"
# Apply brand name mapping from common.sh
display_name="$(get_brand_name "$display_name")"
fi
# Skip system critical apps (input methods, system components)
# Note: Paid apps like CleanMyMac, 1Password are NOT protected here - users can uninstall them
if should_protect_from_uninstall "$bundle_id"; then
continue
fi
# Store tuple: app_path|app_name|bundle_id|display_name
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
# 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/)
@@ -209,11 +152,7 @@ scan_applications() {
max_parallel=32
fi
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
# inline_loading variable already set above (line ~92)
# Process app metadata extraction function
process_app_metadata() {
@@ -221,7 +160,35 @@ scan_applications() {
local output_file="$2"
local current_epoch="$3"
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple"
# Get localized display name (moved from first pass for better performance)
local display_name="$app_name"
if [[ -f "$app_path/Contents/Info.plist" ]]; then
# Try to get localized name from system metadata (best for i18n)
local md_display_name
md_display_name=$(run_with_timeout 0.05 mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "")
# Get bundle names
local bundle_display_name
bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null)
local bundle_name
bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null)
# Priority order for name selection (prefer localized names):
# 1. System metadata display name (kMDItemDisplayName) - respects system language
# 2. CFBundleDisplayName - usually localized
# 3. CFBundleName - fallback
# 4. App folder name - last resort
if [[ -n "$md_display_name" && "$md_display_name" != "(null)" && "$md_display_name" != "$app_name" ]]; then
display_name="$md_display_name"
elif [[ -n "$bundle_display_name" && "$bundle_display_name" != "(null)" ]]; then
display_name="$bundle_display_name"
elif [[ -n "$bundle_name" && "$bundle_name" != "(null)" ]]; then
display_name="$bundle_name"
fi
fi
# Parallel size calculation
local app_size="N/A"
@@ -293,9 +260,9 @@ scan_applications() {
local completed=$(cat "$progress_file" 2> /dev/null || echo 0)
local c="${spinner_chars:$((i % 4)):1}"
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2
printf "\033[H\033[2K%s Scanning applications... %d/%d\n" "$c" "$completed" "$total_apps" >&2
else
echo -ne "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2
printf "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2
fi
((i++))
sleep 0.1 2> /dev/null || sleep 1
@@ -346,12 +313,30 @@ scan_applications() {
fi
# Sort by last used (oldest first) and cache the result
# Show brief processing message for large app lists
if [[ $total_apps -gt 50 ]]; then
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2KProcessing %d applications...\n" "$total_apps" >&2
else
printf "\rProcessing %d applications... " "$total_apps" >&2
fi
fi
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || {
rm -f "$temp_file"
return 1
}
rm -f "$temp_file"
# Clear processing message
if [[ $total_apps -gt 50 ]]; then
if [[ $inline_loading == true ]]; then
printf "\033[H\033[2K" >&2
else
printf "\r\033[K" >&2
fi
fi
# Save to cache (simplified - no metadata)
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true

View File

@@ -3,6 +3,8 @@
set -euo pipefail
# Note: get_display_width() is now defined in lib/core/ui.sh
# Format app info for display
format_app_display() {
local display_name="$1" size="$2" last_used="$3"
@@ -20,18 +22,26 @@ format_app_display() {
local fixed_width=28
local available_width=$((terminal_width - fixed_width))
# Set reasonable bounds for name width: 24-35 chars
# Set reasonable bounds for name width: 24-35 display width
[[ $available_width -lt 24 ]] && available_width=24
[[ $available_width -gt 35 ]] && available_width=35
# Truncate long names if needed
local truncated_name="$display_name"
if [[ ${#display_name} -gt $available_width ]]; then
truncated_name="${display_name:0:$((available_width - 3))}..."
fi
# Truncate long names if needed (based on display width, not char count)
local truncated_name
truncated_name=$(truncate_by_display_width "$display_name" "$available_width")
# Use dynamic column width for better readability
printf "%-*s %9s | %s" "$available_width" "$truncated_name" "$size_str" "$compact_last_used"
# Get actual display width after truncation
local current_display_width
current_display_width=$(get_display_width "$truncated_name")
# Calculate padding needed
# Formula: char_count + (available_width - display_width) = padding to add
local char_count=${#truncated_name}
local padding_needed=$((available_width - current_display_width))
local printf_width=$((char_count + padding_needed))
# Use dynamic column width with corrected padding
printf "%-*s %9s | %s" "$printf_width" "$truncated_name" "$size_str" "$compact_last_used"
}
# Global variable to store selection result (bash 3.2 compatible)
@@ -46,6 +56,14 @@ select_apps_for_uninstall() {
fi
# 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
if [[ -t 2 ]]; then
printf "\rPreparing %d applications... " "$app_count" >&2
fi
fi
local -a menu_options=()
# Prepare metadata (comma-separated) for sorting/filtering inside the menu
local epochs_csv=""
@@ -66,6 +84,13 @@ select_apps_for_uninstall() {
((idx++))
done
# Clear loading message
if [[ $app_count -gt 30 ]]; then
if [[ -t 2 ]]; then
printf "\r\033[K" >&2
fi
fi
# Expose metadata for the paginated menu (optional inputs)
# - MOLE_MENU_META_EPOCHS: numeric last_used_epoch per item
# - MOLE_MENU_META_SIZEKB: numeric size in KB per item