mirror of
https://github.com/tw93/Mole.git
synced 2026-02-16 23:24:11 +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:
164
bin/uninstall.sh
164
bin/uninstall.sh
@@ -88,7 +88,13 @@ scan_applications() {
|
|||||||
fi
|
fi
|
||||||
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
|
local temp_file
|
||||||
temp_file=$(create_temp_file)
|
temp_file=$(create_temp_file)
|
||||||
@@ -97,11 +103,7 @@ scan_applications() {
|
|||||||
local current_epoch
|
local current_epoch
|
||||||
current_epoch=$(date "+%s")
|
current_epoch=$(date "+%s")
|
||||||
|
|
||||||
# Spinner for scanning feedback (simple ASCII for compatibility)
|
# First pass: quickly collect all valid app paths and bundle IDs (NO mdls calls)
|
||||||
local spinner_chars="|/-\\"
|
|
||||||
local spinner_idx=0
|
|
||||||
|
|
||||||
# First pass: quickly collect all valid app paths and bundle IDs
|
|
||||||
local -a app_data_tuples=()
|
local -a app_data_tuples=()
|
||||||
while IFS= read -r -d '' app_path; do
|
while IFS= read -r -d '' app_path; do
|
||||||
if [[ ! -e "$app_path" ]]; then continue; fi
|
if [[ ! -e "$app_path" ]]; then continue; fi
|
||||||
@@ -118,78 +120,19 @@ scan_applications() {
|
|||||||
continue
|
continue
|
||||||
fi
|
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 bundle_id="unknown"
|
||||||
local display_name="$app_name"
|
|
||||||
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
||||||
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown")
|
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
|
fi
|
||||||
|
|
||||||
# Skip system critical apps (input methods, system components)
|
# 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
|
if should_protect_from_uninstall "$bundle_id"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Store tuple: 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}|${display_name}")
|
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}")
|
||||||
done < <(
|
done < <(
|
||||||
# Scan both system and user application directories
|
# Scan both system and user application directories
|
||||||
# Using maxdepth 3 to find apps in subdirectories (e.g., Adobe apps in /Applications/Adobe X/)
|
# 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
|
max_parallel=32
|
||||||
fi
|
fi
|
||||||
local pids=()
|
local pids=()
|
||||||
local inline_loading=false
|
# inline_loading variable already set above (line ~92)
|
||||||
if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then
|
|
||||||
inline_loading=true
|
|
||||||
printf "\033[H" >&2 # Position cursor at top of screen
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Process app metadata extraction function
|
# Process app metadata extraction function
|
||||||
process_app_metadata() {
|
process_app_metadata() {
|
||||||
@@ -221,7 +160,35 @@ scan_applications() {
|
|||||||
local output_file="$2"
|
local output_file="$2"
|
||||||
local current_epoch="$3"
|
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
|
# Parallel size calculation
|
||||||
local app_size="N/A"
|
local app_size="N/A"
|
||||||
@@ -293,9 +260,9 @@ scan_applications() {
|
|||||||
local completed=$(cat "$progress_file" 2> /dev/null || echo 0)
|
local completed=$(cat "$progress_file" 2> /dev/null || echo 0)
|
||||||
local c="${spinner_chars:$((i % 4)):1}"
|
local c="${spinner_chars:$((i % 4)):1}"
|
||||||
if [[ $inline_loading == true ]]; then
|
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
|
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
|
fi
|
||||||
((i++))
|
((i++))
|
||||||
sleep 0.1 2> /dev/null || sleep 1
|
sleep 0.1 2> /dev/null || sleep 1
|
||||||
@@ -346,12 +313,30 @@ scan_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Sort by last used (oldest first) and cache the result
|
# 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" || {
|
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || {
|
||||||
rm -f "$temp_file"
|
rm -f "$temp_file"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
rm -f "$temp_file"
|
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)
|
# Save to cache (simplified - no metadata)
|
||||||
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true
|
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true
|
||||||
|
|
||||||
@@ -555,18 +540,22 @@ main() {
|
|||||||
# Show selected apps with clean alignment
|
# Show selected apps with clean alignment
|
||||||
echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):"
|
echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):"
|
||||||
local -a summary_rows=()
|
local -a summary_rows=()
|
||||||
local max_name_width=0
|
local max_name_display_width=0
|
||||||
local max_size_width=0
|
local max_size_width=0
|
||||||
local name_trunc_limit=30
|
local name_trunc_limit=30
|
||||||
|
|
||||||
for selected_app in "${selected_apps[@]}"; do
|
for selected_app in "${selected_apps[@]}"; do
|
||||||
IFS='|' read -r epoch app_path app_name bundle_id size last_used size_kb <<< "$selected_app"
|
IFS='|' read -r epoch app_path app_name bundle_id size last_used size_kb <<< "$selected_app"
|
||||||
|
|
||||||
local display_name="$app_name"
|
# Truncate by display width if needed
|
||||||
if [[ ${#display_name} -gt $name_trunc_limit ]]; then
|
local display_name
|
||||||
display_name="${display_name:0:$((name_trunc_limit - 3))}..."
|
display_name=$(truncate_by_display_width "$app_name" "$name_trunc_limit")
|
||||||
fi
|
|
||||||
[[ ${#display_name} -gt $max_name_width ]] && max_name_width=${#display_name}
|
# 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"
|
local size_display="$size"
|
||||||
if [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]]; then
|
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")
|
summary_rows+=("$display_name|$size_display|$last_display")
|
||||||
done
|
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
|
((max_size_width < 5)) && max_size_width=5
|
||||||
|
|
||||||
local index=1
|
local index=1
|
||||||
for row in "${summary_rows[@]}"; do
|
for row in "${summary_rows[@]}"; do
|
||||||
IFS='|' read -r name_cell size_cell last_cell <<< "$row"
|
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++))
|
((index++))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ scan_applications() {
|
|||||||
fi
|
fi
|
||||||
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
|
local temp_file
|
||||||
temp_file=$(create_temp_file)
|
temp_file=$(create_temp_file)
|
||||||
@@ -97,11 +103,7 @@ scan_applications() {
|
|||||||
local current_epoch
|
local current_epoch
|
||||||
current_epoch=$(date "+%s")
|
current_epoch=$(date "+%s")
|
||||||
|
|
||||||
# Spinner for scanning feedback (simple ASCII for compatibility)
|
# First pass: quickly collect all valid app paths and bundle IDs (NO mdls calls)
|
||||||
local spinner_chars="|/-\\"
|
|
||||||
local spinner_idx=0
|
|
||||||
|
|
||||||
# First pass: quickly collect all valid app paths and bundle IDs
|
|
||||||
local -a app_data_tuples=()
|
local -a app_data_tuples=()
|
||||||
while IFS= read -r -d '' app_path; do
|
while IFS= read -r -d '' app_path; do
|
||||||
if [[ ! -e "$app_path" ]]; then continue; fi
|
if [[ ! -e "$app_path" ]]; then continue; fi
|
||||||
@@ -118,78 +120,19 @@ scan_applications() {
|
|||||||
continue
|
continue
|
||||||
fi
|
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 bundle_id="unknown"
|
||||||
local display_name="$app_name"
|
|
||||||
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
||||||
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown")
|
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
|
fi
|
||||||
|
|
||||||
# Skip system critical apps (input methods, system components)
|
# 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
|
if should_protect_from_uninstall "$bundle_id"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Store tuple: 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}|${display_name}")
|
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}")
|
||||||
done < <(
|
done < <(
|
||||||
# Scan both system and user application directories
|
# Scan both system and user application directories
|
||||||
# Using maxdepth 3 to find apps in subdirectories (e.g., Adobe apps in /Applications/Adobe X/)
|
# 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
|
max_parallel=32
|
||||||
fi
|
fi
|
||||||
local pids=()
|
local pids=()
|
||||||
local inline_loading=false
|
# inline_loading variable already set above (line ~92)
|
||||||
if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then
|
|
||||||
inline_loading=true
|
|
||||||
printf "\033[H" >&2 # Position cursor at top of screen
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Process app metadata extraction function
|
# Process app metadata extraction function
|
||||||
process_app_metadata() {
|
process_app_metadata() {
|
||||||
@@ -221,7 +160,35 @@ scan_applications() {
|
|||||||
local output_file="$2"
|
local output_file="$2"
|
||||||
local current_epoch="$3"
|
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
|
# Parallel size calculation
|
||||||
local app_size="N/A"
|
local app_size="N/A"
|
||||||
@@ -293,9 +260,9 @@ scan_applications() {
|
|||||||
local completed=$(cat "$progress_file" 2> /dev/null || echo 0)
|
local completed=$(cat "$progress_file" 2> /dev/null || echo 0)
|
||||||
local c="${spinner_chars:$((i % 4)):1}"
|
local c="${spinner_chars:$((i % 4)):1}"
|
||||||
if [[ $inline_loading == true ]]; then
|
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
|
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
|
fi
|
||||||
((i++))
|
((i++))
|
||||||
sleep 0.1 2> /dev/null || sleep 1
|
sleep 0.1 2> /dev/null || sleep 1
|
||||||
@@ -346,12 +313,30 @@ scan_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Sort by last used (oldest first) and cache the result
|
# 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" || {
|
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || {
|
||||||
rm -f "$temp_file"
|
rm -f "$temp_file"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
rm -f "$temp_file"
|
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)
|
# Save to cache (simplified - no metadata)
|
||||||
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true
|
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Note: get_display_width() is now defined in lib/core/ui.sh
|
||||||
|
|
||||||
# Format app info for display
|
# Format app info for display
|
||||||
format_app_display() {
|
format_app_display() {
|
||||||
local display_name="$1" size="$2" last_used="$3"
|
local display_name="$1" size="$2" last_used="$3"
|
||||||
@@ -20,18 +22,26 @@ format_app_display() {
|
|||||||
local fixed_width=28
|
local fixed_width=28
|
||||||
local available_width=$((terminal_width - fixed_width))
|
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 -lt 24 ]] && available_width=24
|
||||||
[[ $available_width -gt 35 ]] && available_width=35
|
[[ $available_width -gt 35 ]] && available_width=35
|
||||||
|
|
||||||
# Truncate long names if needed
|
# Truncate long names if needed (based on display width, not char count)
|
||||||
local truncated_name="$display_name"
|
local truncated_name
|
||||||
if [[ ${#display_name} -gt $available_width ]]; then
|
truncated_name=$(truncate_by_display_width "$display_name" "$available_width")
|
||||||
truncated_name="${display_name:0:$((available_width - 3))}..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use dynamic column width for better readability
|
# Get actual display width after truncation
|
||||||
printf "%-*s %9s | %s" "$available_width" "$truncated_name" "$size_str" "$compact_last_used"
|
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)
|
# Global variable to store selection result (bash 3.2 compatible)
|
||||||
@@ -46,6 +56,14 @@ select_apps_for_uninstall() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Build menu options
|
# 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=()
|
local -a menu_options=()
|
||||||
# Prepare metadata (comma-separated) for sorting/filtering inside the menu
|
# Prepare metadata (comma-separated) for sorting/filtering inside the menu
|
||||||
local epochs_csv=""
|
local epochs_csv=""
|
||||||
@@ -66,6 +84,13 @@ select_apps_for_uninstall() {
|
|||||||
((idx++))
|
((idx++))
|
||||||
done
|
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)
|
# Expose metadata for the paginated menu (optional inputs)
|
||||||
# - MOLE_MENU_META_EPOCHS: numeric last_used_epoch per item
|
# - MOLE_MENU_META_EPOCHS: numeric last_used_epoch per item
|
||||||
# - MOLE_MENU_META_SIZEKB: numeric size in KB per item
|
# - MOLE_MENU_META_SIZEKB: numeric size in KB per item
|
||||||
|
|||||||
Reference in New Issue
Block a user