1
0
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:
Tw93
2025-09-30 00:43:52 +08:00
parent 6897b95ca6
commit e2fd35f8a7
11 changed files with 988 additions and 1820 deletions

View File

@@ -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