mirror of
https://github.com/tw93/Mole.git
synced 2026-02-05 13:48:48 +00:00
Merge branch 'main' into dev
This commit is contained in:
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -40,10 +40,11 @@ Paste the debug output here
|
||||
|
||||
## Environment
|
||||
|
||||
- Mole version: (run `mo --version`)
|
||||
- macOS version: (run `sw_vers`)
|
||||
- Installation method: (Homebrew / curl script)
|
||||
- Architecture: (Intel / Apple Silicon)
|
||||
Please run `mo update` to ensure you are on the latest version, then paste the output of `mo --version` below:
|
||||
|
||||
```text
|
||||
Paste mo --version output here
|
||||
```
|
||||
|
||||
## Additional context
|
||||
|
||||
|
||||
34
README.md
34
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<h1>Mole</h1>
|
||||
<p><em>Dig deep like a mole to optimize your Mac.</em></p>
|
||||
<p><em>Deep clean and optimize your Mac.</em></p>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
@@ -18,15 +18,15 @@
|
||||
|
||||
## Features
|
||||
|
||||
- **All-in-one toolkit** equal to CleanMyMac + AppCleaner + DaisyDisk + Sensei + iStat in one **trusted binary**
|
||||
- **Deep cleanup** finds and removes caches, temp files, browser leftovers, and junk to **free up tens of gigabytes**
|
||||
- **Smart uninstall** finds app bundles plus launch agents, settings, caches, logs, and **leftover files**
|
||||
- **Disk insight + optimization** show large files, display folders, **rebuild caches**, clean swap, refresh services
|
||||
- **Live status** shows CPU, GPU, memory, disk, network, battery, and proxy data so you can **find problems**
|
||||
- **All-in-one toolkit** combining the power of CleanMyMac, AppCleaner, DaisyDisk, Sensei, and iStat in one **trusted binary**
|
||||
- **Deep cleanup** scans and removes caches, logs, browser leftovers, and junk to **reclaim tens of gigabytes**
|
||||
- **Smart uninstall** completely removes apps including launch agents, preferences, caches, and **hidden leftovers**
|
||||
- **Disk insight + optimization** visualizes usage, handles large files, **rebuilds caches**, cleans swap, and refreshes services
|
||||
- **Live status** monitors CPU, GPU, memory, disk, network, battery, and proxy stats to **diagnose issues**
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Install:**
|
||||
**Installation:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/tw93/mole/main/install.sh | bash
|
||||
@@ -63,12 +63,12 @@ mo optimize --whitelist # Adjust protected optimization items
|
||||
|
||||
## Tips
|
||||
|
||||
- **Terminal**: iTerm2 has known compatibility issues, use Alacritty, kitty, WezTerm, Ghostty, or Warp instead
|
||||
- **Safety**: Built with strict protections. See our [Security Audit](SECURITY_AUDIT.md). Preview with `mo clean --dry-run`
|
||||
- **Whitelist**: Use `mo clean --whitelist` to manage protected caches
|
||||
- **Touch ID**: Run `mo touchid` to approve sudo with Touch ID instead of password
|
||||
- **Navigation**: All menus support Vim keys `h/j/k/l` in addition to arrow keys
|
||||
- **Debug**: Use `--debug` flag to see detailed logs: `mo clean --debug`
|
||||
- **Terminal**: iTerm2 has known compatibility issues; we recommend Alacritty, kitty, WezTerm, Ghostty, or Warp.
|
||||
- **Safety**: Built with strict protections. See our [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`.
|
||||
- **Whitelist**: Manage protected paths with `mo clean --whitelist`.
|
||||
- **Touch ID**: Enable Touch ID for sudo commands by running `mo touchid`.
|
||||
- **Navigation**: Supports standard arrow keys and Vim bindings (`h/j/k/l`).
|
||||
- **Debug**: View detailed logs by appending the `--debug` flag (e.g., `mo clean --debug`).
|
||||
|
||||
## Features in Detail
|
||||
|
||||
@@ -195,10 +195,10 @@ Adds 5 commands: `clean`, `uninstall`, `optimize`, `analyze`, `status`. Finds yo
|
||||
|
||||
<a href="https://miaoyan.app/cats.html?name=Mole"><img src="https://miaoyan.app/assets/sponsors.svg" width="1000px" /></a>
|
||||
|
||||
- If Mole freed storage for you, consider starring the repo or sharing it with friends needing a cleaner Mac.
|
||||
- Have ideas or fixes? Open an issue or PR and help shape Mole's future together with the community.
|
||||
- Report bugs by running commands with `--debug` flag and sharing the output: `mo clean --debug`
|
||||
- Love cats? Treat Tangyuan and Cola to canned food via <a href="https://miaoyan.app/cats.html?name=Mole" target="_blank">this link</a> and keep the mascots purring.
|
||||
- If Mole saved you space, consider starring the repo or sharing it with friends who need a cleaner Mac.
|
||||
- Have ideas or fixes? Open an issue or PR to help shape Mole's future with the community.
|
||||
|
||||
- Love cats? Treat Tangyuan and Cola to canned food via <a href="https://miaoyan.app/cats.html?name=Mole" target="_blank">this link</a> to keep our mascots purring.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
BIN
bin/analyze-go
BIN
bin/analyze-go
Binary file not shown.
@@ -223,7 +223,7 @@ safe_clean() {
|
||||
|
||||
# Hard-coded protection for critical apps (cannot be disabled by user)
|
||||
case "$path" in
|
||||
*clash* | *Clash* | *surge* | *Surge* | *mihomo* | *openvpn* | *OpenVPN* | *verge* | *Verge* | *shadowsocks* | *Shadowsocks* | *v2ray* | *V2Ray* | *sing-box* | *tailscale* | *nordvpn* | *NordVPN* | *expressvpn* | *ExpressVPN* | *protonvpn* | *ProtonVPN* | *mullvad* | *Mullvad* | *hiddify* | *Hiddify* | *loon* | *Loon* | *Cursor* | *cursor* | *Claude* | *claude* | *ChatGPT* | *chatgpt* | *Ollama* | *ollama* | *lmstudio* | *Chatbox* | *Gemini* | *gemini* | *Perplexity* | *perplexity* | *Windsurf* | *windsurf* | *Poe* | *poe* | *DiffusionBee* | *diffusionbee* | *DrawThings* | *drawthings*)
|
||||
*clash* | *Clash* | *surge* | *Surge* | *mihomo* | *openvpn* | *OpenVPN* | *verge* | *Verge* | *shadowsocks* | *Shadowsocks* | *v2ray* | *V2Ray* | *sing-box* | *tailscale* | *nordvpn* | *NordVPN* | *expressvpn* | *ExpressVPN* | *protonvpn* | *ProtonVPN* | *mullvad* | *Mullvad* | *hiddify* | *Hiddify* | *loon* | *Loon* | *Cursor* | *cursor* | *Claude* | *claude* | *ChatGPT* | *chatgpt* | *Ollama* | *ollama* | *lmstudio* | *Chatbox* | *Gemini* | *gemini* | *Perplexity* | *perplexity* | *Windsurf* | *windsurf* | *Poe* | *poe* | *DiffusionBee* | *diffusionbee* | *DrawThings* | *drawthings* | *Aerial* | *aerial* | *Fliqlo* | *fliqlo* | *com.apple.finder*)
|
||||
skip=true
|
||||
((skipped_count++))
|
||||
;;
|
||||
|
||||
@@ -99,9 +99,11 @@ show_optimization_summary() {
|
||||
fi
|
||||
summary_details+=("$summary_line4")
|
||||
|
||||
if [[ "${OPTIMIZE_SHOW_TOUCHID_TIP:-false}" == "true" ]]; then
|
||||
echo -e "${YELLOW}☻${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID"
|
||||
if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then
|
||||
summary_details+=("$AUTO_FIX_SUMMARY")
|
||||
fi
|
||||
|
||||
# Fix: Ensure summary is always printed for optimizations
|
||||
print_summary_block "$summary_title" "${summary_details[@]}"
|
||||
}
|
||||
|
||||
@@ -245,6 +247,11 @@ collect_security_fix_actions() {
|
||||
SECURITY_FIXES+=("gatekeeper|Enable Gatekeeper (App download protection)")
|
||||
fi
|
||||
fi
|
||||
if touchid_supported && ! touchid_configured; then
|
||||
if ! is_whitelisted "touchid"; then
|
||||
SECURITY_FIXES+=("touchid|Enable Touch ID for sudo")
|
||||
fi
|
||||
fi
|
||||
|
||||
((${#SECURITY_FIXES[@]} > 0))
|
||||
}
|
||||
@@ -301,6 +308,13 @@ apply_gatekeeper_fix() {
|
||||
return 1
|
||||
}
|
||||
|
||||
apply_touchid_fix() {
|
||||
if "$SCRIPT_DIR/bin/touchid.sh" enable; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
perform_security_fixes() {
|
||||
if ! ensure_sudo_session "Security changes require admin access"; then
|
||||
echo -e "${YELLOW}${ICON_WARNING}${NC} Skipped security fixes (sudo denied)"
|
||||
@@ -317,6 +331,9 @@ perform_security_fixes() {
|
||||
gatekeeper)
|
||||
apply_gatekeeper_fix && ((applied++))
|
||||
;;
|
||||
touchid)
|
||||
apply_touchid_fix && ((applied++))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -496,10 +513,6 @@ main() {
|
||||
|
||||
export OPTIMIZE_SAFE_COUNT=$safe_count
|
||||
export OPTIMIZE_CONFIRM_COUNT=$confirm_count
|
||||
export OPTIMIZE_SHOW_TOUCHID_TIP="false"
|
||||
if touchid_supported && ! touchid_configured; then
|
||||
export OPTIMIZE_SHOW_TOUCHID_TIP="true"
|
||||
fi
|
||||
|
||||
# Show optimization summary at the end
|
||||
show_optimization_summary
|
||||
|
||||
BIN
bin/status-go
BIN
bin/status-go
Binary file not shown.
194
bin/uninstall.sh
194
bin/uninstall.sh
@@ -83,12 +83,24 @@ 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
|
||||
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 +109,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 +126,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 +158,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 +166,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 +266,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 +319,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 +546,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 +575,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
|
||||
|
||||
@@ -597,22 +599,20 @@ main() {
|
||||
rm -f "$apps_file"
|
||||
|
||||
# Pause before looping back
|
||||
echo -e "${GRAY}Press Enter to return to application list, ESC to exit...${NC}"
|
||||
echo -e "${GRAY}Press Enter to return to application list, any other key to exit...${NC}"
|
||||
local key
|
||||
IFS= read -r -s -n1 key || key=""
|
||||
drain_pending_input # Clean up any escape sequence remnants
|
||||
case "$key" in
|
||||
$'\e' | q | Q)
|
||||
show_cursor
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
# Continue loop
|
||||
;;
|
||||
esac
|
||||
drain_pending_input
|
||||
|
||||
# Reset force_rescan to false for subsequent loops,
|
||||
# but relying on batch_uninstall's cache deletion for actual update
|
||||
# Logic: Enter = continue loop, any other key = exit
|
||||
if [[ -z "$key" ]]; then
|
||||
: # Enter pressed, continue loop
|
||||
else
|
||||
show_cursor
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Reset force_rescan to false for subsequent loops
|
||||
force_rescan=false
|
||||
done
|
||||
}
|
||||
|
||||
@@ -83,12 +83,24 @@ 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
|
||||
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 +109,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 +126,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 +158,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 +166,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 +266,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 +319,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
|
||||
|
||||
|
||||
@@ -275,98 +275,37 @@ SOFTWARE_UPDATE_LIST=""
|
||||
get_software_updates() {
|
||||
local cache_file="$CACHE_DIR/softwareupdate_list"
|
||||
|
||||
if [[ -z "$SOFTWARE_UPDATE_LIST" ]]; then
|
||||
# Check cache first
|
||||
if is_cache_valid "$cache_file"; then
|
||||
SOFTWARE_UPDATE_LIST=$(cat "$cache_file" 2> /dev/null || echo "")
|
||||
else
|
||||
# Show spinner while checking (only on first call)
|
||||
local show_spinner=false
|
||||
if [[ -t 1 && -z "${SOFTWAREUPDATE_SPINNER_SHOWN:-}" ]]; then
|
||||
start_inline_spinner "Checking system updates (querying Apple servers)..."
|
||||
show_spinner=true
|
||||
export SOFTWAREUPDATE_SPINNER_SHOWN="true"
|
||||
fi
|
||||
# Optimized: Use defaults to check if updates are pending (much faster)
|
||||
local pending_updates
|
||||
pending_updates=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate LastRecommendedUpdatesAvailable 2> /dev/null || echo "0")
|
||||
|
||||
SOFTWARE_UPDATE_LIST=$(softwareupdate -l 2> /dev/null || echo "")
|
||||
# Save to cache
|
||||
echo "$SOFTWARE_UPDATE_LIST" > "$cache_file" 2> /dev/null || true
|
||||
|
||||
# Stop spinner
|
||||
if [[ "$show_spinner" == "true" ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
fi
|
||||
if [[ "$pending_updates" -gt 0 ]]; then
|
||||
echo "Updates Available"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
echo "$SOFTWARE_UPDATE_LIST"
|
||||
}
|
||||
|
||||
check_appstore_updates() {
|
||||
local spinner_started=false
|
||||
if [[ -t 1 ]]; then
|
||||
printf " Checking App Store updates...\r"
|
||||
start_inline_spinner "Checking App Store updates (querying Apple servers)..."
|
||||
spinner_started=true
|
||||
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
|
||||
else
|
||||
echo "Checking App Store updates..."
|
||||
fi
|
||||
|
||||
local update_list=""
|
||||
update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "")
|
||||
|
||||
if [[ "$spinner_started" == "true" ]]; then
|
||||
stop_inline_spinner
|
||||
unset SOFTWAREUPDATE_SPINNER_SHOWN
|
||||
fi
|
||||
|
||||
local update_count=0
|
||||
if [[ -n "$update_list" ]]; then
|
||||
update_count=$(echo "$update_list" | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
export APPSTORE_UPDATE_COUNT=$update_count
|
||||
|
||||
if [[ $update_count -gt 0 ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} App Store ${YELLOW}${update_count} apps${NC} need update"
|
||||
echo -e " ${GRAY}updates available in final step${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} App Store Up to date"
|
||||
fi
|
||||
# Skipped for speed optimization - consolidated into check_macos_update
|
||||
# We can't easily distinguish app store vs macos updates without the slow softwareupdate -l call
|
||||
export APPSTORE_UPDATE_COUNT=0
|
||||
}
|
||||
|
||||
check_macos_update() {
|
||||
# Check whitelist
|
||||
if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi
|
||||
local spinner_started=false
|
||||
if [[ -t 1 ]]; then
|
||||
printf " Checking macOS updates...\r"
|
||||
start_inline_spinner "Checking macOS updates (querying Apple servers)..."
|
||||
spinner_started=true
|
||||
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
|
||||
else
|
||||
echo "Checking macOS updates..."
|
||||
|
||||
# Fast check using system preferences
|
||||
local updates_available="false"
|
||||
if [[ $(get_software_updates) == "Updates Available" ]]; then
|
||||
updates_available="true"
|
||||
fi
|
||||
|
||||
# Check for macOS system update using cached list
|
||||
local macos_update=""
|
||||
macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "")
|
||||
export MACOS_UPDATE_AVAILABLE="$updates_available"
|
||||
|
||||
if [[ "$spinner_started" == "true" ]]; then
|
||||
stop_inline_spinner
|
||||
unset SOFTWAREUPDATE_SPINNER_SHOWN
|
||||
fi
|
||||
|
||||
export MACOS_UPDATE_AVAILABLE="false"
|
||||
|
||||
if [[ -n "$macos_update" ]]; then
|
||||
export MACOS_UPDATE_AVAILABLE="true"
|
||||
local version=$(echo "$macos_update" | grep -o '[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | head -1)
|
||||
if [[ -n "$version" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}${version} available${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}"
|
||||
fi
|
||||
if [[ "$updates_available" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}"
|
||||
echo -e " ${GRAY}update available in final step${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} macOS Up to date"
|
||||
|
||||
@@ -70,7 +70,7 @@ get_uptime_days() {
|
||||
local boot_output boot_time uptime_days
|
||||
|
||||
boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "")
|
||||
boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p' 2> /dev/null || echo "")
|
||||
boot_time=$(echo "$boot_output" | awk -F 'sec = |, usec' '{print $2}' 2> /dev/null || echo "")
|
||||
|
||||
if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then
|
||||
local now=$(date +%s 2> /dev/null || echo "0")
|
||||
|
||||
@@ -82,6 +82,7 @@ declare -a DEFAULT_WHITELIST_PATTERNS=(
|
||||
"$HOME/Library/Caches/org.R-project.R/R/renv/*"
|
||||
"$HOME/Library/Caches/JetBrains*"
|
||||
"$HOME/Library/Caches/com.jetbrains.toolbox*"
|
||||
"$HOME/Library/Caches/com.apple.finder"
|
||||
"$FINDER_METADATA_SENTINEL"
|
||||
)
|
||||
|
||||
@@ -245,27 +246,53 @@ bytes_to_human_kb() {
|
||||
|
||||
# Get brand-friendly name for an application
|
||||
# Args: $1 - application name
|
||||
# Returns: branded name if mapping exists, original name otherwise
|
||||
# Returns: localized name based on system language preference
|
||||
get_brand_name() {
|
||||
local name="$1"
|
||||
|
||||
case "$name" in
|
||||
"qiyimac" | "爱奇艺") echo "iQiyi" ;;
|
||||
"wechat" | "微信") echo "WeChat" ;;
|
||||
"QQ") echo "QQ" ;;
|
||||
"VooV Meeting" | "腾讯会议") echo "VooV Meeting" ;;
|
||||
"dingtalk" | "钉钉") echo "DingTalk" ;;
|
||||
"NeteaseMusic" | "网易云音乐") echo "NetEase Music" ;;
|
||||
"BaiduNetdisk" | "百度网盘") echo "Baidu NetDisk" ;;
|
||||
"alipay" | "支付宝") echo "Alipay" ;;
|
||||
"taobao" | "淘宝") echo "Taobao" ;;
|
||||
"futunn" | "富途牛牛") echo "Futu NiuNiu" ;;
|
||||
"tencent lemon" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;;
|
||||
"keynote" | "Keynote") echo "Keynote" ;;
|
||||
"pages" | "Pages") echo "Pages" ;;
|
||||
"numbers" | "Numbers") echo "Numbers" ;;
|
||||
*) echo "$name" ;;
|
||||
esac
|
||||
# Detect if system primary language is Chinese
|
||||
local is_chinese=false
|
||||
local sys_lang
|
||||
sys_lang=$(defaults read -g AppleLanguages 2> /dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "")
|
||||
[[ -n "$sys_lang" ]] && is_chinese=true
|
||||
|
||||
# Return localized names based on system language
|
||||
if [[ "$is_chinese" == true ]]; then
|
||||
# Chinese system - prefer Chinese names
|
||||
case "$name" in
|
||||
"qiyimac" | "iQiyi") echo "爱奇艺" ;;
|
||||
"wechat" | "WeChat") echo "微信" ;;
|
||||
"QQ") echo "QQ" ;;
|
||||
"VooV Meeting") echo "腾讯会议" ;;
|
||||
"dingtalk" | "DingTalk") echo "钉钉" ;;
|
||||
"NeteaseMusic" | "NetEase Music") echo "网易云音乐" ;;
|
||||
"BaiduNetdisk" | "Baidu NetDisk") echo "百度网盘" ;;
|
||||
"alipay" | "Alipay") echo "支付宝" ;;
|
||||
"taobao" | "Taobao") echo "淘宝" ;;
|
||||
"futunn" | "Futu NiuNiu") echo "富途牛牛" ;;
|
||||
"tencent lemon" | "Tencent Lemon Cleaner" | "Tencent Lemon") echo "腾讯柠檬清理" ;;
|
||||
*) echo "$name" ;;
|
||||
esac
|
||||
else
|
||||
# Non-Chinese system - use English names
|
||||
case "$name" in
|
||||
"qiyimac" | "爱奇艺") echo "iQiyi" ;;
|
||||
"wechat" | "微信") echo "WeChat" ;;
|
||||
"QQ") echo "QQ" ;;
|
||||
"腾讯会议") echo "VooV Meeting" ;;
|
||||
"dingtalk" | "钉钉") echo "DingTalk" ;;
|
||||
"网易云音乐") echo "NetEase Music" ;;
|
||||
"百度网盘") echo "Baidu NetDisk" ;;
|
||||
"alipay" | "支付宝") echo "Alipay" ;;
|
||||
"taobao" | "淘宝") echo "Taobao" ;;
|
||||
"富途牛牛") echo "Futu NiuNiu" ;;
|
||||
"腾讯柠檬清理" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;;
|
||||
"keynote" | "Keynote") echo "Keynote" ;;
|
||||
"pages" | "Pages") echo "Pages" ;;
|
||||
"numbers" | "Numbers") echo "Numbers" ;;
|
||||
*) echo "$name" ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -42,6 +42,11 @@ _request_password() {
|
||||
# Extra safety: ensure sudo cache is cleared before password input
|
||||
sudo -k 2> /dev/null
|
||||
|
||||
# Save original terminal settings and ensure they're restored on exit
|
||||
local stty_orig
|
||||
stty_orig=$(stty -g < "$tty_path" 2> /dev/null || echo "")
|
||||
trap '[[ -n "${stty_orig:-}" ]] && stty "${stty_orig:-}" < "$tty_path" 2> /dev/null || true' RETURN
|
||||
|
||||
while ((attempts < 3)); do
|
||||
local password=""
|
||||
|
||||
@@ -52,7 +57,13 @@ _request_password() {
|
||||
fi
|
||||
|
||||
printf "${PURPLE}${ICON_ARROW}${NC} Password: " > "$tty_path"
|
||||
IFS= read -r -s password < "$tty_path" || password=""
|
||||
|
||||
# Disable terminal echo to hide password input
|
||||
stty -echo -icanon min 1 time 0 < "$tty_path" 2> /dev/null || true
|
||||
IFS= read -r password < "$tty_path" || password=""
|
||||
# Restore terminal echo immediately
|
||||
stty echo icanon < "$tty_path" 2> /dev/null || true
|
||||
|
||||
printf "\n" > "$tty_path"
|
||||
|
||||
if [[ -z "$password" ]]; then
|
||||
|
||||
123
lib/core/ui.sh
123
lib/core/ui.sh
@@ -17,6 +17,129 @@ clear_screen() { printf '\033[2J\033[H'; }
|
||||
hide_cursor() { [[ -t 1 ]] && printf '\033[?25l' >&2 || true; }
|
||||
show_cursor() { [[ -t 1 ]] && printf '\033[?25h' >&2 || true; }
|
||||
|
||||
# Calculate display width of a string (CJK characters count as 2)
|
||||
# Args: $1 - string to measure
|
||||
# Returns: display width
|
||||
# Note: Works correctly even when LC_ALL=C is set
|
||||
get_display_width() {
|
||||
local str="$1"
|
||||
|
||||
# Optimized pure bash implementation without forks
|
||||
local width
|
||||
|
||||
# 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
|
||||
local char_count=${#str}
|
||||
|
||||
# Get Byte Count (C)
|
||||
export LC_ALL=C
|
||||
local byte_count=${#str}
|
||||
|
||||
# Restore Locale immediately
|
||||
if [[ -n "$old_lc" ]]; then
|
||||
export LC_ALL="$old_lc"
|
||||
else
|
||||
unset LC_ALL
|
||||
fi
|
||||
|
||||
if [[ $byte_count -eq $char_count ]]; then
|
||||
echo "$char_count"
|
||||
return
|
||||
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
|
||||
truncate_by_display_width() {
|
||||
local str="$1"
|
||||
local max_width="$2"
|
||||
local current_width
|
||||
current_width=$(get_display_width "$str")
|
||||
|
||||
if [[ $current_width -le $max_width ]]; then
|
||||
echo "$str"
|
||||
return
|
||||
fi
|
||||
|
||||
# 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'.
|
||||
|
||||
# CRITICAL: Switch to UTF-8 for correct character iteration
|
||||
local old_lc="${LC_ALL:-}"
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
local truncated=""
|
||||
local width=0
|
||||
local i=0
|
||||
local char char_width
|
||||
local strlen=${#str} # Re-calculate in UTF-8
|
||||
|
||||
# Optimization: If total width <= max_width, return original string (checked above)
|
||||
|
||||
while [[ $i -lt $strlen ]]; do
|
||||
char="${str:$i:1}"
|
||||
|
||||
# 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
|
||||
fi
|
||||
|
||||
truncated+="$char"
|
||||
((width += char_width))
|
||||
((i++))
|
||||
done
|
||||
|
||||
# Restore locale
|
||||
if [[ -n "$old_lc" ]]; then
|
||||
export LC_ALL="$old_lc"
|
||||
else
|
||||
unset LC_ALL
|
||||
fi
|
||||
|
||||
echo "${truncated}..."
|
||||
}
|
||||
|
||||
# Keyboard input - read single keypress
|
||||
read_key() {
|
||||
local key rest read_status
|
||||
|
||||
@@ -76,174 +76,51 @@ ask_for_updates() {
|
||||
echo -e "$item"
|
||||
done
|
||||
echo ""
|
||||
echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
|
||||
|
||||
local key
|
||||
if ! key=$(read_key); then
|
||||
echo "skip"
|
||||
echo ""
|
||||
return 1
|
||||
# If Mole has updates, offer to update it
|
||||
if [[ "${MOLE_UPDATE_AVAILABLE:-}" == "true" ]]; then
|
||||
echo -ne "${YELLOW}Update Mole now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
|
||||
|
||||
local key
|
||||
if ! key=$(read_key); then
|
||||
echo "skip"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$key" == "ENTER" ]]; then
|
||||
echo "yes"
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
echo "skip"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$key" == "ENTER" ]]; then
|
||||
echo "yes"
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
echo "skip"
|
||||
echo ""
|
||||
return 1
|
||||
# For other updates, just show instructions
|
||||
# (Mole update check above handles the return 0 case, so we only get here if no Mole update)
|
||||
if [[ "${BREW_OUTDATED_COUNT:-0}" -gt 0 ]]; then
|
||||
echo -e "${YELLOW}Tip:${NC} Run ${GREEN}brew upgrade${NC} to update Homebrew packages"
|
||||
fi
|
||||
if [[ "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
|
||||
echo -e "${YELLOW}Tip:${NC} Open ${BLUE}App Store${NC} to update apps"
|
||||
fi
|
||||
if [[ "${MACOS_UPDATE_AVAILABLE:-}" == "true" ]]; then
|
||||
echo -e "${YELLOW}Tip:${NC} Open ${BLUE}System Settings${NC} to update macOS"
|
||||
fi
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Perform all pending updates
|
||||
# Returns: 0 if all succeeded, 1 if some failed
|
||||
perform_updates() {
|
||||
# Only handle Mole updates here
|
||||
# Other updates are now informational-only in ask_for_updates
|
||||
|
||||
local updated_count=0
|
||||
local total_count=0
|
||||
local brew_formula="${BREW_FORMULA_OUTDATED_COUNT:-0}"
|
||||
local brew_cask="${BREW_CASK_OUTDATED_COUNT:-0}"
|
||||
|
||||
# Get update labels
|
||||
local -a appstore_labels=()
|
||||
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
|
||||
while IFS= read -r label; do
|
||||
[[ -n "$label" ]] && appstore_labels+=("$label")
|
||||
done < <(get_appstore_update_labels || true)
|
||||
fi
|
||||
|
||||
local -a macos_labels=()
|
||||
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
|
||||
while IFS= read -r label; do
|
||||
[[ -n "$label" ]] && macos_labels+=("$label")
|
||||
done < <(get_macos_update_labels || true)
|
||||
fi
|
||||
|
||||
# Check fallback needed
|
||||
local appstore_needs_fallback=false
|
||||
local macos_needs_fallback=false
|
||||
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 && ${#appstore_labels[@]} -eq 0 ]]; then
|
||||
appstore_needs_fallback=true
|
||||
fi
|
||||
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && ${#macos_labels[@]} -eq 0 ]]; then
|
||||
macos_needs_fallback=true
|
||||
fi
|
||||
|
||||
# Count total updates
|
||||
((brew_formula > 0)) && ((total_count++))
|
||||
((brew_cask > 0)) && ((total_count++))
|
||||
[[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]] && ((total_count++))
|
||||
[[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++))
|
||||
[[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++))
|
||||
|
||||
# Update Homebrew formulae
|
||||
if ((brew_formula > 0)); then
|
||||
if ! brew_has_outdated "formula"; then
|
||||
echo -e "${GRAY}-${NC} Homebrew formulae already up to date"
|
||||
((total_count--))
|
||||
echo ""
|
||||
else
|
||||
echo -e "${BLUE}Updating Homebrew formulae...${NC}"
|
||||
local spinner_started=false
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Running brew upgrade"
|
||||
spinner_started=true
|
||||
fi
|
||||
|
||||
local brew_output=""
|
||||
local brew_status=0
|
||||
if ! brew_output=$(brew upgrade --formula 2>&1); then
|
||||
brew_status=$?
|
||||
fi
|
||||
|
||||
if [[ "$spinner_started" == "true" ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
|
||||
local filtered_output
|
||||
filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true)
|
||||
[[ -n "$filtered_output" ]] && echo "$filtered_output"
|
||||
|
||||
if [[ ${brew_status:-0} -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓${NC} Homebrew formulae updated"
|
||||
reset_brew_cache
|
||||
((updated_count++))
|
||||
else
|
||||
echo -e "${RED}✗${NC} Homebrew formula update failed"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update Homebrew casks
|
||||
if ((brew_cask > 0)); then
|
||||
if ! brew_has_outdated "cask"; then
|
||||
echo -e "${GRAY}-${NC} Homebrew casks already up to date"
|
||||
((total_count--))
|
||||
echo ""
|
||||
else
|
||||
echo -e "${BLUE}Updating Homebrew casks...${NC}"
|
||||
local spinner_started=false
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Running brew upgrade --cask"
|
||||
spinner_started=true
|
||||
fi
|
||||
|
||||
local brew_output=""
|
||||
local brew_status=0
|
||||
if ! brew_output=$(brew upgrade --cask 2>&1); then
|
||||
brew_status=$?
|
||||
fi
|
||||
|
||||
if [[ "$spinner_started" == "true" ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
|
||||
local filtered_output
|
||||
filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true)
|
||||
[[ -n "$filtered_output" ]] && echo "$filtered_output"
|
||||
|
||||
if [[ ${brew_status:-0} -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓${NC} Homebrew casks updated"
|
||||
reset_brew_cache
|
||||
((updated_count++))
|
||||
else
|
||||
echo -e "${RED}✗${NC} Homebrew cask update failed"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update App Store apps
|
||||
local macos_handled_via_appstore=false
|
||||
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
|
||||
# Check sudo access
|
||||
if ! has_sudo_session; then
|
||||
if ! ensure_sudo_session "Software updates require admin access"; then
|
||||
echo -e "${YELLOW}☻${NC} App Store updates available — update via System Settings"
|
||||
echo ""
|
||||
((total_count--))
|
||||
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
|
||||
((total_count--))
|
||||
fi
|
||||
else
|
||||
_perform_appstore_update
|
||||
fi
|
||||
else
|
||||
_perform_appstore_update
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update macOS
|
||||
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && "$macos_handled_via_appstore" != "true" ]]; then
|
||||
if ! has_sudo_session; then
|
||||
echo -e "${YELLOW}☻${NC} macOS updates available — update via System Settings"
|
||||
echo ""
|
||||
((total_count--))
|
||||
else
|
||||
_perform_macos_update
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update Mole
|
||||
if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then
|
||||
@@ -253,10 +130,17 @@ perform_updates() {
|
||||
[[ ! -f "$mole_bin" ]] && mole_bin=$(command -v mole 2> /dev/null || echo "")
|
||||
|
||||
if [[ -x "$mole_bin" ]]; then
|
||||
# We use exec here or just run it?
|
||||
# If we run 'mole update', it replaces the script.
|
||||
# Since this function is part of a sourced script, replacing the file on disk is risky while running.
|
||||
# However, 'mole update' script usually handles this by downloading to a temp file and moving it.
|
||||
# But the shell might not like the file changing under it.
|
||||
# The original code ran it this way, so we assume it's safe enough or handled by mole update implementation.
|
||||
|
||||
if "$mole_bin" update 2>&1 | grep -qE "(Updated|latest version)"; then
|
||||
echo -e "${GREEN}✓${NC} Mole updated"
|
||||
reset_mole_cache
|
||||
((updated_count++))
|
||||
updated_count=1
|
||||
else
|
||||
echo -e "${RED}✗${NC} Mole update failed"
|
||||
fi
|
||||
@@ -266,86 +150,9 @@ perform_updates() {
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Summary
|
||||
if [[ $total_count -eq 0 ]]; then
|
||||
echo -e "${GRAY}No updates to perform${NC}"
|
||||
return 0
|
||||
elif [[ $updated_count -eq $total_count ]]; then
|
||||
echo -e "${GREEN}All updates completed (${updated_count}/${total_count})${NC}"
|
||||
return 0
|
||||
elif [[ $updated_count -gt 0 ]]; then
|
||||
echo -e "${YELLOW}Partial updates completed (${updated_count}/${total_count})${NC}"
|
||||
if [[ $updated_count -gt 0 ]]; then
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}No updates were completed${NC}"
|
||||
return 0
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Internal: Perform App Store update
|
||||
_perform_appstore_update() {
|
||||
echo -e "${BLUE}Updating App Store apps...${NC}"
|
||||
local appstore_log
|
||||
appstore_log=$(mktemp "${TMPDIR:-/tmp}/mole-appstore.XXXXXX" 2> /dev/null || echo "/tmp/mole-appstore.log")
|
||||
|
||||
if [[ "$appstore_needs_fallback" == "true" ]]; then
|
||||
echo -e " ${GRAY}Installing all available updates${NC}"
|
||||
echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}"
|
||||
if sudo softwareupdate -i -a 2>&1 | tee "$appstore_log" | grep -v "^$"; then
|
||||
echo -e "${GREEN}✓${NC} Software updates completed"
|
||||
((updated_count++))
|
||||
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
|
||||
macos_handled_via_appstore=true
|
||||
((updated_count++))
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗${NC} Software update failed"
|
||||
fi
|
||||
else
|
||||
echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}"
|
||||
if sudo softwareupdate -i "${appstore_labels[@]}" 2>&1 | tee "$appstore_log" | grep -v "^$"; then
|
||||
echo -e "${GREEN}✓${NC} App Store apps updated"
|
||||
((updated_count++))
|
||||
else
|
||||
echo -e "${RED}✗${NC} App Store update failed"
|
||||
fi
|
||||
fi
|
||||
rm -f "$appstore_log" 2> /dev/null || true
|
||||
reset_softwareupdate_cache
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Internal: Perform macOS update
|
||||
_perform_macos_update() {
|
||||
echo -e "${BLUE}Updating macOS...${NC}"
|
||||
echo -e "${YELLOW}Note:${NC} System update may require restart"
|
||||
|
||||
local macos_log
|
||||
macos_log=$(mktemp "${TMPDIR:-/tmp}/mole-macos.XXXXXX" 2> /dev/null || echo "/tmp/mole-macos.log")
|
||||
|
||||
if [[ "$macos_needs_fallback" == "true" ]]; then
|
||||
echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}"
|
||||
if sudo softwareupdate -i -r 2>&1 | tee "$macos_log" | grep -v "^$"; then
|
||||
echo -e "${GREEN}✓${NC} macOS updated"
|
||||
((updated_count++))
|
||||
else
|
||||
echo -e "${RED}✗${NC} macOS update failed"
|
||||
fi
|
||||
else
|
||||
echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}"
|
||||
if sudo softwareupdate -i "${macos_labels[@]}" 2>&1 | tee "$macos_log" | grep -v "^$"; then
|
||||
echo -e "${GREEN}✓${NC} macOS updated"
|
||||
((updated_count++))
|
||||
else
|
||||
echo -e "${RED}✗${NC} macOS update failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -qi "restart" "$macos_log" 2> /dev/null; then
|
||||
echo -e "${YELLOW}${ICON_WARNING}${NC} Restart required to complete update"
|
||||
fi
|
||||
|
||||
rm -f "$macos_log" 2> /dev/null || true
|
||||
reset_softwareupdate_cache
|
||||
echo ""
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ fix_broken_login_items() {
|
||||
local launch_agents_dir="$HOME/Library/LaunchAgents"
|
||||
[[ -d "$launch_agents_dir" ]] || return 0
|
||||
|
||||
# Check whitelist
|
||||
if command -v is_whitelisted > /dev/null && is_whitelisted "check_login_items"; then return 0; fi
|
||||
|
||||
local broken_count=0
|
||||
|
||||
while IFS= read -r plist_file; do
|
||||
@@ -97,6 +100,9 @@ fix_broken_login_items() {
|
||||
program=$(plutil -extract ProgramArguments.0 raw "$plist_file" 2> /dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Expand tilde in path if present
|
||||
program="${program/#\~/$HOME}"
|
||||
|
||||
# Skip if no program found or program exists
|
||||
[[ -z "$program" ]] && continue
|
||||
[[ -e "$program" ]] && continue
|
||||
|
||||
@@ -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"
|
||||
@@ -16,22 +18,31 @@ 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))
|
||||
|
||||
# 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 +57,15 @@ 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[@]}
|
||||
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
|
||||
fi
|
||||
|
||||
local -a menu_options=()
|
||||
# Prepare metadata (comma-separated) for sorting/filtering inside the menu
|
||||
local epochs_csv=""
|
||||
@@ -54,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}"
|
||||
@@ -66,6 +86,13 @@ select_apps_for_uninstall() {
|
||||
((idx++))
|
||||
done
|
||||
|
||||
# Clear loading message
|
||||
if [[ $app_count -gt 100 ]]; 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
|
||||
|
||||
@@ -130,9 +130,10 @@ batch_uninstall_applications() {
|
||||
running_apps+=("$app_name")
|
||||
fi
|
||||
|
||||
# Check if app requires sudo to delete
|
||||
# Check if app requires sudo to delete (either app bundle or system files)
|
||||
local needs_sudo=false
|
||||
if [[ ! -w "$(dirname "$app_path")" ]] || [[ "$(get_file_owner "$app_path")" == "root" ]]; then
|
||||
sudo_apps+=("$app_name")
|
||||
needs_sudo=true
|
||||
fi
|
||||
|
||||
# Calculate size for summary (including system files)
|
||||
@@ -150,6 +151,10 @@ batch_uninstall_applications() {
|
||||
# Check if system files require sudo
|
||||
# shellcheck disable=SC2128
|
||||
if [[ -n "$system_files" ]]; then
|
||||
needs_sudo=true
|
||||
fi
|
||||
|
||||
if [[ "$needs_sudo" == "true" ]]; then
|
||||
sudo_apps+=("$app_name")
|
||||
fi
|
||||
|
||||
@@ -401,7 +406,12 @@ batch_uninstall_applications() {
|
||||
summary_details+=("No applications were uninstalled.")
|
||||
fi
|
||||
|
||||
print_summary_block "$summary_status" "Uninstall complete" "${summary_details[@]}"
|
||||
local title="Uninstall complete"
|
||||
if [[ "$summary_status" == "warn" ]]; then
|
||||
title="Uninstall incomplete"
|
||||
fi
|
||||
|
||||
print_summary_block "$title" "${summary_details[@]}"
|
||||
printf '\n'
|
||||
|
||||
# Clean up Dock entries for uninstalled apps
|
||||
|
||||
43
mole
43
mole
@@ -22,8 +22,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/lib/core/common.sh"
|
||||
|
||||
# Version info
|
||||
VERSION="1.13.0"
|
||||
MOLE_TAGLINE="can dig deep to clean your Mac."
|
||||
VERSION="1.13.5"
|
||||
MOLE_TAGLINE="Deep clean and optimize your Mac."
|
||||
|
||||
# Check if Touch ID is already configured
|
||||
is_touchid_configured() {
|
||||
@@ -181,7 +181,44 @@ EOF
|
||||
}
|
||||
|
||||
show_version() {
|
||||
printf '\nMole version %s\n\n' "$VERSION"
|
||||
local os_ver
|
||||
if command -v sw_vers > /dev/null; then
|
||||
os_ver=$(sw_vers -productVersion)
|
||||
else
|
||||
os_ver="Unknown"
|
||||
fi
|
||||
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
|
||||
local kernel
|
||||
kernel=$(uname -r)
|
||||
|
||||
local sip_status
|
||||
if command -v csrutil > /dev/null; then
|
||||
sip_status=$(csrutil status 2> /dev/null | grep -o "enabled\|disabled" || echo "Unknown")
|
||||
# Capitalize first letter
|
||||
sip_status="$(tr '[:lower:]' '[:upper:]' <<< ${sip_status:0:1})${sip_status:1}"
|
||||
else
|
||||
sip_status="Unknown"
|
||||
fi
|
||||
|
||||
local disk_free
|
||||
disk_free=$(df -h / 2> /dev/null | awk 'NR==2 {print $4}' || echo "Unknown")
|
||||
|
||||
local install_method="Manual"
|
||||
if is_homebrew_install; then
|
||||
install_method="Homebrew"
|
||||
fi
|
||||
|
||||
printf '\nMole version %s\n' "$VERSION"
|
||||
printf 'macOS: %s\n' "$os_ver"
|
||||
printf 'Architecture: %s\n' "$arch"
|
||||
printf 'Kernel: %s\n' "$kernel"
|
||||
printf 'SIP: %s\n' "$sip_status"
|
||||
printf 'Disk Free: %s\n' "$disk_free"
|
||||
printf 'Install: %s\n' "$install_method"
|
||||
printf 'Shell: %s\n\n' "${SHELL:-Unknown}"
|
||||
}
|
||||
|
||||
show_help() {
|
||||
|
||||
Reference in New Issue
Block a user