mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:04:42 +00:00
🎨 Continue to upgrade to easy to use
This commit is contained in:
263
bin/uninstall.sh
263
bin/uninstall.sh
@@ -11,41 +11,24 @@ set -euo pipefail
|
||||
# Get script directory and source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../lib/common.sh"
|
||||
source "$SCRIPT_DIR/../lib/menu.sh"
|
||||
source "$SCRIPT_DIR/../lib/paginated_menu.sh"
|
||||
source "$SCRIPT_DIR/../lib/app_selector.sh"
|
||||
source "$SCRIPT_DIR/../lib/batch_uninstall.sh"
|
||||
|
||||
# Basic preserved bundle patterns
|
||||
PRESERVED_BUNDLE_PATTERNS=(
|
||||
"com.apple.*"
|
||||
"com.nektony.*"
|
||||
)
|
||||
|
||||
# Check if bundle should be preserved (system apps)
|
||||
should_preserve_bundle() {
|
||||
local bundle_id="$1"
|
||||
for pattern in "${PRESERVED_BUNDLE_PATTERNS[@]}"; do
|
||||
if [[ "$bundle_id" == $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# Note: Bundle preservation logic is now in lib/common.sh
|
||||
|
||||
# Help information
|
||||
show_help() {
|
||||
echo "Mole - Interactive App Uninstaller"
|
||||
echo "========================================"
|
||||
echo "App Uninstaller"
|
||||
echo "==============="
|
||||
echo ""
|
||||
echo "Description: Interactive tool to uninstall applications and clean their data"
|
||||
echo "Uninstall applications and clean their data completely."
|
||||
echo ""
|
||||
echo "Features:"
|
||||
echo " • Navigate with ↑/↓ arrow keys"
|
||||
echo " • Select/deselect apps with SPACE"
|
||||
echo " • Confirm selection with ENTER"
|
||||
echo " • Quit anytime with 'q'"
|
||||
echo " • Apps sorted by last usage time"
|
||||
echo " • Comprehensive cleanup of app data"
|
||||
echo "Controls:"
|
||||
echo " ↑/↓ Navigate"
|
||||
echo " SPACE Select/deselect"
|
||||
echo " ENTER Confirm"
|
||||
echo " Q Quit"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " ./uninstall.sh Launch interactive uninstaller"
|
||||
@@ -69,7 +52,7 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
fi
|
||||
|
||||
# Initialize global variables
|
||||
declare -a selected_apps=()
|
||||
selected_apps=() # Global array for app selection
|
||||
declare -a apps_data=()
|
||||
declare -a selection_state=()
|
||||
current_line=0
|
||||
@@ -109,7 +92,7 @@ get_app_last_used() {
|
||||
scan_applications() {
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
echo -n "Scanning applications... " >&2
|
||||
echo -n "Scanning... " >&2
|
||||
|
||||
# Pre-cache current epoch to avoid repeated calls
|
||||
local current_epoch=$(date "+%s")
|
||||
@@ -121,10 +104,80 @@ scan_applications() {
|
||||
|
||||
local app_name=$(basename "$app_path" .app)
|
||||
|
||||
# Quick bundle ID check first (only if plist exists)
|
||||
# Try to get English name from bundle info, fallback to folder name
|
||||
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=$(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=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||
local 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}"
|
||||
|
||||
# Brand name mapping for better user recognition (post-process)
|
||||
case "$display_name" in
|
||||
"qiyimac"|"爱奇艺") display_name="iQiyi" ;;
|
||||
"wechat"|"微信") display_name="WeChat" ;;
|
||||
"QQ"|"QQ") display_name="QQ" ;;
|
||||
"VooV Meeting"|"腾讯会议") display_name="VooV Meeting" ;;
|
||||
"dingtalk"|"钉钉") display_name="DingTalk" ;;
|
||||
"NeteaseMusic"|"网易云音乐") display_name="NetEase Music" ;;
|
||||
"BaiduNetdisk"|"百度网盘") display_name="Baidu NetDisk" ;;
|
||||
"alipay"|"支付宝") display_name="Alipay" ;;
|
||||
"taobao"|"淘宝") display_name="Taobao" ;;
|
||||
"futunn"|"富途牛牛") display_name="Futu NiuNiu" ;;
|
||||
"tencent lemon"|"Tencent Lemon Cleaner") display_name="Tencent Lemon" ;;
|
||||
"keynote"|"Keynote") display_name="Keynote" ;;
|
||||
"pages"|"Pages") display_name="Pages" ;;
|
||||
"numbers"|"Numbers") display_name="Numbers" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Skip protected system apps early
|
||||
@@ -132,8 +185,8 @@ scan_applications() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# Store tuple: app_path|app_name|bundle_id
|
||||
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}")
|
||||
# Store tuple: app_path|app_name|bundle_id|display_name
|
||||
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
|
||||
done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null)
|
||||
|
||||
# Second pass: process each app with accurate size calculation
|
||||
@@ -141,12 +194,12 @@ scan_applications() {
|
||||
local total_apps=${#app_data_tuples[@]}
|
||||
|
||||
for app_data_tuple in "${app_data_tuples[@]}"; do
|
||||
IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple"
|
||||
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
|
||||
|
||||
# Show progress every few items
|
||||
((app_count++))
|
||||
if (( app_count % 3 == 0 )) || [[ $app_count -eq $total_apps ]]; then
|
||||
echo -ne "\rScanning applications... processing $app_count/$total_apps apps" >&2
|
||||
if (( app_count % 5 == 0 )) || [[ $app_count -eq $total_apps ]]; then
|
||||
echo -ne "\rScanning... $app_count/$total_apps" >&2
|
||||
fi
|
||||
|
||||
# Accurate size calculation - this is what takes time but user wants it
|
||||
@@ -155,27 +208,70 @@ scan_applications() {
|
||||
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
||||
fi
|
||||
|
||||
# Simplified last used check using file modification time
|
||||
local last_used="Old"
|
||||
# Get real last used date from macOS metadata
|
||||
local last_used="Never"
|
||||
local last_used_epoch=0
|
||||
|
||||
if [[ -d "$app_path" ]]; then
|
||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
if [[ $days_ago -lt 30 ]]; then
|
||||
last_used="Recent"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
last_used="This year"
|
||||
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||
|
||||
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
||||
# Convert macOS date format to epoch
|
||||
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0")
|
||||
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
|
||||
if [[ $days_ago -eq 0 ]]; then
|
||||
last_used="Today"
|
||||
elif [[ $days_ago -eq 1 ]]; then
|
||||
last_used="Yesterday"
|
||||
elif [[ $days_ago -lt 7 ]]; then
|
||||
last_used="${days_ago} days ago"
|
||||
elif [[ $days_ago -lt 30 ]]; then
|
||||
local weeks_ago=$(( days_ago / 7 ))
|
||||
if [[ $weeks_ago -eq 1 ]]; then
|
||||
last_used="1 week ago"
|
||||
else
|
||||
last_used="${weeks_ago} weeks ago"
|
||||
fi
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
local months_ago=$(( days_ago / 30 ))
|
||||
if [[ $months_ago -eq 1 ]]; then
|
||||
last_used="1 month ago"
|
||||
else
|
||||
last_used="${months_ago} months ago"
|
||||
fi
|
||||
else
|
||||
local years_ago=$(( days_ago / 365 ))
|
||||
if [[ $years_ago -eq 1 ]]; then
|
||||
last_used="1 year ago"
|
||||
else
|
||||
last_used="${years_ago} years ago"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Fallback to file modification time if no usage metadata
|
||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
if [[ $days_ago -lt 30 ]]; then
|
||||
last_used="Recent"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
last_used="This year"
|
||||
else
|
||||
last_used="Old"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Format: epoch|app_path|app_name|bundle_id|size|last_used_display
|
||||
echo "${last_used_epoch}|${app_path}|${app_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
||||
# Format: epoch|app_path|display_name|bundle_id|size|last_used_display
|
||||
echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
||||
done
|
||||
|
||||
echo -e "\rScanning applications... found $app_count apps ✓" >&2
|
||||
echo -e "\rFound $app_count applications ✓" >&2
|
||||
|
||||
# Check if we found any applications
|
||||
if [[ ! -s "$temp_file" ]]; then
|
||||
@@ -221,57 +317,8 @@ load_applications() {
|
||||
# Read a single key with proper escape sequence handling
|
||||
# This function has been replaced by the menu.sh library
|
||||
|
||||
# Old interactive_app_selection and show_selection_help functions removed
|
||||
# They have been replaced by the new menu system in lib/app_selector.sh
|
||||
|
||||
# Find and list app-related files
|
||||
find_app_files() {
|
||||
local bundle_id="$1"
|
||||
local app_name="$2"
|
||||
local -a files_to_clean=()
|
||||
|
||||
# Application Support
|
||||
[[ -d ~/Library/Application\ Support/"$app_name" ]] && files_to_clean+=("$HOME/Library/Application Support/$app_name")
|
||||
[[ -d ~/Library/Application\ Support/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Application Support/$bundle_id")
|
||||
|
||||
# Caches
|
||||
[[ -d ~/Library/Caches/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Caches/$bundle_id")
|
||||
|
||||
# Preferences
|
||||
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
||||
|
||||
# Logs
|
||||
[[ -d ~/Library/Logs/"$app_name" ]] && files_to_clean+=("$HOME/Library/Logs/$app_name")
|
||||
[[ -d ~/Library/Logs/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Logs/$bundle_id")
|
||||
|
||||
# Saved Application State
|
||||
[[ -d ~/Library/Saved\ Application\ State/"$bundle_id".savedState ]] && files_to_clean+=("$HOME/Library/Saved Application State/$bundle_id.savedState")
|
||||
|
||||
# Containers (sandboxed apps)
|
||||
[[ -d ~/Library/Containers/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Containers/$bundle_id")
|
||||
|
||||
# Group Containers
|
||||
while IFS= read -r -d '' container; do
|
||||
files_to_clean+=("$container")
|
||||
done < <(find ~/Library/Group\ Containers -name "*$bundle_id*" -type d -print0 2>/dev/null)
|
||||
|
||||
printf '%s\n' "${files_to_clean[@]}"
|
||||
}
|
||||
|
||||
# Calculate total size of files
|
||||
calculate_total_size() {
|
||||
local files="$1"
|
||||
local total_kb=0
|
||||
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" && -e "$file" ]]; then
|
||||
local size_kb=$(du -sk "$file" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
((total_kb += size_kb))
|
||||
fi
|
||||
done <<< "$files"
|
||||
|
||||
echo "$total_kb"
|
||||
}
|
||||
# Note: App file discovery and size calculation functions moved to lib/common.sh
|
||||
# Use find_app_files() and calculate_total_size() from common.sh
|
||||
|
||||
# Uninstall selected applications
|
||||
uninstall_applications() {
|
||||
@@ -384,8 +431,8 @@ uninstall_applications() {
|
||||
|
||||
# Cleanup function - restore cursor and clean up
|
||||
cleanup() {
|
||||
# Restore cursor
|
||||
printf '\033[?25h'
|
||||
# Restore cursor using common function
|
||||
show_cursor
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
@@ -394,10 +441,9 @@ trap cleanup EXIT INT TERM
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
echo "🗑️ Mole - Interactive App Uninstaller"
|
||||
echo "============================================"
|
||||
echo
|
||||
|
||||
# Hide cursor during operation
|
||||
hide_cursor
|
||||
|
||||
# Scan applications
|
||||
local apps_file=$(scan_applications)
|
||||
|
||||
@@ -412,17 +458,16 @@ main() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Interactive selection using new menu system
|
||||
# Interactive selection using paginated menu
|
||||
if ! select_apps_for_uninstall; then
|
||||
rm -f "$apps_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Restore cursor for normal interaction
|
||||
printf '\033[?25h'
|
||||
# Restore cursor for normal interaction after selection
|
||||
show_cursor
|
||||
clear
|
||||
echo "You selected ${#selected_apps[@]} application(s) for uninstallation:"
|
||||
echo ""
|
||||
|
||||
if [[ ${#selected_apps[@]} -gt 0 ]]; then
|
||||
for selected_app in "${selected_apps[@]}"; do
|
||||
@@ -439,8 +484,6 @@ main() {
|
||||
|
||||
# Cleanup
|
||||
rm -f "$apps_file"
|
||||
|
||||
log_success "App uninstaller finished"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
|
||||
Reference in New Issue
Block a user