mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:39:42 +00:00
🎨 Updates for better use and support
This commit is contained in:
110
bin/clean.sh
110
bin/clean.sh
@@ -130,26 +130,74 @@ safe_clean() {
|
|||||||
local total_size_bytes=0
|
local total_size_bytes=0
|
||||||
local total_count=0
|
local total_count=0
|
||||||
|
|
||||||
|
# Optimized: skip size calculation for empty checks, just try to delete
|
||||||
|
# Size calculation is the slowest part - do it in parallel
|
||||||
|
local -a existing_paths=()
|
||||||
for path in "${targets[@]}"; do
|
for path in "${targets[@]}"; do
|
||||||
local size_bytes=0
|
[[ -e "$path" ]] && existing_paths+=("$path")
|
||||||
local count=0
|
|
||||||
|
|
||||||
if [[ -e "$path" ]]; then
|
|
||||||
size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
|
||||||
count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
||||||
|
|
||||||
if [[ "$count" -eq 0 || "$size_bytes" -eq 0 ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf "$path" 2>/dev/null || true
|
|
||||||
|
|
||||||
((total_size_bytes += size_bytes))
|
|
||||||
((total_count += count))
|
|
||||||
removed_any=1
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ ${#existing_paths[@]} -eq 0 ]]; then
|
||||||
|
LAST_CLEAN_RESULT=0
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fast parallel processing for multiple targets
|
||||||
|
if [[ ${#existing_paths[@]} -gt 3 ]]; then
|
||||||
|
local temp_dir=$(mktemp -d)
|
||||||
|
|
||||||
|
# Launch parallel du jobs (bash 3.2 compatible)
|
||||||
|
local -a pids=()
|
||||||
|
for path in "${existing_paths[@]}"; do
|
||||||
|
(
|
||||||
|
local size=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
echo "$size $count" > "$temp_dir/$(echo -n "$path" | shasum -a 256 | cut -d' ' -f1)"
|
||||||
|
) &
|
||||||
|
pids+=($!)
|
||||||
|
|
||||||
|
# Limit to 15 parallel jobs (bash 3.2 compatible)
|
||||||
|
if (( ${#pids[@]} >= 15 )); then
|
||||||
|
wait "${pids[0]}" 2>/dev/null || true
|
||||||
|
pids=("${pids[@]:1}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for remaining jobs
|
||||||
|
for pid in "${pids[@]}"; do
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Collect results and delete
|
||||||
|
for path in "${existing_paths[@]}"; do
|
||||||
|
local hash=$(echo -n "$path" | shasum -a 256 | cut -d' ' -f1)
|
||||||
|
if [[ -f "$temp_dir/$hash" ]]; then
|
||||||
|
read -r size count < "$temp_dir/$hash"
|
||||||
|
if [[ "$count" -gt 0 && "$size" -gt 0 ]]; then
|
||||||
|
rm -rf "$path" 2>/dev/null || true
|
||||||
|
((total_size_bytes += size))
|
||||||
|
((total_count += count))
|
||||||
|
removed_any=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
else
|
||||||
|
# Serial processing for few targets (faster than parallel overhead)
|
||||||
|
for path in "${existing_paths[@]}"; do
|
||||||
|
local size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
if [[ "$count" -gt 0 && "$size_bytes" -gt 0 ]]; then
|
||||||
|
rm -rf "$path" 2>/dev/null || true
|
||||||
|
((total_size_bytes += size_bytes))
|
||||||
|
((total_count += count))
|
||||||
|
removed_any=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Only show output if something was actually cleaned
|
# Only show output if something was actually cleaned
|
||||||
if [[ $removed_any -eq 1 ]]; then
|
if [[ $removed_any -eq 1 ]]; then
|
||||||
local size_human
|
local size_human
|
||||||
@@ -178,7 +226,7 @@ safe_clean() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start_cleanup() {
|
start_cleanup() {
|
||||||
echo "Removing app caches, browser data, developer tools, and temporary files..."
|
echo "Mole will remove app caches, browser data, developer tools, and temporary files."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if we're in an interactive terminal
|
# Check if we're in an interactive terminal
|
||||||
@@ -191,13 +239,33 @@ start_cleanup() {
|
|||||||
else
|
else
|
||||||
# Non-interactive mode - skip password prompt
|
# Non-interactive mode - skip password prompt
|
||||||
password=""
|
password=""
|
||||||
log_info "Running in non-interactive mode, skipping system-level cleanup."
|
echo ""
|
||||||
|
echo -e "${BLUE}ℹ${NC} Running in non-interactive mode"
|
||||||
|
echo " • System-level cleanup will be skipped (requires password)"
|
||||||
|
echo " • User-level cleanup will proceed automatically"
|
||||||
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
|
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then
|
||||||
SYSTEM_CLEAN=true
|
SYSTEM_CLEAN=true
|
||||||
# Start sudo keepalive with shorter intervals for reliability
|
# Start sudo keepalive with error handling and shorter intervals
|
||||||
while true; do sudo -n true; sleep 30; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
|
(
|
||||||
|
local retry_count=0
|
||||||
|
while true; do
|
||||||
|
if ! sudo -n true 2>/dev/null; then
|
||||||
|
((retry_count++))
|
||||||
|
if [[ $retry_count -ge 3 ]]; then
|
||||||
|
log_warning "Sudo keepalive failed, system-level cleanup may be interrupted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
retry_count=0
|
||||||
|
sleep 30
|
||||||
|
kill -0 "$$" 2>/dev/null || exit
|
||||||
|
done
|
||||||
|
) 2>/dev/null &
|
||||||
SUDO_KEEPALIVE_PID=$!
|
SUDO_KEEPALIVE_PID=$!
|
||||||
log_info "Starting comprehensive cleanup with admin privileges..."
|
log_info "Starting comprehensive cleanup with admin privileges..."
|
||||||
else
|
else
|
||||||
|
|||||||
122
bin/uninstall.sh
122
bin/uninstall.sh
@@ -90,6 +90,30 @@ get_app_last_used() {
|
|||||||
|
|
||||||
# Scan applications and collect information
|
# Scan applications and collect information
|
||||||
scan_applications() {
|
scan_applications() {
|
||||||
|
# Cache configuration
|
||||||
|
local cache_dir="$HOME/.cache/mole"
|
||||||
|
local cache_file="$cache_dir/app_scan_cache"
|
||||||
|
local cache_meta="$cache_dir/app_scan_meta"
|
||||||
|
local cache_ttl=3600 # 1 hour cache validity
|
||||||
|
|
||||||
|
mkdir -p "$cache_dir" 2>/dev/null
|
||||||
|
|
||||||
|
# Quick count of current apps
|
||||||
|
local current_app_count=$(find /Applications -name "*.app" -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
# Check if cache is valid
|
||||||
|
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
|
||||||
|
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
||||||
|
local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
# Cache is valid if: age < TTL AND app count matches
|
||||||
|
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
|
||||||
|
echo "Using cached app list (${cache_age}s old, $current_app_count apps) ✓" >&2
|
||||||
|
echo "$cache_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
local temp_file=$(mktemp)
|
local temp_file=$(mktemp)
|
||||||
|
|
||||||
echo -n "Scanning... " >&2
|
echo -n "Scanning... " >&2
|
||||||
@@ -160,24 +184,9 @@ scan_applications() {
|
|||||||
|
|
||||||
# Select the first (best) candidate
|
# Select the first (best) candidate
|
||||||
display_name="${candidates[0]:-$app_name}"
|
display_name="${candidates[0]:-$app_name}"
|
||||||
|
|
||||||
# Brand name mapping for better user recognition (post-process)
|
# Apply brand name mapping from common.sh
|
||||||
case "$display_name" in
|
display_name="$(get_brand_name "$display_name")"
|
||||||
"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
|
fi
|
||||||
|
|
||||||
# Skip protected system apps early
|
# Skip protected system apps early
|
||||||
@@ -189,20 +198,21 @@ scan_applications() {
|
|||||||
app_data_tuples+=("${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)
|
done < <(find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null)
|
||||||
|
|
||||||
# Second pass: process each app with accurate size calculation
|
# Second pass: process each app with parallel size calculation
|
||||||
local app_count=0
|
local app_count=0
|
||||||
local total_apps=${#app_data_tuples[@]}
|
local total_apps=${#app_data_tuples[@]}
|
||||||
|
local max_parallel=10 # Process 10 apps in parallel
|
||||||
|
local pids=()
|
||||||
|
|
||||||
|
# Process app metadata extraction function
|
||||||
|
process_app_metadata() {
|
||||||
|
local app_data_tuple="$1"
|
||||||
|
local output_file="$2"
|
||||||
|
local current_epoch="$3"
|
||||||
|
|
||||||
for app_data_tuple in "${app_data_tuples[@]}"; do
|
|
||||||
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
|
IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple"
|
||||||
|
|
||||||
# Show progress every few items
|
# Parallel size calculation
|
||||||
((app_count++))
|
|
||||||
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
|
|
||||||
local app_size="N/A"
|
local app_size="N/A"
|
||||||
if [[ -d "$app_path" ]]; then
|
if [[ -d "$app_path" ]]; then
|
||||||
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
||||||
@@ -216,7 +226,6 @@ scan_applications() {
|
|||||||
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||||
|
|
||||||
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
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")
|
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
|
if [[ $last_used_epoch -gt 0 ]]; then
|
||||||
@@ -230,29 +239,17 @@ scan_applications() {
|
|||||||
last_used="${days_ago} days ago"
|
last_used="${days_ago} days ago"
|
||||||
elif [[ $days_ago -lt 30 ]]; then
|
elif [[ $days_ago -lt 30 ]]; then
|
||||||
local weeks_ago=$(( days_ago / 7 ))
|
local weeks_ago=$(( days_ago / 7 ))
|
||||||
if [[ $weeks_ago -eq 1 ]]; then
|
[[ $weeks_ago -eq 1 ]] && last_used="1 week ago" || last_used="${weeks_ago} weeks ago"
|
||||||
last_used="1 week ago"
|
|
||||||
else
|
|
||||||
last_used="${weeks_ago} weeks ago"
|
|
||||||
fi
|
|
||||||
elif [[ $days_ago -lt 365 ]]; then
|
elif [[ $days_ago -lt 365 ]]; then
|
||||||
local months_ago=$(( days_ago / 30 ))
|
local months_ago=$(( days_ago / 30 ))
|
||||||
if [[ $months_ago -eq 1 ]]; then
|
[[ $months_ago -eq 1 ]] && last_used="1 month ago" || last_used="${months_ago} months ago"
|
||||||
last_used="1 month ago"
|
|
||||||
else
|
|
||||||
last_used="${months_ago} months ago"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
local years_ago=$(( days_ago / 365 ))
|
local years_ago=$(( days_ago / 365 ))
|
||||||
if [[ $years_ago -eq 1 ]]; then
|
[[ $years_ago -eq 1 ]] && last_used="1 year ago" || last_used="${years_ago} years ago"
|
||||||
last_used="1 year ago"
|
|
||||||
else
|
|
||||||
last_used="${years_ago} years ago"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Fallback to file modification time if no usage metadata
|
# Fallback to file modification time
|
||||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||||
if [[ $last_used_epoch -gt 0 ]]; then
|
if [[ $last_used_epoch -gt 0 ]]; then
|
||||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||||
@@ -267,8 +264,33 @@ scan_applications() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Format: epoch|app_path|display_name|bundle_id|size|last_used_display
|
# Write to output file atomically
|
||||||
echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$temp_file"
|
echo "${last_used_epoch}|${app_path}|${display_name}|${bundle_id}|${app_size}|${last_used}" >> "$output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
export -f process_app_metadata
|
||||||
|
|
||||||
|
# Process apps in parallel batches
|
||||||
|
for app_data_tuple in "${app_data_tuples[@]}"; do
|
||||||
|
((app_count++))
|
||||||
|
|
||||||
|
# Launch background process
|
||||||
|
process_app_metadata "$app_data_tuple" "$temp_file" "$current_epoch" &
|
||||||
|
pids+=($!)
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
echo -ne "\rScanning... $app_count/$total_apps" >&2
|
||||||
|
|
||||||
|
# Wait if we've hit max parallel limit
|
||||||
|
if (( ${#pids[@]} >= max_parallel )); then
|
||||||
|
wait "${pids[0]}" 2>/dev/null
|
||||||
|
pids=("${pids[@]:1}") # Remove first pid
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for remaining background processes
|
||||||
|
for pid in "${pids[@]}"; do
|
||||||
|
wait "$pid" 2>/dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -e "\rFound $app_count applications ✓" >&2
|
echo -e "\rFound $app_count applications ✓" >&2
|
||||||
@@ -279,9 +301,13 @@ scan_applications() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sort by last used (oldest first) and return the temp file path
|
# Sort by last used (oldest first) and cache the result
|
||||||
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"
|
||||||
|
|
||||||
|
# Update cache with app count metadata
|
||||||
|
cp "${temp_file}.sorted" "$cache_file" 2>/dev/null || true
|
||||||
|
echo "$current_app_count" > "$cache_meta" 2>/dev/null || true
|
||||||
echo "${temp_file}.sorted"
|
echo "${temp_file}.sorted"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +505,7 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
# 直接执行批量卸载,确认已在批量卸载函数中处理
|
# Execute batch uninstallation, confirmation handled in batch_uninstall_applications
|
||||||
batch_uninstall_applications
|
batch_uninstall_applications
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ select_apps_for_uninstall() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "🗑️ App Uninstaller"
|
echo "🗑️ App Uninstaller"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Mole will uninstall selected apps and clean all their related files."
|
||||||
|
echo ""
|
||||||
echo "Found ${#apps_data[@]} apps. Select apps to remove:"
|
echo "Found ${#apps_data[@]} apps. Select apps to remove:"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ batch_uninstall_applications() {
|
|||||||
local total_estimated_size=0
|
local total_estimated_size=0
|
||||||
local -a app_details=()
|
local -a app_details=()
|
||||||
|
|
||||||
echo "📋 Analyzing selected applications..."
|
echo ""
|
||||||
|
echo -e "${BLUE}📋 Analyzing selected applications...${NC}"
|
||||||
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 <<< "$selected_app"
|
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
|
||||||
|
|
||||||
@@ -51,22 +52,24 @@ batch_uninstall_applications() {
|
|||||||
|
|
||||||
# Show summary and get batch confirmation
|
# Show summary and get batch confirmation
|
||||||
echo ""
|
echo ""
|
||||||
echo "Will remove ${#selected_apps[@]} applications, free $size_display"
|
echo -e "${YELLOW}📦 Will remove ${BLUE}${#selected_apps[@]}${YELLOW} applications, free ${GREEN}$size_display${NC}"
|
||||||
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
||||||
echo "Running apps will be force-quit: ${running_apps[*]}"
|
echo -e "${YELLOW}⚠️ Running apps will be force-quit: ${RED}${running_apps[*]}${NC}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Press ENTER to confirm, or any other key to cancel: " -r
|
echo -e -n "${BLUE}Press ENTER to confirm, or any other key to cancel:${NC} "
|
||||||
|
read -r
|
||||||
|
|
||||||
if [[ -n "$REPLY" ]]; then
|
if [[ -n "$REPLY" ]]; then
|
||||||
log_info "Uninstallation cancelled by user"
|
log_info "Uninstallation cancelled by user"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "⚡ Starting uninstallation in 3 seconds... (Press Ctrl+C to abort)"
|
echo -e "${PURPLE}⚡ Starting uninstallation in 3 seconds...${NC} ${YELLOW}(Press Ctrl+C to abort)${NC}"
|
||||||
sleep 1 && echo "⚡ 2..."
|
sleep 1 && echo -e "${PURPLE}⚡ ${BLUE}2${PURPLE}...${NC}"
|
||||||
sleep 1 && echo "⚡ 1..."
|
sleep 1 && echo -e "${PURPLE}⚡ ${BLUE}1${PURPLE}...${NC}"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
echo -e "${GREEN}✨ Let's go!${NC}"
|
||||||
|
|
||||||
# Force quit running apps first (batch)
|
# Force quit running apps first (batch)
|
||||||
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
||||||
@@ -92,7 +95,7 @@ batch_uninstall_applications() {
|
|||||||
# Decode the related files list
|
# Decode the related files list
|
||||||
local related_files=$(echo "$encoded_files" | base64 -d)
|
local related_files=$(echo "$encoded_files" | base64 -d)
|
||||||
|
|
||||||
echo "🗑️ Uninstalling: $app_name"
|
echo -e "${YELLOW}🗑️ Uninstalling: ${BLUE}$app_name${NC}"
|
||||||
|
|
||||||
# Remove the application
|
# Remove the application
|
||||||
if rm -rf "$app_path" 2>/dev/null; then
|
if rm -rf "$app_path" 2>/dev/null; then
|
||||||
@@ -127,24 +130,24 @@ batch_uninstall_applications() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "===================================================================="
|
echo "===================================================================="
|
||||||
echo "🎉 UNINSTALLATION COMPLETE!"
|
echo "🎉 UNINSTALLATION COMPLETE!"
|
||||||
|
|
||||||
if [[ $success_count -gt 0 ]]; then
|
if [[ $success_count -gt 0 ]]; then
|
||||||
if [[ $total_size_freed -gt 1048576 ]]; then
|
if [[ $total_size_freed -gt 1048576 ]]; then
|
||||||
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fGB", $1/1024/1024}')
|
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}')
|
||||||
elif [[ $total_size_freed -gt 1024 ]]; then
|
elif [[ $total_size_freed -gt 1024 ]]; then
|
||||||
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
|
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
|
||||||
else
|
else
|
||||||
local freed_display="${total_size_freed}KB"
|
local freed_display="${total_size_freed}KB"
|
||||||
fi
|
fi
|
||||||
echo "🗑️ Apps uninstalled: $success_count | Space freed: $freed_display"
|
echo "🗑️ Apps uninstalled: $success_count | Space freed: ${GREEN}${freed_display}${NC}"
|
||||||
else
|
else
|
||||||
echo "🗑️ No applications were uninstalled"
|
echo "🗑️ No applications were uninstalled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $failed_count -gt 0 ]]; then
|
if [[ $failed_count -gt 0 ]]; then
|
||||||
echo "⚠️ Failed to uninstall: $failed_count"
|
echo -e "${RED}⚠️ Failed to uninstall: $failed_count${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "===================================================================="
|
echo "===================================================================="
|
||||||
if [[ $failed_count -gt 0 ]]; then
|
if [[ $failed_count -gt 0 ]]; then
|
||||||
log_warning "$failed_count applications failed to uninstall"
|
log_warning "$failed_count applications failed to uninstall"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ log_info() {
|
|||||||
|
|
||||||
log_success() {
|
log_success() {
|
||||||
rotate_log
|
rotate_log
|
||||||
echo -e "${GREEN}✅ $1${NC}"
|
echo -e " ${GREEN}✓${NC} $1"
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,8 @@ read_key() {
|
|||||||
*) echo "OTHER" ;;
|
*) echo "OTHER" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
echo "OTHER"
|
# ESC pressed alone - treat as quit
|
||||||
|
echo "QUIT"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*) echo "OTHER" ;;
|
*) echo "OTHER" ;;
|
||||||
@@ -253,15 +254,22 @@ readonly PRESERVED_BUNDLE_PATTERNS=(
|
|||||||
".GlobalPreferences"
|
".GlobalPreferences"
|
||||||
|
|
||||||
# Input methods (critical for international users)
|
# Input methods (critical for international users)
|
||||||
"com.tencent.inputmethod.*"
|
# Specific input method bundles
|
||||||
"com.sogou.*"
|
"com.tencent.inputmethod.QQInput"
|
||||||
"com.baidu.*"
|
"com.sogou.inputmethod.*"
|
||||||
"*.inputmethod.*"
|
"com.baidu.inputmethod.*"
|
||||||
"*input*"
|
"com.apple.inputmethod.*"
|
||||||
"*inputmethod*"
|
"com.googlecode.rimeime.*"
|
||||||
"*InputMethod*"
|
"im.rime.*"
|
||||||
"*ime*"
|
"org.pqrs.Karabiner*"
|
||||||
"*IME*"
|
# Generic patterns (more conservative)
|
||||||
|
"*.inputmethod"
|
||||||
|
"*.InputMethod"
|
||||||
|
"*IME"
|
||||||
|
# Keep system input services safe
|
||||||
|
"com.apple.inputsource*"
|
||||||
|
"com.apple.TextInputMenuAgent"
|
||||||
|
"com.apple.TextInputSwitcher"
|
||||||
|
|
||||||
# Cleanup and system tools (avoid infinite loops and preserve licenses)
|
# Cleanup and system tools (avoid infinite loops and preserve licenses)
|
||||||
"com.nektony.*" # App Cleaner & Uninstaller
|
"com.nektony.*" # App Cleaner & Uninstaller
|
||||||
@@ -382,3 +390,27 @@ calculate_total_size() {
|
|||||||
|
|
||||||
echo "$total_kb"
|
echo "$total_kb"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get normalized brand name (bash 3.2 compatible using case statement)
|
||||||
|
get_brand_name() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
# Brand name mapping for better user recognition
|
||||||
|
case "$name" in
|
||||||
|
"qiyimac"|"爱奇艺") echo "iQiyi" ;;
|
||||||
|
"wechat"|"微信") echo "WeChat" ;;
|
||||||
|
"QQ") echo "QQ" ;;
|
||||||
|
"VooV Meeting"|"腾讯会议") echo "VooV Meeting" ;;
|
||||||
|
"dingtalk"|"钉钉") echo "DingTalk" ;;
|
||||||
|
"NeteaseMusic"|"网易云音乐") echo "NetEase Music" ;;
|
||||||
|
"BaiduNetdisk"|"百度网盘") echo "Baidu NetDisk" ;;
|
||||||
|
"alipay"|"支付宝") echo "Alipay" ;;
|
||||||
|
"taobao"|"淘宝") echo "Taobao" ;;
|
||||||
|
"futunn"|"富途牛牛") echo "Futu NiuNiu" ;;
|
||||||
|
"tencent lemon"|"Tencent Lemon Cleaner") echo "Tencent Lemon" ;;
|
||||||
|
"keynote"|"Keynote") echo "Keynote" ;;
|
||||||
|
"pages"|"Pages") echo "Pages" ;;
|
||||||
|
"numbers"|"Numbers") echo "Numbers" ;;
|
||||||
|
*) echo "$name" ;; # Return original if no mapping found
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|||||||
80
mole
80
mole
@@ -21,11 +21,72 @@ source "$SCRIPT_DIR/lib/common.sh"
|
|||||||
|
|
||||||
# Version info
|
# Version info
|
||||||
VERSION="1.1.0"
|
VERSION="1.1.0"
|
||||||
MOLE_TAGLINE="Dig deep like a mole to clean your Mac."
|
MOLE_TAGLINE="can dig deep to clean your Mac."
|
||||||
|
|
||||||
|
# Check for updates (non-blocking, cached)
|
||||||
|
check_for_updates() {
|
||||||
|
local cache_dir="$HOME/.cache/mole"
|
||||||
|
local version_cache="$cache_dir/version_check"
|
||||||
|
local check_interval=86400 # Check once per day (24 hours)
|
||||||
|
|
||||||
|
mkdir -p "$cache_dir" 2>/dev/null
|
||||||
|
|
||||||
|
# Check if we should run version check (based on cache age)
|
||||||
|
if [[ -f "$version_cache" ]]; then
|
||||||
|
local cache_age=$(($(date +%s) - $(stat -f%m "$version_cache" 2>/dev/null || echo 0)))
|
||||||
|
if [[ $cache_age -lt $check_interval ]]; then
|
||||||
|
# Cache is still fresh, show cached message if exists
|
||||||
|
if [[ -s "$version_cache" ]]; then
|
||||||
|
cat "$version_cache"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run version check in background (non-blocking)
|
||||||
|
(
|
||||||
|
local latest_version=""
|
||||||
|
local timeout=3 # 3 second timeout for version check
|
||||||
|
|
||||||
|
# Try to fetch latest version from GitHub with timeout
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
latest_version=$(curl -fsSL --connect-timeout 2 --max-time $timeout \
|
||||||
|
"https://api.github.com/repos/tw93/mole/releases/latest" 2>/dev/null | \
|
||||||
|
grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^[Vv]//')
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
latest_version=$(wget -qO- --timeout=$timeout --tries=1 \
|
||||||
|
"https://api.github.com/repos/tw93/mole/releases/latest" 2>/dev/null | \
|
||||||
|
grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^[Vv]//')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare versions if fetch succeeded
|
||||||
|
if [[ -n "$latest_version" && "$latest_version" != "$VERSION" ]]; then
|
||||||
|
# Version mismatch - cache the update message
|
||||||
|
local msg="${YELLOW}📢 New version available: ${GREEN}${latest_version}${YELLOW} (current: ${VERSION})${NC}\n Run ${GREEN}mole update${YELLOW} to upgrade${NC}"
|
||||||
|
echo -e "$msg" > "$version_cache"
|
||||||
|
echo -e "$msg"
|
||||||
|
else
|
||||||
|
# Up to date or check failed - clear cache
|
||||||
|
echo "" > "$version_cache"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Touch cache file to update timestamp
|
||||||
|
touch "$version_cache" 2>/dev/null
|
||||||
|
) &
|
||||||
|
|
||||||
|
# Don't wait for background check
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
show_brand_banner() {
|
show_brand_banner() {
|
||||||
printf '%b🦡 Mole — %s%b\n' \
|
cat << EOF
|
||||||
"$GREEN" "$MOLE_TAGLINE" "$NC"
|
${GREEN} __ __ _ ${NC}
|
||||||
|
${GREEN}| \/ | ___ | | ___ ${NC}
|
||||||
|
${GREEN}| |\/| |/ _ \| |/ _ \\${NC}
|
||||||
|
${GREEN}| | | | (_) | | __/${NC} ${BLUE}https://github.com/tw93/mole${NC}
|
||||||
|
${GREEN}|_| |_|\___/|_|\___|${NC} ${GREEN}${MOLE_TAGLINE}${NC}
|
||||||
|
|
||||||
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
show_version() {
|
show_version() {
|
||||||
@@ -58,17 +119,17 @@ update_mole() {
|
|||||||
local tmp_installer
|
local tmp_installer
|
||||||
tmp_installer="$(mktemp)" || { log_error "Failed to create temp file"; exit 1; }
|
tmp_installer="$(mktemp)" || { log_error "Failed to create temp file"; exit 1; }
|
||||||
|
|
||||||
# Download installer
|
# Download installer with timeout
|
||||||
if command -v curl >/dev/null 2>&1; then
|
if command -v curl >/dev/null 2>&1; then
|
||||||
if ! curl -fsSL "$installer_url" -o "$tmp_installer"; then
|
if ! curl -fsSL --connect-timeout 10 --max-time 60 "$installer_url" -o "$tmp_installer"; then
|
||||||
rm -f "$tmp_installer"
|
rm -f "$tmp_installer"
|
||||||
log_error "Failed to download installer"
|
log_error "Failed to download installer (network timeout or error)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
if ! wget -qO "$tmp_installer" "$installer_url"; then
|
if ! wget --timeout=10 --tries=3 -qO "$tmp_installer" "$installer_url"; then
|
||||||
rm -f "$tmp_installer"
|
rm -f "$tmp_installer"
|
||||||
log_error "Failed to download installer"
|
log_error "Failed to download installer (network timeout or error)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -183,6 +244,9 @@ interactive_main_menu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
# Check for updates (non-blocking, won't delay startup)
|
||||||
|
check_for_updates
|
||||||
|
|
||||||
case "${1:-""}" in
|
case "${1:-""}" in
|
||||||
"clean")
|
"clean")
|
||||||
exec "$SCRIPT_DIR/bin/clean.sh"
|
exec "$SCRIPT_DIR/bin/clean.sh"
|
||||||
|
|||||||
Reference in New Issue
Block a user