mirror of
https://github.com/tw93/Mole.git
synced 2026-02-12 23:15:15 +00:00
Uninstall function detailed upgrade
This commit is contained in:
@@ -108,15 +108,14 @@ scan_applications() {
|
|||||||
find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' '
|
find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' '
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if cache is valid
|
# Check if cache is valid unless explicitly disabled
|
||||||
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
|
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
|
||||||
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
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")
|
local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
# Cache is valid if: age < TTL AND app count matches
|
# Cache is valid if: age < TTL AND app count matches
|
||||||
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
|
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
|
||||||
# Only show cache info in debug mode
|
# Silent - cache hit, no need to show progress
|
||||||
[[ -n "${MOLE_DEBUG:-}" ]] && echo "Using cached app list (${cache_age}s old, $current_app_count apps) ✓" >&2
|
|
||||||
echo "$cache_file"
|
echo "$cache_file"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -124,7 +123,6 @@ scan_applications() {
|
|||||||
|
|
||||||
local temp_file=$(create_temp_file)
|
local temp_file=$(create_temp_file)
|
||||||
|
|
||||||
echo "" >&2 # Add space before scanning output without breaking stdout return
|
|
||||||
# Pre-cache current epoch to avoid repeated calls
|
# Pre-cache current epoch to avoid repeated calls
|
||||||
local current_epoch=$(date "+%s")
|
local current_epoch=$(date "+%s")
|
||||||
|
|
||||||
@@ -219,6 +217,11 @@ scan_applications() {
|
|||||||
local total_apps=${#app_data_tuples[@]}
|
local total_apps=${#app_data_tuples[@]}
|
||||||
local max_parallel=10 # Process 10 apps in parallel
|
local max_parallel=10 # Process 10 apps in parallel
|
||||||
local pids=()
|
local pids=()
|
||||||
|
local inline_loading=false
|
||||||
|
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() {
|
||||||
@@ -296,7 +299,11 @@ scan_applications() {
|
|||||||
|
|
||||||
# Update progress with spinner
|
# Update progress with spinner
|
||||||
local spinner_char="${spinner_chars:$((spinner_idx % 4)):1}"
|
local spinner_char="${spinner_chars:$((spinner_idx % 4)):1}"
|
||||||
echo -ne "\r\033[K ${spinner_char} Scanning applications... $app_count/$total_apps" >&2
|
if [[ $inline_loading == true ]]; then
|
||||||
|
printf "\033[H\033[2K${spinner_char} Scanning applications... %d/%d" "$app_count" "$total_apps" >&2
|
||||||
|
else
|
||||||
|
echo -ne "\r\033[K${spinner_char} Scanning applications... $app_count/$total_apps" >&2
|
||||||
|
fi
|
||||||
((spinner_idx++))
|
((spinner_idx++))
|
||||||
|
|
||||||
# Wait if we've hit max parallel limit
|
# Wait if we've hit max parallel limit
|
||||||
@@ -311,15 +318,22 @@ scan_applications() {
|
|||||||
wait "$pid" 2>/dev/null
|
wait "$pid" 2>/dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -e "\r\033[K ✓ Found $app_count applications" >&2
|
|
||||||
echo "" >&2
|
|
||||||
|
|
||||||
# Check if we found any applications
|
# Check if we found any applications
|
||||||
if [[ ! -s "$temp_file" ]]; then
|
if [[ ! -s "$temp_file" ]]; then
|
||||||
|
if [[ $inline_loading == true ]]; then
|
||||||
|
printf "\033[H\033[2K" >&2
|
||||||
|
else
|
||||||
|
echo -ne "\r\033[K" >&2
|
||||||
|
fi
|
||||||
|
echo "No applications found to uninstall" >&2
|
||||||
rm -f "$temp_file"
|
rm -f "$temp_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $inline_loading == true ]]; then
|
||||||
|
printf "\033[H\033[2K" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# Sort by last used (oldest first) and cache the result
|
# Sort by last used (oldest first) and cache the result
|
||||||
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file"; return 1; }
|
sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file"; return 1; }
|
||||||
rm -f "$temp_file"
|
rm -f "$temp_file"
|
||||||
@@ -388,13 +402,13 @@ uninstall_applications() {
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if app is running
|
# Check if app is running (use app path for precise matching)
|
||||||
if pgrep -f "$app_name" >/dev/null 2>&1; then
|
if pgrep -f "$app_path" >/dev/null 2>&1; then
|
||||||
echo -e "${YELLOW}⚠ $app_name is currently running${NC}"
|
echo -e "${YELLOW}⚠ $app_name is currently running${NC}"
|
||||||
read -p " Force quit $app_name? (y/N): " -n 1 -r
|
read -p " Force quit $app_name? (y/N): " -n 1 -r
|
||||||
echo
|
echo
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
pkill -f "$app_name" 2>/dev/null || true
|
pkill -f "$app_path" 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
else
|
else
|
||||||
echo -e " ${BLUE}○${NC} Skipped $app_name"
|
echo -e " ${BLUE}○${NC} Skipped $app_name"
|
||||||
@@ -509,6 +523,10 @@ uninstall_applications() {
|
|||||||
# Cleanup function - restore cursor and clean up
|
# Cleanup function - restore cursor and clean up
|
||||||
cleanup() {
|
cleanup() {
|
||||||
# Restore cursor using common function
|
# Restore cursor using common function
|
||||||
|
if [[ "${MOLE_ALT_SCREEN_ACTIVE:-}" == "1" ]]; then
|
||||||
|
leave_alt_screen
|
||||||
|
unset MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
fi
|
||||||
show_cursor
|
show_cursor
|
||||||
exit "${1:-0}"
|
exit "${1:-0}"
|
||||||
}
|
}
|
||||||
@@ -518,30 +536,78 @@ trap cleanup EXIT INT TERM
|
|||||||
|
|
||||||
# Main function
|
# Main function
|
||||||
main() {
|
main() {
|
||||||
|
local use_inline_loading=false
|
||||||
|
if [[ -t 1 && -t 2 ]]; then
|
||||||
|
use_inline_loading=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Hide cursor during operation
|
# Hide cursor during operation
|
||||||
hide_cursor
|
hide_cursor
|
||||||
|
|
||||||
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
enter_alt_screen
|
||||||
|
export MOLE_ALT_SCREEN_ACTIVE=1
|
||||||
|
export MOLE_INLINE_LOADING=1
|
||||||
|
export MOLE_MANAGED_ALT_SCREEN=1
|
||||||
|
printf "\033[2J\033[H" >&2
|
||||||
|
else
|
||||||
|
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
fi
|
||||||
|
|
||||||
# Scan applications
|
# Scan applications
|
||||||
local apps_file=$(scan_applications)
|
local apps_file=""
|
||||||
|
if ! apps_file=$(scan_applications); then
|
||||||
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
printf "\033[2J\033[H" >&2
|
||||||
|
leave_alt_screen
|
||||||
|
unset MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
printf "\033[2J\033[H" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$apps_file" ]]; then
|
if [[ ! -f "$apps_file" ]]; then
|
||||||
echo ""
|
# Error message already shown by scan_applications
|
||||||
log_error "Failed to scan applications"
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
leave_alt_screen
|
||||||
|
unset MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
|
||||||
|
fi
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load applications
|
# Load applications
|
||||||
if ! load_applications "$apps_file"; then
|
if ! load_applications "$apps_file"; then
|
||||||
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
leave_alt_screen
|
||||||
|
unset MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
|
||||||
|
fi
|
||||||
rm -f "$apps_file"
|
rm -f "$apps_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Interactive selection using paginated menu
|
# Interactive selection using paginated menu
|
||||||
if ! select_apps_for_uninstall; then
|
if ! select_apps_for_uninstall; then
|
||||||
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
leave_alt_screen
|
||||||
|
unset MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
|
||||||
|
fi
|
||||||
rm -f "$apps_file"
|
rm -f "$apps_file"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $use_inline_loading == true ]]; then
|
||||||
|
leave_alt_screen
|
||||||
|
unset MOLE_ALT_SCREEN_ACTIVE
|
||||||
|
unset MOLE_INLINE_LOADING MOLE_MANAGED_ALT_SCREEN
|
||||||
|
fi
|
||||||
|
|
||||||
# Restore cursor and show a concise summary before confirmation
|
# Restore cursor and show a concise summary before confirmation
|
||||||
show_cursor
|
show_cursor
|
||||||
clear
|
clear
|
||||||
|
|||||||
@@ -37,10 +37,8 @@ select_apps_for_uninstall() {
|
|||||||
menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")")
|
menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Clear screen before menu (alternate screen preserves main screen)
|
|
||||||
clear_screen
|
|
||||||
|
|
||||||
# Use paginated menu - result will be stored in MOLE_SELECTION_RESULT
|
# Use paginated menu - result will be stored in MOLE_SELECTION_RESULT
|
||||||
|
# Note: paginated_multi_select enters alternate screen and handles clearing
|
||||||
MOLE_SELECTION_RESULT=""
|
MOLE_SELECTION_RESULT=""
|
||||||
paginated_multi_select "Select Apps to Remove" "${menu_options[@]}"
|
paginated_multi_select "Select Apps to Remove" "${menu_options[@]}"
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ batch_uninstall_applications() {
|
|||||||
local -a sudo_apps=()
|
local -a sudo_apps=()
|
||||||
local total_estimated_size=0
|
local total_estimated_size=0
|
||||||
local -a app_details=()
|
local -a app_details=()
|
||||||
|
local -a dock_cleanup_paths=()
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
# Silent analysis without spinner output (avoid visual flicker)
|
# Silent analysis without spinner output (avoid visual flicker)
|
||||||
@@ -31,8 +32,8 @@ batch_uninstall_applications() {
|
|||||||
[[ -z "$selected_app" ]] && continue
|
[[ -z "$selected_app" ]] && continue
|
||||||
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"
|
||||||
|
|
||||||
# Check if app is running
|
# Check if app is running (use app path for precise matching)
|
||||||
if pgrep -f "$app_name" >/dev/null 2>&1; then
|
if pgrep -f "$app_path" >/dev/null 2>&1; then
|
||||||
running_apps+=("$app_name")
|
running_apps+=("$app_name")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -49,14 +50,46 @@ batch_uninstall_applications() {
|
|||||||
((total_estimated_size += total_kb))
|
((total_estimated_size += total_kb))
|
||||||
|
|
||||||
# Store details for later use
|
# Store details for later use
|
||||||
# Base64 encode related_files to handle multi-line data safely
|
# Base64 encode related_files to handle multi-line data safely (single line)
|
||||||
local encoded_files=$(echo "$related_files" | base64)
|
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")
|
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Format size display (convert KB to bytes for bytes_to_human())
|
# Format size display (convert KB to bytes for bytes_to_human())
|
||||||
local size_display=$(bytes_to_human "$((total_estimated_size * 1024))")
|
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)
|
# Show summary and get batch confirmation first (before asking for password)
|
||||||
local app_total=${#selected_apps[@]}
|
local app_total=${#selected_apps[@]}
|
||||||
local app_text="app"
|
local app_text="app"
|
||||||
@@ -100,12 +133,7 @@ batch_uninstall_applications() {
|
|||||||
if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi
|
if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi
|
||||||
|
|
||||||
# Force quit running apps first (batch)
|
# Force quit running apps first (batch)
|
||||||
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
# Note: Apps are already killed in the individual uninstall loop below with app_path for precise matching
|
||||||
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
|
|
||||||
|
|
||||||
# Perform uninstallations (silent mode, show results at end)
|
# Perform uninstallations (silent mode, show results at end)
|
||||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||||
@@ -114,11 +142,11 @@ batch_uninstall_applications() {
|
|||||||
local -a success_items=()
|
local -a success_items=()
|
||||||
for detail in "${app_details[@]}"; do
|
for detail in "${app_details[@]}"; do
|
||||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files <<< "$detail"
|
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 reason=""
|
||||||
local needs_sudo=false
|
local needs_sudo=false
|
||||||
[[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]] && needs_sudo=true
|
[[ ! -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"
|
reason="still running"
|
||||||
fi
|
fi
|
||||||
if [[ -z "$reason" ]]; then
|
if [[ -z "$reason" ]]; then
|
||||||
@@ -139,6 +167,7 @@ batch_uninstall_applications() {
|
|||||||
((files_cleaned++))
|
((files_cleaned++))
|
||||||
((total_items++))
|
((total_items++))
|
||||||
success_items+=("$app_name")
|
success_items+=("$app_name")
|
||||||
|
dock_cleanup_paths+=("$app_path")
|
||||||
else
|
else
|
||||||
((failed_count++))
|
((failed_count++))
|
||||||
failed_items+=("$app_name:$reason")
|
failed_items+=("$app_name:$reason")
|
||||||
@@ -148,7 +177,6 @@ batch_uninstall_applications() {
|
|||||||
# Summary
|
# Summary
|
||||||
local freed_display=$(bytes_to_human "$((total_size_freed * 1024))")
|
local freed_display=$(bytes_to_human "$((total_size_freed * 1024))")
|
||||||
local bar="================================================================================"
|
local bar="================================================================================"
|
||||||
echo ""
|
|
||||||
echo "$bar"
|
echo "$bar"
|
||||||
if [[ $success_count -gt 0 ]]; then
|
if [[ $success_count -gt 0 ]]; then
|
||||||
local success_list="${success_items[*]}"
|
local success_list="${success_items[*]}"
|
||||||
@@ -179,6 +207,10 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
echo "$bar"
|
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
|
# Clean up sudo keepalive if it was started
|
||||||
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
||||||
kill "$sudo_keepalive_pid" 2>/dev/null || true
|
kill "$sudo_keepalive_pid" 2>/dev/null || true
|
||||||
|
|||||||
165
lib/common.sh
165
lib/common.sh
@@ -268,14 +268,38 @@ bytes_to_human() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ((bytes >= 1073741824)); then # >= 1GB
|
if ((bytes >= 1073741824)); then # >= 1GB
|
||||||
echo "$bytes" | awk '{printf "%.2fGB", $1/1073741824}'
|
local divisor=1073741824
|
||||||
elif ((bytes >= 1048576)); then # >= 1MB
|
local whole=$((bytes / divisor))
|
||||||
echo "$bytes" | awk '{printf "%.1fMB", $1/1048576}'
|
local remainder=$((bytes % divisor))
|
||||||
elif ((bytes >= 1024)); then # >= 1KB
|
local frac=$(( (remainder * 100 + divisor / 2) / divisor )) # Two decimals, rounded
|
||||||
echo "$bytes" | awk '{printf "%.0fKB", $1/1024}'
|
if ((frac >= 100)); then
|
||||||
else
|
frac=0
|
||||||
echo "${bytes}B"
|
((whole++))
|
||||||
|
fi
|
||||||
|
printf "%d.%02dGB\n" "$whole" "$frac"
|
||||||
|
return 0
|
||||||
fi
|
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
|
# 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
|
# Uninstall helper abstractions
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
force_kill_app() {
|
force_kill_app() {
|
||||||
# Args: app_name; tries graceful then force kill; returns 0 if stopped, 1 otherwise
|
# Args: app_name [app_path]; tries graceful then force kill; returns 0 if stopped, 1 otherwise
|
||||||
local app="$1"
|
local app_name="$1"
|
||||||
if pgrep -f "$app" >/dev/null 2>&1; then
|
local app_path="${2:-}"
|
||||||
pkill -f "$app" 2>/dev/null || true
|
|
||||||
|
# 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
|
sleep 1
|
||||||
fi
|
fi
|
||||||
if pgrep -f "$app" >/dev/null 2>&1; then
|
if pgrep -f "$match_pattern" >/dev/null 2>&1; then
|
||||||
pkill -9 -f "$app" 2>/dev/null || true
|
pkill -9 -f "$match_pattern" 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
fi
|
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() {
|
map_uninstall_reason() {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ paginated_multi_select() {
|
|||||||
local title="$1"
|
local title="$1"
|
||||||
shift
|
shift
|
||||||
local -a items=("$@")
|
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
|
# Validation
|
||||||
if [[ ${#items[@]} -eq 0 ]]; then
|
if [[ ${#items[@]} -eq 0 ]]; then
|
||||||
@@ -55,11 +59,14 @@ paginated_multi_select() {
|
|||||||
else
|
else
|
||||||
stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true
|
stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
leave_alt_screen
|
if [[ "${external_alt_screen:-false}" == false ]]; then
|
||||||
|
leave_alt_screen
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cleanup function
|
# Cleanup function
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
trap - EXIT INT TERM
|
||||||
restore_terminal
|
restore_terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +81,13 @@ paginated_multi_select() {
|
|||||||
|
|
||||||
# Setup terminal - preserve interrupt character
|
# Setup terminal - preserve interrupt character
|
||||||
stty -echo -icanon intr ^C 2>/dev/null || true
|
stty -echo -icanon intr ^C 2>/dev/null || true
|
||||||
enter_alt_screen
|
if [[ $external_alt_screen == false ]]; then
|
||||||
# Clear screen once on entry to alt screen
|
enter_alt_screen
|
||||||
printf "\033[2J\033[H" >&2
|
# Clear screen once on entry to alt screen
|
||||||
|
printf "\033[2J\033[H" >&2
|
||||||
|
else
|
||||||
|
printf "\033[H" >&2
|
||||||
|
fi
|
||||||
hide_cursor
|
hide_cursor
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
@@ -84,11 +95,11 @@ paginated_multi_select() {
|
|||||||
|
|
||||||
render_item() {
|
render_item() {
|
||||||
local idx=$1 is_current=$2
|
local idx=$1 is_current=$2
|
||||||
local checkbox="☐"
|
local checkbox="[ ]"
|
||||||
[[ ${selected[idx]} == true ]] && checkbox="☑"
|
[[ ${selected[idx]} == true ]] && checkbox="[x]"
|
||||||
|
|
||||||
if [[ $is_current == true ]]; then
|
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
|
else
|
||||||
printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2
|
printf "\r\033[2K %s %s\n" "$checkbox" "${items[idx]}" >&2
|
||||||
fi
|
fi
|
||||||
@@ -168,7 +179,10 @@ EOF
|
|||||||
local key=$(read_key)
|
local key=$(read_key)
|
||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
"QUIT") cleanup; return 1 ;;
|
"QUIT")
|
||||||
|
cleanup
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
"UP")
|
"UP")
|
||||||
if [[ $cursor_pos -gt 0 ]]; then
|
if [[ $cursor_pos -gt 0 ]]; then
|
||||||
((cursor_pos--))
|
((cursor_pos--))
|
||||||
|
|||||||
69
mole
69
mole
@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/lib/common.sh"
|
source "$SCRIPT_DIR/lib/common.sh"
|
||||||
|
|
||||||
# Version info
|
# Version info
|
||||||
VERSION="1.7.3"
|
VERSION="1.7.4"
|
||||||
MOLE_TAGLINE="can dig deep to clean your Mac."
|
MOLE_TAGLINE="can dig deep to clean your Mac."
|
||||||
|
|
||||||
# Get latest version from remote repository
|
# Get latest version from remote repository
|
||||||
@@ -400,28 +400,53 @@ remove_mole() {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Display main menu options
|
# Display main menu options with minimal refresh to avoid flicker
|
||||||
show_main_menu() {
|
show_main_menu() {
|
||||||
local selected="${1:-1}"
|
local selected="${1:-1}"
|
||||||
local full_draw="${2:-true}"
|
local _full_draw="${2:-true}" # Kept for compatibility (unused)
|
||||||
|
local banner="${MAIN_MENU_BANNER:-}"
|
||||||
|
local update_message="${MAIN_MENU_UPDATE_MESSAGE:-}"
|
||||||
|
|
||||||
# Full redraw each time (prevents ghost menu items)
|
# Fallback if globals missing (should not happen)
|
||||||
clear_screen
|
if [[ -z "$banner" ]]; then
|
||||||
echo ""
|
banner="$(show_brand_banner)"
|
||||||
show_brand_banner
|
MAIN_MENU_BANNER="$banner"
|
||||||
show_update_notification
|
fi
|
||||||
echo ""
|
|
||||||
|
|
||||||
show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)"
|
printf '\033[H' # Move cursor to home
|
||||||
show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)"
|
|
||||||
show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)"
|
local line=""
|
||||||
show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)"
|
# Leading spacer
|
||||||
show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)"
|
printf '\r\033[2K\n'
|
||||||
|
|
||||||
|
# Brand banner
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
printf '\r\033[2K%s\n' "$line"
|
||||||
|
done <<< "$banner"
|
||||||
|
|
||||||
|
# Update notification block (if present)
|
||||||
|
if [[ -n "$update_message" ]]; then
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
printf '\r\033[2K%s\n' "$line"
|
||||||
|
done <<< "$update_message"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Spacer before menu options
|
||||||
|
printf '\r\033[2K\n'
|
||||||
|
|
||||||
|
printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)")"
|
||||||
|
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
|
||||||
|
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
|
||||||
|
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
|
||||||
|
printf '\r\033[2K%s\n' "$(show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)")"
|
||||||
|
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
echo ""
|
printf '\r\033[2K\n'
|
||||||
echo -e " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit"
|
printf '\r\033[2K%s\n' " ${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Enter${NC} Select ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Clear any remaining content below without full screen wipe
|
||||||
|
printf '\033[J'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Interactive main menu loop
|
# Interactive main menu loop
|
||||||
@@ -439,6 +464,18 @@ interactive_main_menu() {
|
|||||||
fi
|
fi
|
||||||
local current_option=1
|
local current_option=1
|
||||||
local first_draw=true
|
local first_draw=true
|
||||||
|
local brand_banner=""
|
||||||
|
local msg_cache="$HOME/.cache/mole/update_message"
|
||||||
|
local update_message=""
|
||||||
|
|
||||||
|
brand_banner="$(show_brand_banner)"
|
||||||
|
MAIN_MENU_BANNER="$brand_banner"
|
||||||
|
|
||||||
|
if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then
|
||||||
|
update_message="$(cat "$msg_cache" 2>/dev/null || echo "")"
|
||||||
|
fi
|
||||||
|
MAIN_MENU_UPDATE_MESSAGE="$update_message"
|
||||||
|
|
||||||
cleanup_and_exit() {
|
cleanup_and_exit() {
|
||||||
show_cursor
|
show_cursor
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user