mirror of
https://github.com/tw93/Mole.git
synced 2026-02-12 06:29:00 +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
|
## Environment
|
||||||
|
|
||||||
- Mole version: (run `mo --version`)
|
Please run `mo update` to ensure you are on the latest version, then paste the output of `mo --version` below:
|
||||||
- macOS version: (run `sw_vers`)
|
|
||||||
- Installation method: (Homebrew / curl script)
|
```text
|
||||||
- Architecture: (Intel / Apple Silicon)
|
Paste mo --version output here
|
||||||
|
```
|
||||||
|
|
||||||
## Additional context
|
## Additional context
|
||||||
|
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h1>Mole</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -18,15 +18,15 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **All-in-one toolkit** equal to CleanMyMac + AppCleaner + DaisyDisk + Sensei + iStat in one **trusted binary**
|
- **All-in-one toolkit** combining the power of CleanMyMac, AppCleaner, DaisyDisk, Sensei, and iStat in one **trusted binary**
|
||||||
- **Deep cleanup** finds and removes caches, temp files, browser leftovers, and junk to **free up tens of gigabytes**
|
- **Deep cleanup** scans and removes caches, logs, browser leftovers, and junk to **reclaim tens of gigabytes**
|
||||||
- **Smart uninstall** finds app bundles plus launch agents, settings, caches, logs, and **leftover files**
|
- **Smart uninstall** completely removes apps including launch agents, preferences, caches, and **hidden leftovers**
|
||||||
- **Disk insight + optimization** show large files, display folders, **rebuild caches**, clean swap, refresh services
|
- **Disk insight + optimization** visualizes usage, handles large files, **rebuilds caches**, cleans swap, and refreshes services
|
||||||
- **Live status** shows CPU, GPU, memory, disk, network, battery, and proxy data so you can **find problems**
|
- **Live status** monitors CPU, GPU, memory, disk, network, battery, and proxy stats to **diagnose issues**
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
**Install:**
|
**Installation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/tw93/mole/main/install.sh | 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
|
## Tips
|
||||||
|
|
||||||
- **Terminal**: iTerm2 has known compatibility issues, use Alacritty, kitty, WezTerm, Ghostty, or Warp instead
|
- **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 with `mo clean --dry-run`
|
- **Safety**: Built with strict protections. See our [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`.
|
||||||
- **Whitelist**: Use `mo clean --whitelist` to manage protected caches
|
- **Whitelist**: Manage protected paths with `mo clean --whitelist`.
|
||||||
- **Touch ID**: Run `mo touchid` to approve sudo with Touch ID instead of password
|
- **Touch ID**: Enable Touch ID for sudo commands by running `mo touchid`.
|
||||||
- **Navigation**: All menus support Vim keys `h/j/k/l` in addition to arrow keys
|
- **Navigation**: Supports standard arrow keys and Vim bindings (`h/j/k/l`).
|
||||||
- **Debug**: Use `--debug` flag to see detailed logs: `mo clean --debug`
|
- **Debug**: View detailed logs by appending the `--debug` flag (e.g., `mo clean --debug`).
|
||||||
|
|
||||||
## Features in Detail
|
## 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>
|
<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.
|
- 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 and help shape Mole's future together with the community.
|
- Have ideas or fixes? Open an issue or PR to help shape Mole's future 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.
|
- 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
|
## 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)
|
# Hard-coded protection for critical apps (cannot be disabled by user)
|
||||||
case "$path" in
|
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
|
skip=true
|
||||||
((skipped_count++))
|
((skipped_count++))
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ show_optimization_summary() {
|
|||||||
fi
|
fi
|
||||||
summary_details+=("$summary_line4")
|
summary_details+=("$summary_line4")
|
||||||
|
|
||||||
if [[ "${OPTIMIZE_SHOW_TOUCHID_TIP:-false}" == "true" ]]; then
|
if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then
|
||||||
echo -e "${YELLOW}☻${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID"
|
summary_details+=("$AUTO_FIX_SUMMARY")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Fix: Ensure summary is always printed for optimizations
|
||||||
print_summary_block "$summary_title" "${summary_details[@]}"
|
print_summary_block "$summary_title" "${summary_details[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +247,11 @@ collect_security_fix_actions() {
|
|||||||
SECURITY_FIXES+=("gatekeeper|Enable Gatekeeper (App download protection)")
|
SECURITY_FIXES+=("gatekeeper|Enable Gatekeeper (App download protection)")
|
||||||
fi
|
fi
|
||||||
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))
|
((${#SECURITY_FIXES[@]} > 0))
|
||||||
}
|
}
|
||||||
@@ -301,6 +308,13 @@ apply_gatekeeper_fix() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply_touchid_fix() {
|
||||||
|
if "$SCRIPT_DIR/bin/touchid.sh" enable; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
perform_security_fixes() {
|
perform_security_fixes() {
|
||||||
if ! ensure_sudo_session "Security changes require admin access"; then
|
if ! ensure_sudo_session "Security changes require admin access"; then
|
||||||
echo -e "${YELLOW}${ICON_WARNING}${NC} Skipped security fixes (sudo denied)"
|
echo -e "${YELLOW}${ICON_WARNING}${NC} Skipped security fixes (sudo denied)"
|
||||||
@@ -317,6 +331,9 @@ perform_security_fixes() {
|
|||||||
gatekeeper)
|
gatekeeper)
|
||||||
apply_gatekeeper_fix && ((applied++))
|
apply_gatekeeper_fix && ((applied++))
|
||||||
;;
|
;;
|
||||||
|
touchid)
|
||||||
|
apply_touchid_fix && ((applied++))
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -496,10 +513,6 @@ main() {
|
|||||||
|
|
||||||
export OPTIMIZE_SAFE_COUNT=$safe_count
|
export OPTIMIZE_SAFE_COUNT=$safe_count
|
||||||
export OPTIMIZE_CONFIRM_COUNT=$confirm_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 at the end
|
||||||
show_optimization_summary
|
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
|
[[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file
|
||||||
if [[ $cache_age -lt $cache_ttl ]]; then
|
if [[ $cache_age -lt $cache_ttl ]]; then
|
||||||
# Cache hit - return immediately
|
# 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"
|
echo "$cache_file"
|
||||||
return 0
|
return 0
|
||||||
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 +109,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 +126,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 +158,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 +166,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 +266,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 +319,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 +546,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 +575,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
|
||||||
|
|
||||||
@@ -597,22 +599,20 @@ main() {
|
|||||||
rm -f "$apps_file"
|
rm -f "$apps_file"
|
||||||
|
|
||||||
# Pause before looping back
|
# 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
|
local key
|
||||||
IFS= read -r -s -n1 key || key=""
|
IFS= read -r -s -n1 key || key=""
|
||||||
drain_pending_input # Clean up any escape sequence remnants
|
drain_pending_input
|
||||||
case "$key" in
|
|
||||||
$'\e' | q | Q)
|
|
||||||
show_cursor
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# Continue loop
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Reset force_rescan to false for subsequent loops,
|
# Logic: Enter = continue loop, any other key = exit
|
||||||
# but relying on batch_uninstall's cache deletion for actual update
|
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
|
force_rescan=false
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,12 +83,24 @@ scan_applications() {
|
|||||||
[[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file
|
[[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file
|
||||||
if [[ $cache_age -lt $cache_ttl ]]; then
|
if [[ $cache_age -lt $cache_ttl ]]; then
|
||||||
# Cache hit - return immediately
|
# 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"
|
echo "$cache_file"
|
||||||
return 0
|
return 0
|
||||||
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 +109,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 +126,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 +158,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 +166,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 +266,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 +319,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
|
||||||
|
|
||||||
|
|||||||
@@ -275,98 +275,37 @@ SOFTWARE_UPDATE_LIST=""
|
|||||||
get_software_updates() {
|
get_software_updates() {
|
||||||
local cache_file="$CACHE_DIR/softwareupdate_list"
|
local cache_file="$CACHE_DIR/softwareupdate_list"
|
||||||
|
|
||||||
if [[ -z "$SOFTWARE_UPDATE_LIST" ]]; then
|
# Optimized: Use defaults to check if updates are pending (much faster)
|
||||||
# Check cache first
|
local pending_updates
|
||||||
if is_cache_valid "$cache_file"; then
|
pending_updates=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate LastRecommendedUpdatesAvailable 2> /dev/null || echo "0")
|
||||||
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
|
|
||||||
|
|
||||||
SOFTWARE_UPDATE_LIST=$(softwareupdate -l 2> /dev/null || echo "")
|
if [[ "$pending_updates" -gt 0 ]]; then
|
||||||
# Save to cache
|
echo "Updates Available"
|
||||||
echo "$SOFTWARE_UPDATE_LIST" > "$cache_file" 2> /dev/null || true
|
else
|
||||||
|
echo ""
|
||||||
# Stop spinner
|
|
||||||
if [[ "$show_spinner" == "true" ]]; then
|
|
||||||
stop_inline_spinner
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
echo "$SOFTWARE_UPDATE_LIST"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_appstore_updates() {
|
check_appstore_updates() {
|
||||||
local spinner_started=false
|
# Skipped for speed optimization - consolidated into check_macos_update
|
||||||
if [[ -t 1 ]]; then
|
# We can't easily distinguish app store vs macos updates without the slow softwareupdate -l call
|
||||||
printf " Checking App Store updates...\r"
|
export APPSTORE_UPDATE_COUNT=0
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_macos_update() {
|
check_macos_update() {
|
||||||
# Check whitelist
|
# Check whitelist
|
||||||
if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi
|
if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi
|
||||||
local spinner_started=false
|
|
||||||
if [[ -t 1 ]]; then
|
# Fast check using system preferences
|
||||||
printf " Checking macOS updates...\r"
|
local updates_available="false"
|
||||||
start_inline_spinner "Checking macOS updates (querying Apple servers)..."
|
if [[ $(get_software_updates) == "Updates Available" ]]; then
|
||||||
spinner_started=true
|
updates_available="true"
|
||||||
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
|
|
||||||
else
|
|
||||||
echo "Checking macOS updates..."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for macOS system update using cached list
|
export MACOS_UPDATE_AVAILABLE="$updates_available"
|
||||||
local macos_update=""
|
|
||||||
macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "")
|
|
||||||
|
|
||||||
if [[ "$spinner_started" == "true" ]]; then
|
if [[ "$updates_available" == "true" ]]; then
|
||||||
stop_inline_spinner
|
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}"
|
||||||
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
|
|
||||||
echo -e " ${GRAY}update available in final step${NC}"
|
echo -e " ${GRAY}update available in final step${NC}"
|
||||||
else
|
else
|
||||||
echo -e " ${GREEN}✓${NC} macOS Up to date"
|
echo -e " ${GREEN}✓${NC} macOS Up to date"
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ get_uptime_days() {
|
|||||||
local boot_output boot_time uptime_days
|
local boot_output boot_time uptime_days
|
||||||
|
|
||||||
boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "")
|
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
|
if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then
|
||||||
local now=$(date +%s 2> /dev/null || echo "0")
|
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/org.R-project.R/R/renv/*"
|
||||||
"$HOME/Library/Caches/JetBrains*"
|
"$HOME/Library/Caches/JetBrains*"
|
||||||
"$HOME/Library/Caches/com.jetbrains.toolbox*"
|
"$HOME/Library/Caches/com.jetbrains.toolbox*"
|
||||||
|
"$HOME/Library/Caches/com.apple.finder"
|
||||||
"$FINDER_METADATA_SENTINEL"
|
"$FINDER_METADATA_SENTINEL"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -245,27 +246,53 @@ bytes_to_human_kb() {
|
|||||||
|
|
||||||
# Get brand-friendly name for an application
|
# Get brand-friendly name for an application
|
||||||
# Args: $1 - application name
|
# Args: $1 - application name
|
||||||
# Returns: branded name if mapping exists, original name otherwise
|
# Returns: localized name based on system language preference
|
||||||
get_brand_name() {
|
get_brand_name() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
|
|
||||||
case "$name" in
|
# Detect if system primary language is Chinese
|
||||||
"qiyimac" | "爱奇艺") echo "iQiyi" ;;
|
local is_chinese=false
|
||||||
"wechat" | "微信") echo "WeChat" ;;
|
local sys_lang
|
||||||
"QQ") echo "QQ" ;;
|
sys_lang=$(defaults read -g AppleLanguages 2> /dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "")
|
||||||
"VooV Meeting" | "腾讯会议") echo "VooV Meeting" ;;
|
[[ -n "$sys_lang" ]] && is_chinese=true
|
||||||
"dingtalk" | "钉钉") echo "DingTalk" ;;
|
|
||||||
"NeteaseMusic" | "网易云音乐") echo "NetEase Music" ;;
|
# Return localized names based on system language
|
||||||
"BaiduNetdisk" | "百度网盘") echo "Baidu NetDisk" ;;
|
if [[ "$is_chinese" == true ]]; then
|
||||||
"alipay" | "支付宝") echo "Alipay" ;;
|
# Chinese system - prefer Chinese names
|
||||||
"taobao" | "淘宝") echo "Taobao" ;;
|
case "$name" in
|
||||||
"futunn" | "富途牛牛") echo "Futu NiuNiu" ;;
|
"qiyimac" | "iQiyi") echo "爱奇艺" ;;
|
||||||
"tencent lemon" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;;
|
"wechat" | "WeChat") echo "微信" ;;
|
||||||
"keynote" | "Keynote") echo "Keynote" ;;
|
"QQ") echo "QQ" ;;
|
||||||
"pages" | "Pages") echo "Pages" ;;
|
"VooV Meeting") echo "腾讯会议" ;;
|
||||||
"numbers" | "Numbers") echo "Numbers" ;;
|
"dingtalk" | "DingTalk") echo "钉钉" ;;
|
||||||
*) echo "$name" ;;
|
"NeteaseMusic" | "NetEase Music") echo "网易云音乐" ;;
|
||||||
esac
|
"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
|
# Extra safety: ensure sudo cache is cleared before password input
|
||||||
sudo -k 2> /dev/null
|
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
|
while ((attempts < 3)); do
|
||||||
local password=""
|
local password=""
|
||||||
|
|
||||||
@@ -52,7 +57,13 @@ _request_password() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf "${PURPLE}${ICON_ARROW}${NC} Password: " > "$tty_path"
|
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"
|
printf "\n" > "$tty_path"
|
||||||
|
|
||||||
if [[ -z "$password" ]]; then
|
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; }
|
hide_cursor() { [[ -t 1 ]] && printf '\033[?25l' >&2 || true; }
|
||||||
show_cursor() { [[ -t 1 ]] && printf '\033[?25h' >&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
|
# Keyboard input - read single keypress
|
||||||
read_key() {
|
read_key() {
|
||||||
local key rest read_status
|
local key rest read_status
|
||||||
|
|||||||
@@ -76,174 +76,51 @@ ask_for_updates() {
|
|||||||
echo -e "$item"
|
echo -e "$item"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
|
|
||||||
|
|
||||||
local key
|
# If Mole has updates, offer to update it
|
||||||
if ! key=$(read_key); then
|
if [[ "${MOLE_UPDATE_AVAILABLE:-}" == "true" ]]; then
|
||||||
echo "skip"
|
echo -ne "${YELLOW}Update Mole now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
|
||||||
echo ""
|
|
||||||
return 1
|
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
|
fi
|
||||||
|
|
||||||
if [[ "$key" == "ENTER" ]]; then
|
# For other updates, just show instructions
|
||||||
echo "yes"
|
# (Mole update check above handles the return 0 case, so we only get here if no Mole update)
|
||||||
echo ""
|
if [[ "${BREW_OUTDATED_COUNT:-0}" -gt 0 ]]; then
|
||||||
return 0
|
echo -e "${YELLOW}Tip:${NC} Run ${GREEN}brew upgrade${NC} to update Homebrew packages"
|
||||||
else
|
|
||||||
echo "skip"
|
|
||||||
echo ""
|
|
||||||
return 1
|
|
||||||
fi
|
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
|
# Perform all pending updates
|
||||||
# Returns: 0 if all succeeded, 1 if some failed
|
# Returns: 0 if all succeeded, 1 if some failed
|
||||||
perform_updates() {
|
perform_updates() {
|
||||||
|
# Only handle Mole updates here
|
||||||
|
# Other updates are now informational-only in ask_for_updates
|
||||||
|
|
||||||
local updated_count=0
|
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
|
# Update Mole
|
||||||
if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then
|
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 "")
|
[[ ! -f "$mole_bin" ]] && mole_bin=$(command -v mole 2> /dev/null || echo "")
|
||||||
|
|
||||||
if [[ -x "$mole_bin" ]]; then
|
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
|
if "$mole_bin" update 2>&1 | grep -qE "(Updated|latest version)"; then
|
||||||
echo -e "${GREEN}✓${NC} Mole updated"
|
echo -e "${GREEN}✓${NC} Mole updated"
|
||||||
reset_mole_cache
|
reset_mole_cache
|
||||||
((updated_count++))
|
updated_count=1
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗${NC} Mole update failed"
|
echo -e "${RED}✗${NC} Mole update failed"
|
||||||
fi
|
fi
|
||||||
@@ -266,86 +150,9 @@ perform_updates() {
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Summary
|
if [[ $updated_count -gt 0 ]]; then
|
||||||
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}"
|
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}No updates were completed${NC}"
|
return 1
|
||||||
return 0
|
|
||||||
fi
|
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"
|
local launch_agents_dir="$HOME/Library/LaunchAgents"
|
||||||
[[ -d "$launch_agents_dir" ]] || return 0
|
[[ -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
|
local broken_count=0
|
||||||
|
|
||||||
while IFS= read -r plist_file; do
|
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 "")
|
program=$(plutil -extract ProgramArguments.0 raw "$plist_file" 2> /dev/null || echo "")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Expand tilde in path if present
|
||||||
|
program="${program/#\~/$HOME}"
|
||||||
|
|
||||||
# Skip if no program found or program exists
|
# Skip if no program found or program exists
|
||||||
[[ -z "$program" ]] && continue
|
[[ -z "$program" ]] && continue
|
||||||
[[ -e "$program" ]] && continue
|
[[ -e "$program" ]] && continue
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -16,22 +18,31 @@ format_app_display() {
|
|||||||
[[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size"
|
[[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size"
|
||||||
|
|
||||||
# Calculate available width for app name based on terminal width
|
# 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 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 +57,15 @@ 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[@]}
|
||||||
|
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=()
|
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=""
|
||||||
@@ -54,7 +74,7 @@ select_apps_for_uninstall() {
|
|||||||
for app_data in "${apps_data[@]}"; do
|
for app_data in "${apps_data[@]}"; do
|
||||||
# Keep extended field 7 (size_kb) if present
|
# Keep extended field 7 (size_kb) if present
|
||||||
IFS='|' read -r epoch _ display_name _ size last_used size_kb <<< "$app_data"
|
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)
|
# Build csv lists (avoid trailing commas)
|
||||||
if [[ $idx -eq 0 ]]; then
|
if [[ $idx -eq 0 ]]; then
|
||||||
epochs_csv="${epoch:-0}"
|
epochs_csv="${epoch:-0}"
|
||||||
@@ -66,6 +86,13 @@ select_apps_for_uninstall() {
|
|||||||
((idx++))
|
((idx++))
|
||||||
done
|
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)
|
# 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
|
||||||
|
|||||||
@@ -130,9 +130,10 @@ batch_uninstall_applications() {
|
|||||||
running_apps+=("$app_name")
|
running_apps+=("$app_name")
|
||||||
fi
|
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
|
if [[ ! -w "$(dirname "$app_path")" ]] || [[ "$(get_file_owner "$app_path")" == "root" ]]; then
|
||||||
sudo_apps+=("$app_name")
|
needs_sudo=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Calculate size for summary (including system files)
|
# Calculate size for summary (including system files)
|
||||||
@@ -150,6 +151,10 @@ batch_uninstall_applications() {
|
|||||||
# Check if system files require sudo
|
# Check if system files require sudo
|
||||||
# shellcheck disable=SC2128
|
# shellcheck disable=SC2128
|
||||||
if [[ -n "$system_files" ]]; then
|
if [[ -n "$system_files" ]]; then
|
||||||
|
needs_sudo=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$needs_sudo" == "true" ]]; then
|
||||||
sudo_apps+=("$app_name")
|
sudo_apps+=("$app_name")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -401,7 +406,12 @@ batch_uninstall_applications() {
|
|||||||
summary_details+=("No applications were uninstalled.")
|
summary_details+=("No applications were uninstalled.")
|
||||||
fi
|
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'
|
printf '\n'
|
||||||
|
|
||||||
# Clean up Dock entries for uninstalled apps
|
# 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"
|
source "$SCRIPT_DIR/lib/core/common.sh"
|
||||||
|
|
||||||
# Version info
|
# Version info
|
||||||
VERSION="1.13.0"
|
VERSION="1.13.5"
|
||||||
MOLE_TAGLINE="can dig deep to clean your Mac."
|
MOLE_TAGLINE="Deep clean and optimize your Mac."
|
||||||
|
|
||||||
# Check if Touch ID is already configured
|
# Check if Touch ID is already configured
|
||||||
is_touchid_configured() {
|
is_touchid_configured() {
|
||||||
@@ -181,7 +181,44 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_version() {
|
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() {
|
show_help() {
|
||||||
|
|||||||
Reference in New Issue
Block a user