mirror of
https://github.com/tw93/Mole.git
synced 2026-02-08 17:19:19 +00:00
Uninstall function detailed upgrade
This commit is contained in:
@@ -37,10 +37,8 @@ select_apps_for_uninstall() {
|
||||
menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")")
|
||||
done
|
||||
|
||||
# Clear screen before menu (alternate screen preserves main screen)
|
||||
clear_screen
|
||||
|
||||
# Use paginated menu - result will be stored in MOLE_SELECTION_RESULT
|
||||
# Note: paginated_multi_select enters alternate screen and handles clearing
|
||||
MOLE_SELECTION_RESULT=""
|
||||
paginated_multi_select "Select Apps to Remove" "${menu_options[@]}"
|
||||
local exit_code=$?
|
||||
|
||||
@@ -24,6 +24,7 @@ batch_uninstall_applications() {
|
||||
local -a sudo_apps=()
|
||||
local total_estimated_size=0
|
||||
local -a app_details=()
|
||||
local -a dock_cleanup_paths=()
|
||||
|
||||
echo ""
|
||||
# Silent analysis without spinner output (avoid visual flicker)
|
||||
@@ -31,8 +32,8 @@ batch_uninstall_applications() {
|
||||
[[ -z "$selected_app" ]] && continue
|
||||
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
|
||||
|
||||
# Check if app is running
|
||||
if pgrep -f "$app_name" >/dev/null 2>&1; then
|
||||
# Check if app is running (use app path for precise matching)
|
||||
if pgrep -f "$app_path" >/dev/null 2>&1; then
|
||||
running_apps+=("$app_name")
|
||||
fi
|
||||
|
||||
@@ -49,14 +50,46 @@ batch_uninstall_applications() {
|
||||
((total_estimated_size += total_kb))
|
||||
|
||||
# Store details for later use
|
||||
# Base64 encode related_files to handle multi-line data safely
|
||||
local encoded_files=$(echo "$related_files" | base64)
|
||||
# Base64 encode related_files to handle multi-line data safely (single line)
|
||||
local encoded_files
|
||||
encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n')
|
||||
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files")
|
||||
done
|
||||
|
||||
# Format size display (convert KB to bytes for bytes_to_human())
|
||||
local size_display=$(bytes_to_human "$((total_estimated_size * 1024))")
|
||||
|
||||
# Display detailed file list for each app before confirmation
|
||||
echo -e "${PURPLE}Files to be removed:${NC}"
|
||||
echo ""
|
||||
for detail in "${app_details[@]}"; do
|
||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
|
||||
local related_files=$(printf '%s' "$encoded_files" | base64 -d)
|
||||
local app_size_display=$(bytes_to_human "$((total_kb * 1024))")
|
||||
|
||||
echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name} ${GRAY}(${app_size_display})${NC}"
|
||||
echo -e " ${GREEN}✓${NC} $(echo "$app_path" | sed "s|$HOME|~|")"
|
||||
|
||||
# Show related files (limit to 5 most important ones for brevity)
|
||||
local file_count=0
|
||||
local max_files=5
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" && -e "$file" ]]; then
|
||||
if [[ $file_count -lt $max_files ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $(echo "$file" | sed "s|$HOME|~|")"
|
||||
fi
|
||||
((file_count++))
|
||||
fi
|
||||
done <<< "$related_files"
|
||||
|
||||
# Show count of remaining files if truncated
|
||||
if [[ $file_count -gt $max_files ]]; then
|
||||
local remaining=$((file_count - max_files))
|
||||
echo -e " ${GRAY} ... and ${remaining} more files${NC}"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Show summary and get batch confirmation first (before asking for password)
|
||||
local app_total=${#selected_apps[@]}
|
||||
local app_text="app"
|
||||
@@ -100,12 +133,7 @@ batch_uninstall_applications() {
|
||||
if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi
|
||||
|
||||
# Force quit running apps first (batch)
|
||||
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
||||
pkill -f "${running_apps[0]}" 2>/dev/null || true
|
||||
for app_name in "${running_apps[@]:1}"; do pkill -f "$app_name" 2>/dev/null || true; done
|
||||
sleep 2
|
||||
if pgrep -f "${running_apps[0]}" >/dev/null 2>&1; then sleep 1; fi
|
||||
fi
|
||||
# Note: Apps are already killed in the individual uninstall loop below with app_path for precise matching
|
||||
|
||||
# Perform uninstallations (silent mode, show results at end)
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
@@ -114,11 +142,11 @@ batch_uninstall_applications() {
|
||||
local -a success_items=()
|
||||
for detail in "${app_details[@]}"; do
|
||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
|
||||
local related_files=$(echo "$encoded_files" | base64 -d)
|
||||
local related_files=$(printf '%s' "$encoded_files" | base64 -d)
|
||||
local reason=""
|
||||
local needs_sudo=false
|
||||
[[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]] && needs_sudo=true
|
||||
if ! force_kill_app "$app_name"; then
|
||||
if ! force_kill_app "$app_name" "$app_path"; then
|
||||
reason="still running"
|
||||
fi
|
||||
if [[ -z "$reason" ]]; then
|
||||
@@ -139,6 +167,7 @@ batch_uninstall_applications() {
|
||||
((files_cleaned++))
|
||||
((total_items++))
|
||||
success_items+=("$app_name")
|
||||
dock_cleanup_paths+=("$app_path")
|
||||
else
|
||||
((failed_count++))
|
||||
failed_items+=("$app_name:$reason")
|
||||
@@ -148,7 +177,6 @@ batch_uninstall_applications() {
|
||||
# Summary
|
||||
local freed_display=$(bytes_to_human "$((total_size_freed * 1024))")
|
||||
local bar="================================================================================"
|
||||
echo ""
|
||||
echo "$bar"
|
||||
if [[ $success_count -gt 0 ]]; then
|
||||
local success_list="${success_items[*]}"
|
||||
@@ -179,6 +207,10 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
echo "$bar"
|
||||
|
||||
if [[ ${#dock_cleanup_paths[@]} -gt 0 ]]; then
|
||||
remove_apps_from_dock "${dock_cleanup_paths[@]}"
|
||||
fi
|
||||
|
||||
# Clean up sudo keepalive if it was started
|
||||
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
||||
kill "$sudo_keepalive_pid" 2>/dev/null || true
|
||||
|
||||
165
lib/common.sh
165
lib/common.sh
@@ -268,14 +268,38 @@ bytes_to_human() {
|
||||
fi
|
||||
|
||||
if ((bytes >= 1073741824)); then # >= 1GB
|
||||
echo "$bytes" | awk '{printf "%.2fGB", $1/1073741824}'
|
||||
elif ((bytes >= 1048576)); then # >= 1MB
|
||||
echo "$bytes" | awk '{printf "%.1fMB", $1/1048576}'
|
||||
elif ((bytes >= 1024)); then # >= 1KB
|
||||
echo "$bytes" | awk '{printf "%.0fKB", $1/1024}'
|
||||
else
|
||||
echo "${bytes}B"
|
||||
local divisor=1073741824
|
||||
local whole=$((bytes / divisor))
|
||||
local remainder=$((bytes % divisor))
|
||||
local frac=$(( (remainder * 100 + divisor / 2) / divisor )) # Two decimals, rounded
|
||||
if ((frac >= 100)); then
|
||||
frac=0
|
||||
((whole++))
|
||||
fi
|
||||
printf "%d.%02dGB\n" "$whole" "$frac"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ((bytes >= 1048576)); then # >= 1MB
|
||||
local divisor=1048576
|
||||
local whole=$((bytes / divisor))
|
||||
local remainder=$((bytes % divisor))
|
||||
local frac=$(( (remainder * 10 + divisor / 2) / divisor )) # One decimal, rounded
|
||||
if ((frac >= 10)); then
|
||||
frac=0
|
||||
((whole++))
|
||||
fi
|
||||
printf "%d.%01dMB\n" "$whole" "$frac"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ((bytes >= 1024)); then # >= 1KB
|
||||
local rounded_kb=$(((bytes + 512) / 1024)) # Nearest integer KB
|
||||
printf "%dKB\n" "$rounded_kb"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "%dB\n" "$bytes"
|
||||
}
|
||||
|
||||
# Calculate directory size in bytes
|
||||
@@ -726,17 +750,130 @@ mktemp_dir() { local d; d=$(mktemp -d) || return 1; register_temp_dir "$d"; ech
|
||||
# Uninstall helper abstractions
|
||||
# =========================================================================
|
||||
force_kill_app() {
|
||||
# Args: app_name; tries graceful then force kill; returns 0 if stopped, 1 otherwise
|
||||
local app="$1"
|
||||
if pgrep -f "$app" >/dev/null 2>&1; then
|
||||
pkill -f "$app" 2>/dev/null || true
|
||||
# Args: app_name [app_path]; tries graceful then force kill; returns 0 if stopped, 1 otherwise
|
||||
local app_name="$1"
|
||||
local app_path="${2:-}"
|
||||
|
||||
# Use app path for precise matching if provided
|
||||
local match_pattern="$app_name"
|
||||
if [[ -n "$app_path" && -e "$app_path" ]]; then
|
||||
# Use the app bundle path for more precise matching
|
||||
match_pattern="$app_path"
|
||||
fi
|
||||
|
||||
if pgrep -f "$match_pattern" >/dev/null 2>&1; then
|
||||
pkill -f "$match_pattern" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
if pgrep -f "$app" >/dev/null 2>&1; then
|
||||
pkill -9 -f "$app" 2>/dev/null || true
|
||||
if pgrep -f "$match_pattern" >/dev/null 2>&1; then
|
||||
pkill -9 -f "$match_pattern" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
pgrep -f "$app" >/dev/null 2>&1 && return 1 || return 0
|
||||
pgrep -f "$match_pattern" >/dev/null 2>&1 && return 1 || return 0
|
||||
}
|
||||
|
||||
# Remove application icons from the Dock (best effort)
|
||||
remove_apps_from_dock() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local plist="$HOME/Library/Preferences/com.apple.dock.plist"
|
||||
[[ -f "$plist" ]] || return 0
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Execute Python helper to prune dock entries for the given app paths.
|
||||
# Exit status 2 means entries were removed.
|
||||
local target_count=$#
|
||||
|
||||
python3 - "$@" <<'PY'
|
||||
import os
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
plist_path = os.path.expanduser('~/Library/Preferences/com.apple.dock.plist')
|
||||
if not os.path.exists(plist_path):
|
||||
sys.exit(0)
|
||||
|
||||
def normalise(path):
|
||||
if not path:
|
||||
return ''
|
||||
return os.path.normpath(os.path.realpath(path.rstrip('/')))
|
||||
|
||||
targets = {normalise(arg) for arg in sys.argv[1:] if arg}
|
||||
targets = {t for t in targets if t}
|
||||
if not targets:
|
||||
sys.exit(0)
|
||||
|
||||
with open(plist_path, 'rb') as fh:
|
||||
try:
|
||||
data = plistlib.load(fh)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
apps = data.get('persistent-apps')
|
||||
if not isinstance(apps, list):
|
||||
sys.exit(0)
|
||||
|
||||
changed = False
|
||||
filtered = []
|
||||
for item in apps:
|
||||
try:
|
||||
url = item['tile-data']['file-data']['_CFURLString']
|
||||
except (KeyError, TypeError):
|
||||
filtered.append(item)
|
||||
continue
|
||||
|
||||
if not isinstance(url, str):
|
||||
filtered.append(item)
|
||||
continue
|
||||
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
path = urllib.parse.unquote(parsed.path or '')
|
||||
if not path:
|
||||
filtered.append(item)
|
||||
continue
|
||||
|
||||
candidate = normalise(path)
|
||||
if any(candidate == t or candidate.startswith(t + os.sep) for t in targets):
|
||||
changed = True
|
||||
continue
|
||||
|
||||
filtered.append(item)
|
||||
|
||||
if not changed:
|
||||
sys.exit(0)
|
||||
|
||||
data['persistent-apps'] = filtered
|
||||
with open(plist_path, 'wb') as fh:
|
||||
try:
|
||||
plistlib.dump(data, fh, fmt=plistlib.FMT_BINARY)
|
||||
except Exception:
|
||||
plistlib.dump(data, fh)
|
||||
|
||||
# Restart Dock to apply changes (ignore errors)
|
||||
try:
|
||||
subprocess.run(['killall', 'Dock'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(2)
|
||||
PY
|
||||
local python_status=$?
|
||||
if [[ $python_status -eq 2 ]]; then
|
||||
if [[ $target_count -gt 1 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icons from Dock"
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icon from Dock"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return $python_status
|
||||
}
|
||||
|
||||
map_uninstall_reason() {
|
||||
|
||||
@@ -12,6 +12,10 @@ paginated_multi_select() {
|
||||
local title="$1"
|
||||
shift
|
||||
local -a items=("$@")
|
||||
local external_alt_screen=false
|
||||
if [[ "${MOLE_MANAGED_ALT_SCREEN:-}" == "1" || "${MOLE_MANAGED_ALT_SCREEN:-}" == "true" ]]; then
|
||||
external_alt_screen=true
|
||||
fi
|
||||
|
||||
# Validation
|
||||
if [[ ${#items[@]} -eq 0 ]]; then
|
||||
@@ -55,11 +59,14 @@ paginated_multi_select() {
|
||||
else
|
||||
stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true
|
||||
fi
|
||||
leave_alt_screen
|
||||
if [[ "${external_alt_screen:-false}" == false ]]; then
|
||||
leave_alt_screen
|
||||
fi
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
trap - EXIT INT TERM
|
||||
restore_terminal
|
||||
}
|
||||
|
||||
@@ -74,9 +81,13 @@ paginated_multi_select() {
|
||||
|
||||
# Setup terminal - preserve interrupt character
|
||||
stty -echo -icanon intr ^C 2>/dev/null || true
|
||||
enter_alt_screen
|
||||
# Clear screen once on entry to alt screen
|
||||
printf "\033[2J\033[H" >&2
|
||||
if [[ $external_alt_screen == false ]]; then
|
||||
enter_alt_screen
|
||||
# Clear screen once on entry to alt screen
|
||||
printf "\033[2J\033[H" >&2
|
||||
else
|
||||
printf "\033[H" >&2
|
||||
fi
|
||||
hide_cursor
|
||||
|
||||
# Helper functions
|
||||
@@ -84,11 +95,11 @@ paginated_multi_select() {
|
||||
|
||||
render_item() {
|
||||
local idx=$1 is_current=$2
|
||||
local checkbox="☐"
|
||||
[[ ${selected[idx]} == true ]] && checkbox="☑"
|
||||
local checkbox="[ ]"
|
||||
[[ ${selected[idx]} == true ]] && checkbox="[x]"
|
||||
|
||||
if [[ $is_current == true ]]; then
|
||||
printf "\r\033[2K\033[7m▶ %s %s\033[0m\n" "$checkbox" "${items[idx]}" >&2
|
||||
printf "\r\033[2K${BLUE}> %s %s${NC}\n" "$checkbox" "${items[idx]}" >&2
|
||||
else
|
||||
printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2
|
||||
fi
|
||||
@@ -168,7 +179,10 @@ EOF
|
||||
local key=$(read_key)
|
||||
|
||||
case "$key" in
|
||||
"QUIT") cleanup; return 1 ;;
|
||||
"QUIT")
|
||||
cleanup
|
||||
return 1
|
||||
;;
|
||||
"UP")
|
||||
if [[ $cursor_pos -gt 0 ]]; then
|
||||
((cursor_pos--))
|
||||
|
||||
Reference in New Issue
Block a user