mac_cleaner 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +52 -0
- data/exe/mac_cleaner +5 -0
- data/lib/common.sh +1751 -0
- data/lib/mac_cleaner/analyzer.rb +156 -0
- data/lib/mac_cleaner/cleaner.rb +497 -0
- data/lib/mac_cleaner/cli.rb +22 -0
- data/lib/mac_cleaner/version.rb +3 -0
- data/lib/mac_cleaner.rb +8 -0
- data/lib/paginated_menu.sh +688 -0
- data/lib/simple_menu.sh +292 -0
- data/lib/whitelist_manager.sh +289 -0
- metadata +72 -4
data/lib/common.sh
ADDED
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Mole - Common Functions Library
|
|
3
|
+
# Shared utilities and functions for all modules
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Prevent multiple sourcing
|
|
8
|
+
if [[ -n "${MOLE_COMMON_LOADED:-}" ]]; then
|
|
9
|
+
return 0
|
|
10
|
+
fi
|
|
11
|
+
readonly MOLE_COMMON_LOADED=1
|
|
12
|
+
|
|
13
|
+
# Color definitions (readonly for safety)
|
|
14
|
+
readonly ESC=$'\033'
|
|
15
|
+
readonly GREEN="${ESC}[0;32m"
|
|
16
|
+
readonly BLUE="${ESC}[0;34m"
|
|
17
|
+
readonly YELLOW="${ESC}[1;33m"
|
|
18
|
+
readonly PURPLE="${ESC}[0;35m"
|
|
19
|
+
readonly RED="${ESC}[0;31m"
|
|
20
|
+
readonly GRAY="${ESC}[0;90m"
|
|
21
|
+
readonly NC="${ESC}[0m"
|
|
22
|
+
|
|
23
|
+
# Icon definitions (shared across modules)
|
|
24
|
+
readonly ICON_CONFIRM="◎" # Confirm operation / spinner text
|
|
25
|
+
readonly ICON_ADMIN="⚙" # Gear indicator for admin/settings/system info
|
|
26
|
+
readonly ICON_SUCCESS="✓" # Success mark
|
|
27
|
+
readonly ICON_ERROR="☻" # Error / warning mark
|
|
28
|
+
readonly ICON_EMPTY="○" # Hollow circle (empty state / unchecked)
|
|
29
|
+
readonly ICON_SOLID="●" # Solid circle (selected / system marker)
|
|
30
|
+
readonly ICON_LIST="•" # Basic list bullet
|
|
31
|
+
readonly ICON_ARROW="➤" # Pointer / prompt indicator
|
|
32
|
+
readonly ICON_WARNING="☻" # Warning marker (shares glyph with error)
|
|
33
|
+
readonly ICON_NAV_UP="↑" # Navigation up
|
|
34
|
+
readonly ICON_NAV_DOWN="↓" # Navigation down
|
|
35
|
+
readonly ICON_NAV_LEFT="←" # Navigation left
|
|
36
|
+
readonly ICON_NAV_RIGHT="→" # Navigation right
|
|
37
|
+
|
|
38
|
+
# Spinner character helpers (ASCII by default, overridable via env)
|
|
39
|
+
mo_spinner_chars() {
|
|
40
|
+
local chars="${MO_SPINNER_CHARS:-|/-\\}"
|
|
41
|
+
[[ -z "$chars" ]] && chars='|/-\\'
|
|
42
|
+
printf "%s" "$chars"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Logging configuration
|
|
46
|
+
readonly LOG_FILE="${HOME}/.config/mole/mole.log"
|
|
47
|
+
readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB
|
|
48
|
+
|
|
49
|
+
# Ensure log directory exists
|
|
50
|
+
mkdir -p "$(dirname "$LOG_FILE")" 2> /dev/null || true
|
|
51
|
+
|
|
52
|
+
# Log file maintenance (must be defined before logging functions)
|
|
53
|
+
rotate_log() {
|
|
54
|
+
local max_size="${MOLE_MAX_LOG_SIZE:-$LOG_MAX_SIZE_DEFAULT}"
|
|
55
|
+
if [[ -f "$LOG_FILE" ]] && [[ $(stat -f%z "$LOG_FILE" 2> /dev/null || echo 0) -gt "$max_size" ]]; then
|
|
56
|
+
mv "$LOG_FILE" "${LOG_FILE}.old" 2> /dev/null || true
|
|
57
|
+
touch "$LOG_FILE" 2> /dev/null || true
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Enhanced logging functions with file logging support
|
|
62
|
+
log_info() {
|
|
63
|
+
rotate_log
|
|
64
|
+
echo -e "${BLUE}$1${NC}"
|
|
65
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE" 2> /dev/null || true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log_success() {
|
|
69
|
+
rotate_log
|
|
70
|
+
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"
|
|
71
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2> /dev/null || true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log_warning() {
|
|
75
|
+
rotate_log
|
|
76
|
+
echo -e "${YELLOW}$1${NC}"
|
|
77
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE" 2> /dev/null || true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log_error() {
|
|
81
|
+
rotate_log
|
|
82
|
+
echo -e "${RED}${ICON_ERROR}${NC} $1" >&2
|
|
83
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
log_header() {
|
|
87
|
+
rotate_log
|
|
88
|
+
echo -e "\n${PURPLE}${ICON_ARROW} $1${NC}"
|
|
89
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SECTION: $1" >> "$LOG_FILE" 2> /dev/null || true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Icon output helpers
|
|
93
|
+
icon_confirm() {
|
|
94
|
+
echo -e "${BLUE}${ICON_CONFIRM}${NC} $1"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
icon_admin() {
|
|
98
|
+
echo -e "${BLUE}${ICON_ADMIN}${NC} $1"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
icon_success() {
|
|
102
|
+
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
icon_error() {
|
|
106
|
+
echo -e " ${RED}${ICON_ERROR}${NC} $1"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
icon_empty() {
|
|
110
|
+
echo -e " ${BLUE}${ICON_EMPTY}${NC} $1"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
icon_list() {
|
|
114
|
+
echo -e " ${ICON_LIST} $1"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
icon_menu() {
|
|
118
|
+
local num="$1"
|
|
119
|
+
local text="$2"
|
|
120
|
+
echo -e "${BLUE}${ICON_ARROW} ${num}. ${text}${NC}"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Consistent summary blocks for command results
|
|
124
|
+
print_summary_block() {
|
|
125
|
+
local heading=""
|
|
126
|
+
|
|
127
|
+
if [[ $# -gt 0 ]]; then
|
|
128
|
+
shift
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if [[ $# -gt 0 ]]; then
|
|
132
|
+
heading="$1"
|
|
133
|
+
shift
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
local -a details=("$@")
|
|
137
|
+
local divider="======================================================================"
|
|
138
|
+
|
|
139
|
+
echo "$divider"
|
|
140
|
+
if [[ -n "$heading" ]]; then
|
|
141
|
+
echo -e "${BLUE}${heading}${NC}"
|
|
142
|
+
fi
|
|
143
|
+
for detail in "${details[@]}"; do
|
|
144
|
+
[[ -z "$detail" ]] && continue
|
|
145
|
+
echo -e "${detail}"
|
|
146
|
+
done
|
|
147
|
+
echo "$divider"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# System detection
|
|
151
|
+
detect_architecture() {
|
|
152
|
+
if [[ "$(uname -m)" == "arm64" ]]; then
|
|
153
|
+
echo "Apple Silicon"
|
|
154
|
+
else
|
|
155
|
+
echo "Intel"
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get_free_space() {
|
|
160
|
+
df -h / | awk 'NR==2 {print $4}'
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Common UI functions
|
|
164
|
+
clear_screen() {
|
|
165
|
+
printf '\033[2J\033[H'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
hide_cursor() {
|
|
169
|
+
printf '\033[?25l'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
show_cursor() {
|
|
173
|
+
printf '\033[?25h'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Keyboard input handling (simple and robust)
|
|
177
|
+
read_key() {
|
|
178
|
+
local key rest read_status
|
|
179
|
+
|
|
180
|
+
# Read with explicit status check
|
|
181
|
+
IFS= read -r -s -n 1 key
|
|
182
|
+
read_status=$?
|
|
183
|
+
|
|
184
|
+
# Handle read failure (Ctrl+D, EOF, etc.) - treat as quit
|
|
185
|
+
if [[ $read_status -ne 0 ]]; then
|
|
186
|
+
echo "QUIT"
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# Raw typing mode (filter): map most keys to CHAR:<key>
|
|
191
|
+
if [[ "${MOLE_READ_KEY_FORCE_CHAR:-}" == "1" ]]; then
|
|
192
|
+
# Some terminals return empty on Enter with -n1
|
|
193
|
+
if [[ -z "$key" ]]; then
|
|
194
|
+
echo "ENTER"
|
|
195
|
+
return 0
|
|
196
|
+
fi
|
|
197
|
+
case "$key" in
|
|
198
|
+
$'\n' | $'\r') echo "ENTER" ;;
|
|
199
|
+
$'\x7f' | $'\x08') echo "DELETE" ;;
|
|
200
|
+
$'\x1b') echo "QUIT" ;; # ESC cancels filter
|
|
201
|
+
*)
|
|
202
|
+
case "$key" in
|
|
203
|
+
[[:print:]]) echo "CHAR:$key" ;;
|
|
204
|
+
*) echo "OTHER" ;;
|
|
205
|
+
esac
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# Some terminals can yield empty on Enter with -n1; treat as ENTER
|
|
212
|
+
if [[ -z "$key" ]]; then
|
|
213
|
+
echo "ENTER"
|
|
214
|
+
return 0
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
case "$key" in
|
|
218
|
+
$'\n' | $'\r') echo "ENTER" ;;
|
|
219
|
+
' ') echo "SPACE" ;;
|
|
220
|
+
'Q') echo "QUIT" ;;
|
|
221
|
+
'R') echo "RETRY" ;;
|
|
222
|
+
'o' | 'O') echo "OPEN" ;;
|
|
223
|
+
'/') echo "FILTER" ;; # Trigger filter mode
|
|
224
|
+
$'\x03') echo "QUIT" ;; # Ctrl+C
|
|
225
|
+
$'\x7f' | $'\x08') echo "DELETE" ;; # Backspace/Delete key
|
|
226
|
+
$'\x1b')
|
|
227
|
+
# ESC sequence - could be arrow key, delete key, or ESC alone
|
|
228
|
+
# Read the next two bytes within 1s
|
|
229
|
+
if IFS= read -r -s -n 1 -t 1 rest 2> /dev/null; then
|
|
230
|
+
if [[ "$rest" == "[" ]]; then
|
|
231
|
+
# Got ESC [, read next character
|
|
232
|
+
if IFS= read -r -s -n 1 -t 1 rest2 2> /dev/null; then
|
|
233
|
+
case "$rest2" in
|
|
234
|
+
"A") echo "UP" ;;
|
|
235
|
+
"B") echo "DOWN" ;;
|
|
236
|
+
"C") echo "RIGHT" ;;
|
|
237
|
+
"D") echo "LEFT" ;;
|
|
238
|
+
"3")
|
|
239
|
+
# Delete key (Fn+Delete): ESC [ 3 ~
|
|
240
|
+
IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null
|
|
241
|
+
if [[ "$rest3" == "~" ]]; then
|
|
242
|
+
echo "DELETE"
|
|
243
|
+
else
|
|
244
|
+
echo "OTHER"
|
|
245
|
+
fi
|
|
246
|
+
;;
|
|
247
|
+
"5")
|
|
248
|
+
# Page Up key: ESC [ 5 ~
|
|
249
|
+
IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null
|
|
250
|
+
[[ "$rest3" == "~" ]] && echo "OTHER" || echo "OTHER"
|
|
251
|
+
;;
|
|
252
|
+
"6")
|
|
253
|
+
# Page Down key: ESC [ 6 ~
|
|
254
|
+
IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null
|
|
255
|
+
[[ "$rest3" == "~" ]] && echo "OTHER" || echo "OTHER"
|
|
256
|
+
;;
|
|
257
|
+
*) echo "OTHER" ;;
|
|
258
|
+
esac
|
|
259
|
+
else
|
|
260
|
+
echo "QUIT" # ESC [ timeout
|
|
261
|
+
fi
|
|
262
|
+
else
|
|
263
|
+
echo "QUIT" # ESC + something else
|
|
264
|
+
fi
|
|
265
|
+
else
|
|
266
|
+
# ESC pressed alone - treat as quit
|
|
267
|
+
echo "QUIT"
|
|
268
|
+
fi
|
|
269
|
+
;;
|
|
270
|
+
*)
|
|
271
|
+
# Printable ASCII -> expose as CHAR:<key> (for live filtering)
|
|
272
|
+
case "$key" in
|
|
273
|
+
[[:print:]]) echo "CHAR:$key" ;;
|
|
274
|
+
*) echo "OTHER" ;;
|
|
275
|
+
esac
|
|
276
|
+
;;
|
|
277
|
+
esac
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Drain pending input (useful for scrolling prevention)
|
|
281
|
+
drain_pending_input() {
|
|
282
|
+
local drained=0
|
|
283
|
+
# Single pass with reasonable timeout
|
|
284
|
+
# Touchpad scrolling can generate bursts of arrow keys
|
|
285
|
+
while IFS= read -r -s -n 1 -t 0.001 _ 2> /dev/null; do
|
|
286
|
+
((drained++))
|
|
287
|
+
# Safety limit to prevent infinite loop
|
|
288
|
+
[[ $drained -gt 500 ]] && break
|
|
289
|
+
done
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Menu display helper
|
|
293
|
+
show_menu_option() {
|
|
294
|
+
local number="$1"
|
|
295
|
+
local text="$2"
|
|
296
|
+
local selected="$3"
|
|
297
|
+
|
|
298
|
+
if [[ "$selected" == "true" ]]; then
|
|
299
|
+
echo -e "${BLUE}${ICON_ARROW} $number. $text${NC}"
|
|
300
|
+
else
|
|
301
|
+
echo " $number. $text"
|
|
302
|
+
fi
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Error handling
|
|
306
|
+
handle_error() {
|
|
307
|
+
local message="$1"
|
|
308
|
+
local exit_code="${2:-1}"
|
|
309
|
+
|
|
310
|
+
log_error "$message"
|
|
311
|
+
exit "$exit_code"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# File size utilities
|
|
315
|
+
get_human_size() {
|
|
316
|
+
local path="$1"
|
|
317
|
+
if [[ ! -e "$path" ]]; then
|
|
318
|
+
echo "N/A"
|
|
319
|
+
return 1
|
|
320
|
+
fi
|
|
321
|
+
du -sh "$path" 2> /dev/null | cut -f1 || echo "N/A"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Convert bytes to human readable format
|
|
325
|
+
bytes_to_human() {
|
|
326
|
+
local bytes="$1"
|
|
327
|
+
if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then
|
|
328
|
+
echo "0B"
|
|
329
|
+
return 1
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
if ((bytes >= 1073741824)); then # >= 1GB
|
|
333
|
+
local divisor=1073741824
|
|
334
|
+
local whole=$((bytes / divisor))
|
|
335
|
+
local remainder=$((bytes % divisor))
|
|
336
|
+
local frac=$(((remainder * 100 + divisor / 2) / divisor)) # Two decimals, rounded
|
|
337
|
+
if ((frac >= 100)); then
|
|
338
|
+
frac=0
|
|
339
|
+
((whole++))
|
|
340
|
+
fi
|
|
341
|
+
printf "%d.%02dGB\n" "$whole" "$frac"
|
|
342
|
+
return 0
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
if ((bytes >= 1048576)); then # >= 1MB
|
|
346
|
+
local divisor=1048576
|
|
347
|
+
local whole=$((bytes / divisor))
|
|
348
|
+
local remainder=$((bytes % divisor))
|
|
349
|
+
local frac=$(((remainder * 10 + divisor / 2) / divisor)) # One decimal, rounded
|
|
350
|
+
if ((frac >= 10)); then
|
|
351
|
+
frac=0
|
|
352
|
+
((whole++))
|
|
353
|
+
fi
|
|
354
|
+
printf "%d.%01dMB\n" "$whole" "$frac"
|
|
355
|
+
return 0
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
if ((bytes >= 1024)); then # >= 1KB
|
|
359
|
+
local rounded_kb=$(((bytes + 512) / 1024)) # Nearest integer KB
|
|
360
|
+
printf "%dKB\n" "$rounded_kb"
|
|
361
|
+
return 0
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
printf "%dB\n" "$bytes"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# Calculate directory size in bytes
|
|
368
|
+
get_directory_size_bytes() {
|
|
369
|
+
local path="$1"
|
|
370
|
+
if [[ ! -d "$path" ]]; then
|
|
371
|
+
echo "0"
|
|
372
|
+
return 1
|
|
373
|
+
fi
|
|
374
|
+
du -sk "$path" 2> /dev/null | cut -f1 | awk '{print $1 * 1024}' || echo "0"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Permission checks
|
|
378
|
+
check_sudo() {
|
|
379
|
+
if ! sudo -n true 2> /dev/null; then
|
|
380
|
+
return 1
|
|
381
|
+
fi
|
|
382
|
+
return 0
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# Check if Touch ID is configured for sudo
|
|
386
|
+
check_touchid_support() {
|
|
387
|
+
if [[ -f /etc/pam.d/sudo ]]; then
|
|
388
|
+
grep -q "pam_tid.so" /etc/pam.d/sudo 2> /dev/null
|
|
389
|
+
return $?
|
|
390
|
+
fi
|
|
391
|
+
return 1
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Request sudo access with Touch ID support
|
|
395
|
+
# Usage: request_sudo_access "prompt message" [optional: force_password]
|
|
396
|
+
request_sudo_access() {
|
|
397
|
+
local prompt_msg="${1:-Admin access required}"
|
|
398
|
+
local force_password="${2:-false}"
|
|
399
|
+
|
|
400
|
+
# Check if already has sudo access
|
|
401
|
+
if sudo -n true 2> /dev/null; then
|
|
402
|
+
return 0
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
# If Touch ID is supported and not forced to use password
|
|
406
|
+
if [[ "$force_password" != "true" ]] && check_touchid_support; then
|
|
407
|
+
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg} ${GRAY}(Touch ID or password)${NC}"
|
|
408
|
+
if sudo -v 2> /dev/null; then
|
|
409
|
+
return 0
|
|
410
|
+
else
|
|
411
|
+
return 1
|
|
412
|
+
fi
|
|
413
|
+
else
|
|
414
|
+
# Traditional password method
|
|
415
|
+
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg}"
|
|
416
|
+
echo -ne "${PURPLE}${ICON_ARROW}${NC} Password: "
|
|
417
|
+
read -s password
|
|
418
|
+
echo ""
|
|
419
|
+
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2> /dev/null; then
|
|
420
|
+
return 0
|
|
421
|
+
else
|
|
422
|
+
return 1
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
request_sudo() {
|
|
428
|
+
echo "This operation requires administrator privileges."
|
|
429
|
+
echo -n "Please enter your password: "
|
|
430
|
+
read -s password
|
|
431
|
+
echo
|
|
432
|
+
if echo "$password" | sudo -S true 2> /dev/null; then
|
|
433
|
+
return 0
|
|
434
|
+
else
|
|
435
|
+
log_error "Invalid password or cancelled"
|
|
436
|
+
return 1
|
|
437
|
+
fi
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
# Homebrew update utilities
|
|
441
|
+
update_via_homebrew() {
|
|
442
|
+
local version="${1:-unknown}"
|
|
443
|
+
|
|
444
|
+
if [[ -t 1 ]]; then
|
|
445
|
+
start_inline_spinner "Updating Homebrew..."
|
|
446
|
+
else
|
|
447
|
+
echo "Updating Homebrew..."
|
|
448
|
+
fi
|
|
449
|
+
# Filter out common noise but show important info
|
|
450
|
+
brew update 2>&1 | grep -Ev "^(==>|Already up-to-date)" || true
|
|
451
|
+
if [[ -t 1 ]]; then
|
|
452
|
+
stop_inline_spinner
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
if [[ -t 1 ]]; then
|
|
456
|
+
start_inline_spinner "Upgrading Mole..."
|
|
457
|
+
else
|
|
458
|
+
echo "Upgrading Mole..."
|
|
459
|
+
fi
|
|
460
|
+
local upgrade_output
|
|
461
|
+
upgrade_output=$(brew upgrade mole 2>&1) || true
|
|
462
|
+
if [[ -t 1 ]]; then
|
|
463
|
+
stop_inline_spinner
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
if echo "$upgrade_output" | grep -q "already installed"; then
|
|
467
|
+
# Get current version
|
|
468
|
+
local current_version
|
|
469
|
+
current_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}')
|
|
470
|
+
echo -e "${GREEN}${ICON_SUCCESS}${NC} Already on latest version (${current_version:-$version})"
|
|
471
|
+
elif echo "$upgrade_output" | grep -q "Error:"; then
|
|
472
|
+
log_error "Homebrew upgrade failed"
|
|
473
|
+
echo "$upgrade_output" | grep "Error:" >&2
|
|
474
|
+
return 1
|
|
475
|
+
else
|
|
476
|
+
# Show relevant output, filter noise
|
|
477
|
+
echo "$upgrade_output" | grep -Ev "^(==>|Updating Homebrew|Warning:)" || true
|
|
478
|
+
# Get new version
|
|
479
|
+
local new_version
|
|
480
|
+
new_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}')
|
|
481
|
+
echo -e "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-$version})"
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
# Clear version check cache
|
|
485
|
+
rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message"
|
|
486
|
+
return 0
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# Load basic configuration
|
|
490
|
+
load_config() {
|
|
491
|
+
MOLE_MAX_LOG_SIZE="${MOLE_MAX_LOG_SIZE:-1048576}"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# Initialize configuration on sourcing
|
|
495
|
+
load_config
|
|
496
|
+
|
|
497
|
+
# ============================================================================
|
|
498
|
+
# Spinner and Progress Indicators
|
|
499
|
+
# ============================================================================
|
|
500
|
+
|
|
501
|
+
# Global spinner process IDs
|
|
502
|
+
SPINNER_PID=""
|
|
503
|
+
INLINE_SPINNER_PID=""
|
|
504
|
+
|
|
505
|
+
# Start a full-line spinner with message
|
|
506
|
+
start_spinner() {
|
|
507
|
+
local message="$1"
|
|
508
|
+
|
|
509
|
+
if [[ ! -t 1 ]]; then
|
|
510
|
+
echo -n " ${BLUE}|${NC} $message"
|
|
511
|
+
return
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
echo -n " ${BLUE}|${NC} $message"
|
|
515
|
+
(
|
|
516
|
+
local delay=0.5
|
|
517
|
+
while true; do
|
|
518
|
+
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}|${NC} $message. "
|
|
519
|
+
sleep $delay
|
|
520
|
+
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}|${NC} $message.. "
|
|
521
|
+
sleep $delay
|
|
522
|
+
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}|${NC} $message..."
|
|
523
|
+
sleep $delay
|
|
524
|
+
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}|${NC} $message "
|
|
525
|
+
sleep $delay
|
|
526
|
+
done
|
|
527
|
+
) &
|
|
528
|
+
SPINNER_PID=$!
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# Start an inline spinner (rotating character)
|
|
532
|
+
start_inline_spinner() {
|
|
533
|
+
stop_inline_spinner 2> /dev/null || true
|
|
534
|
+
local message="$1"
|
|
535
|
+
|
|
536
|
+
if [[ -t 1 ]]; then
|
|
537
|
+
(
|
|
538
|
+
trap 'exit 0' TERM INT EXIT
|
|
539
|
+
local chars
|
|
540
|
+
chars="$(mo_spinner_chars)"
|
|
541
|
+
[[ -z "$chars" ]] && chars='|/-\'
|
|
542
|
+
local i=0
|
|
543
|
+
while true; do
|
|
544
|
+
local c="${chars:$((i % ${#chars})):1}"
|
|
545
|
+
printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}%s${NC} %s" "$c" "$message" 2> /dev/null || exit 0
|
|
546
|
+
((i++))
|
|
547
|
+
# macOS supports decimal sleep, this is the primary target
|
|
548
|
+
sleep 0.1 2> /dev/null || sleep 1 2> /dev/null || exit 0
|
|
549
|
+
done
|
|
550
|
+
) &
|
|
551
|
+
INLINE_SPINNER_PID=$!
|
|
552
|
+
disown 2> /dev/null || true
|
|
553
|
+
else
|
|
554
|
+
echo -n " ${BLUE}|${NC} $message"
|
|
555
|
+
fi
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# Stop inline spinner
|
|
559
|
+
stop_inline_spinner() {
|
|
560
|
+
if [[ -n "$INLINE_SPINNER_PID" ]]; then
|
|
561
|
+
kill "$INLINE_SPINNER_PID" 2> /dev/null || true
|
|
562
|
+
wait "$INLINE_SPINNER_PID" 2> /dev/null || true
|
|
563
|
+
INLINE_SPINNER_PID=""
|
|
564
|
+
[[ -t 1 ]] && printf "\r\033[K"
|
|
565
|
+
fi
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# Stop spinner with optional result message
|
|
569
|
+
stop_spinner() {
|
|
570
|
+
local result_message="${1:-Done}"
|
|
571
|
+
|
|
572
|
+
stop_inline_spinner
|
|
573
|
+
|
|
574
|
+
if [[ -n "$SPINNER_PID" ]]; then
|
|
575
|
+
kill "$SPINNER_PID" 2> /dev/null || true
|
|
576
|
+
wait "$SPINNER_PID" 2> /dev/null || true
|
|
577
|
+
SPINNER_PID=""
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
if [[ -n "$result_message" ]]; then
|
|
581
|
+
if [[ -t 1 ]]; then
|
|
582
|
+
printf "\r${MOLE_SPINNER_PREFIX:-}${GREEN}${ICON_SUCCESS}${NC} %s\n" "$result_message"
|
|
583
|
+
else
|
|
584
|
+
echo " ${ICON_SUCCESS} $result_message"
|
|
585
|
+
fi
|
|
586
|
+
fi
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# ============================================================================
|
|
590
|
+
# User Interaction - Confirmation Dialogs
|
|
591
|
+
# ============================================================================
|
|
592
|
+
|
|
593
|
+
# ============================================================================
|
|
594
|
+
# Temporary File Management
|
|
595
|
+
# ============================================================================
|
|
596
|
+
|
|
597
|
+
# Global temp file tracking
|
|
598
|
+
declare -a MOLE_TEMP_FILES=()
|
|
599
|
+
declare -a MOLE_TEMP_DIRS=()
|
|
600
|
+
|
|
601
|
+
# Create tracked temporary file
|
|
602
|
+
# Returns: temp file path
|
|
603
|
+
create_temp_file() {
|
|
604
|
+
local temp
|
|
605
|
+
temp=$(mktemp) || return 1
|
|
606
|
+
MOLE_TEMP_FILES+=("$temp")
|
|
607
|
+
echo "$temp"
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
# Create tracked temporary directory
|
|
611
|
+
# Returns: temp directory path
|
|
612
|
+
create_temp_dir() {
|
|
613
|
+
local temp
|
|
614
|
+
temp=$(mktemp -d) || return 1
|
|
615
|
+
MOLE_TEMP_DIRS+=("$temp")
|
|
616
|
+
echo "$temp"
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# Create temp file with prefix (for analyze.sh compatibility)
|
|
620
|
+
# Args: $1 - prefix/suffix string
|
|
621
|
+
# Returns: temp file path
|
|
622
|
+
create_temp_file_named() {
|
|
623
|
+
local suffix="${1:-}"
|
|
624
|
+
local temp
|
|
625
|
+
temp=$(mktemp "/tmp/mole_${suffix}_XXXXXX") || return 1
|
|
626
|
+
MOLE_TEMP_FILES+=("$temp")
|
|
627
|
+
echo "$temp"
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
# Cleanup all tracked temp files
|
|
631
|
+
cleanup_temp_files() {
|
|
632
|
+
local file
|
|
633
|
+
if [[ ${#MOLE_TEMP_FILES[@]} -gt 0 ]]; then
|
|
634
|
+
for file in "${MOLE_TEMP_FILES[@]}"; do
|
|
635
|
+
[[ -f "$file" ]] && rm -f "$file" 2> /dev/null || true
|
|
636
|
+
done
|
|
637
|
+
fi
|
|
638
|
+
|
|
639
|
+
if [[ ${#MOLE_TEMP_DIRS[@]} -gt 0 ]]; then
|
|
640
|
+
for file in "${MOLE_TEMP_DIRS[@]}"; do
|
|
641
|
+
[[ -d "$file" ]] && rm -rf "$file" 2> /dev/null || true
|
|
642
|
+
done
|
|
643
|
+
fi
|
|
644
|
+
|
|
645
|
+
MOLE_TEMP_FILES=()
|
|
646
|
+
MOLE_TEMP_DIRS=()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
# Auto-cleanup on script exit (call this in main scripts)
|
|
650
|
+
register_temp_cleanup() {
|
|
651
|
+
trap cleanup_temp_files EXIT INT TERM
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# ============================================================================
|
|
655
|
+
# Parallel Processing Framework
|
|
656
|
+
# ============================================================================
|
|
657
|
+
|
|
658
|
+
# Execute commands in parallel with job control
|
|
659
|
+
# Args: $1 - max parallel jobs
|
|
660
|
+
# $2 - worker function name
|
|
661
|
+
# $3+ - items to process
|
|
662
|
+
parallel_execute() {
|
|
663
|
+
local max_jobs="${1:-12}"
|
|
664
|
+
local worker_func="$2"
|
|
665
|
+
shift 2
|
|
666
|
+
local -a items=("$@")
|
|
667
|
+
|
|
668
|
+
if [[ ${#items[@]} -eq 0 ]]; then
|
|
669
|
+
return 0
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
local -a pids=()
|
|
673
|
+
for item in "${items[@]}"; do
|
|
674
|
+
# Execute worker function in background
|
|
675
|
+
"$worker_func" "$item" &
|
|
676
|
+
pids+=($!)
|
|
677
|
+
|
|
678
|
+
# Wait for a slot if we've hit max parallel jobs
|
|
679
|
+
if ((${#pids[@]} >= max_jobs)); then
|
|
680
|
+
wait "${pids[0]}" 2> /dev/null || true
|
|
681
|
+
pids=("${pids[@]:1}")
|
|
682
|
+
fi
|
|
683
|
+
done
|
|
684
|
+
|
|
685
|
+
# Wait for remaining background jobs
|
|
686
|
+
if ((${#pids[@]} > 0)); then
|
|
687
|
+
for pid in "${pids[@]}"; do
|
|
688
|
+
wait "$pid" 2> /dev/null || true
|
|
689
|
+
done
|
|
690
|
+
fi
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
# ============================================================================
|
|
694
|
+
# Lightweight spinner helper wrappers
|
|
695
|
+
# ============================================================================
|
|
696
|
+
# Usage: with_spinner "Message" cmd arg...
|
|
697
|
+
# Set MOLE_SPINNER_PREFIX=" " for indented spinner (e.g., in clean context)
|
|
698
|
+
with_spinner() {
|
|
699
|
+
local msg="$1"
|
|
700
|
+
shift || true
|
|
701
|
+
local timeout="${MOLE_CMD_TIMEOUT:-180}" # Default 3min timeout
|
|
702
|
+
|
|
703
|
+
if [[ -t 1 ]]; then
|
|
704
|
+
start_inline_spinner "$msg"
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
# Run command with timeout protection
|
|
708
|
+
if command -v timeout > /dev/null 2>&1; then
|
|
709
|
+
# GNU timeout available
|
|
710
|
+
timeout "$timeout" "$@" > /dev/null 2>&1 || {
|
|
711
|
+
local exit_code=$?
|
|
712
|
+
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
713
|
+
# Exit code 124 means timeout
|
|
714
|
+
[[ $exit_code -eq 124 ]] && echo -e " ${YELLOW}${ICON_WARNING}${NC} $msg timed out (skipped)" >&2
|
|
715
|
+
return $exit_code
|
|
716
|
+
}
|
|
717
|
+
else
|
|
718
|
+
# Fallback: run in background with manual timeout
|
|
719
|
+
"$@" > /dev/null 2>&1 &
|
|
720
|
+
local cmd_pid=$!
|
|
721
|
+
local elapsed=0
|
|
722
|
+
while kill -0 $cmd_pid 2> /dev/null; do
|
|
723
|
+
if [[ $elapsed -ge $timeout ]]; then
|
|
724
|
+
kill -TERM $cmd_pid 2> /dev/null || true
|
|
725
|
+
wait $cmd_pid 2> /dev/null || true
|
|
726
|
+
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
727
|
+
echo -e " ${YELLOW}${ICON_WARNING}${NC} $msg timed out (skipped)" >&2
|
|
728
|
+
return 124
|
|
729
|
+
fi
|
|
730
|
+
sleep 1
|
|
731
|
+
((elapsed++))
|
|
732
|
+
done
|
|
733
|
+
wait $cmd_pid 2> /dev/null || {
|
|
734
|
+
local exit_code=$?
|
|
735
|
+
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
|
736
|
+
return $exit_code
|
|
737
|
+
}
|
|
738
|
+
fi
|
|
739
|
+
|
|
740
|
+
if [[ -t 1 ]]; then
|
|
741
|
+
stop_inline_spinner
|
|
742
|
+
fi
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
# ============================================================================
|
|
746
|
+
# Cache/tool cleanup abstraction
|
|
747
|
+
# ============================================================================
|
|
748
|
+
# clean_tool_cache "Label" command...
|
|
749
|
+
clean_tool_cache() {
|
|
750
|
+
local label="$1"
|
|
751
|
+
shift || true
|
|
752
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
753
|
+
echo -e " ${YELLOW}→${NC} $label (would clean)"
|
|
754
|
+
return 0
|
|
755
|
+
fi
|
|
756
|
+
if MOLE_SPINNER_PREFIX=" " with_spinner "$label" "$@"; then
|
|
757
|
+
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label"
|
|
758
|
+
else
|
|
759
|
+
local exit_code=$?
|
|
760
|
+
# Timeout returns 124, don't show error message (already shown by with_spinner)
|
|
761
|
+
if [[ $exit_code -ne 124 ]]; then
|
|
762
|
+
echo -e " ${YELLOW}${ICON_WARNING}${NC} $label failed (skipped)" >&2
|
|
763
|
+
fi
|
|
764
|
+
fi
|
|
765
|
+
return 0 # Always return success to continue cleanup
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
# ============================================================================
|
|
769
|
+
# Unified confirmation prompt with consistent style
|
|
770
|
+
# ============================================================================
|
|
771
|
+
|
|
772
|
+
# Unified action prompt
|
|
773
|
+
# Usage: prompt_action "action" "cancel_text" -> returns 0 for yes, 1 for no
|
|
774
|
+
# Example: prompt_action "enable" "quit" -> "☛ Press Enter to enable, ESC to quit: "
|
|
775
|
+
prompt_action() {
|
|
776
|
+
local action="$1"
|
|
777
|
+
local cancel="${2:-cancel}"
|
|
778
|
+
|
|
779
|
+
echo ""
|
|
780
|
+
echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to ${action}, ${GRAY}ESC${NC} to ${cancel}: "
|
|
781
|
+
IFS= read -r -s -n1 key || key=""
|
|
782
|
+
|
|
783
|
+
case "$key" in
|
|
784
|
+
$'\e') # ESC
|
|
785
|
+
echo ""
|
|
786
|
+
return 1
|
|
787
|
+
;;
|
|
788
|
+
"" | $'\n' | $'\r') # Enter
|
|
789
|
+
printf "\r\033[K" # Clear the prompt line
|
|
790
|
+
return 0
|
|
791
|
+
;;
|
|
792
|
+
*)
|
|
793
|
+
echo ""
|
|
794
|
+
return 1
|
|
795
|
+
;;
|
|
796
|
+
esac
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
# Legacy confirmation prompt (kept for compatibility)
|
|
800
|
+
# confirm_prompt "Message" -> 0 yes, 1 no
|
|
801
|
+
confirm_prompt() {
|
|
802
|
+
local message="$1"
|
|
803
|
+
echo -n "$message (Enter=OK / ESC q=Cancel): "
|
|
804
|
+
IFS= read -r -s -n1 _key || _key=""
|
|
805
|
+
case "$_key" in
|
|
806
|
+
$'\e' | q | Q)
|
|
807
|
+
echo ""
|
|
808
|
+
return 1
|
|
809
|
+
;;
|
|
810
|
+
"" | $'\n' | $'\r' | y | Y)
|
|
811
|
+
echo ""
|
|
812
|
+
return 0
|
|
813
|
+
;;
|
|
814
|
+
*)
|
|
815
|
+
echo ""
|
|
816
|
+
return 1
|
|
817
|
+
;;
|
|
818
|
+
esac
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
# Get optimal parallel job count based on CPU cores
|
|
822
|
+
|
|
823
|
+
# =========================================================================
|
|
824
|
+
# Size helpers
|
|
825
|
+
# =========================================================================
|
|
826
|
+
bytes_to_human_kb() { bytes_to_human "$((${1:-0} * 1024))"; }
|
|
827
|
+
print_space_stat() {
|
|
828
|
+
local freed_kb="$1"
|
|
829
|
+
shift || true
|
|
830
|
+
local current_free
|
|
831
|
+
current_free=$(get_free_space)
|
|
832
|
+
local human
|
|
833
|
+
human=$(bytes_to_human_kb "$freed_kb")
|
|
834
|
+
echo "Space freed: ${GREEN}${human}${NC} | Free space now: $current_free"
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
# =========================================================================
|
|
838
|
+
# mktemp unification wrappers (register access)
|
|
839
|
+
# =========================================================================
|
|
840
|
+
register_temp_file() { MOLE_TEMP_FILES+=("$1"); }
|
|
841
|
+
register_temp_dir() { MOLE_TEMP_DIRS+=("$1"); }
|
|
842
|
+
|
|
843
|
+
mktemp_file() {
|
|
844
|
+
local f
|
|
845
|
+
f=$(mktemp) || return 1
|
|
846
|
+
register_temp_file "$f"
|
|
847
|
+
echo "$f"
|
|
848
|
+
}
|
|
849
|
+
mktemp_dir() {
|
|
850
|
+
local d
|
|
851
|
+
d=$(mktemp -d) || return 1
|
|
852
|
+
register_temp_dir "$d"
|
|
853
|
+
echo "$d"
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
# =========================================================================
|
|
857
|
+
# Uninstall helper abstractions
|
|
858
|
+
# =========================================================================
|
|
859
|
+
force_kill_app() {
|
|
860
|
+
# Args: app_name [app_path]; tries graceful then force kill; returns 0 if stopped, 1 otherwise
|
|
861
|
+
local app_name="$1"
|
|
862
|
+
local app_path="${2:-}"
|
|
863
|
+
|
|
864
|
+
# Use app path for precise matching if provided
|
|
865
|
+
local match_pattern="$app_name"
|
|
866
|
+
if [[ -n "$app_path" && -e "$app_path" ]]; then
|
|
867
|
+
# Use the app bundle path for more precise matching
|
|
868
|
+
match_pattern="$app_path"
|
|
869
|
+
fi
|
|
870
|
+
|
|
871
|
+
if pgrep -f "$match_pattern" > /dev/null 2>&1; then
|
|
872
|
+
pkill -f "$match_pattern" 2> /dev/null || true
|
|
873
|
+
sleep 1
|
|
874
|
+
fi
|
|
875
|
+
if pgrep -f "$match_pattern" > /dev/null 2>&1; then
|
|
876
|
+
pkill -9 -f "$match_pattern" 2> /dev/null || true
|
|
877
|
+
sleep 1
|
|
878
|
+
fi
|
|
879
|
+
pgrep -f "$match_pattern" > /dev/null 2>&1 && return 1 || return 0
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
# Remove application icons from the Dock (best effort)
|
|
883
|
+
remove_apps_from_dock() {
|
|
884
|
+
if [[ $# -eq 0 ]]; then
|
|
885
|
+
return 0
|
|
886
|
+
fi
|
|
887
|
+
|
|
888
|
+
local plist="$HOME/Library/Preferences/com.apple.dock.plist"
|
|
889
|
+
[[ -f "$plist" ]] || return 0
|
|
890
|
+
|
|
891
|
+
if ! command -v python3 > /dev/null 2>&1; then
|
|
892
|
+
return 0
|
|
893
|
+
fi
|
|
894
|
+
|
|
895
|
+
# Execute Python helper to prune dock entries for the given app paths.
|
|
896
|
+
# Exit status 2 means entries were removed.
|
|
897
|
+
local target_count=$#
|
|
898
|
+
|
|
899
|
+
python3 - "$@" << 'PY'
|
|
900
|
+
import os
|
|
901
|
+
import plistlib
|
|
902
|
+
import subprocess
|
|
903
|
+
import sys
|
|
904
|
+
import urllib.parse
|
|
905
|
+
|
|
906
|
+
plist_path = os.path.expanduser('~/Library/Preferences/com.apple.dock.plist')
|
|
907
|
+
if not os.path.exists(plist_path):
|
|
908
|
+
sys.exit(0)
|
|
909
|
+
|
|
910
|
+
def normalise(path):
|
|
911
|
+
if not path:
|
|
912
|
+
return ''
|
|
913
|
+
return os.path.normpath(os.path.realpath(path.rstrip('/')))
|
|
914
|
+
|
|
915
|
+
targets = {normalise(arg) for arg in sys.argv[1:] if arg}
|
|
916
|
+
targets = {t for t in targets if t}
|
|
917
|
+
if not targets:
|
|
918
|
+
sys.exit(0)
|
|
919
|
+
|
|
920
|
+
with open(plist_path, 'rb') as fh:
|
|
921
|
+
try:
|
|
922
|
+
data = plistlib.load(fh)
|
|
923
|
+
except Exception:
|
|
924
|
+
sys.exit(0)
|
|
925
|
+
|
|
926
|
+
apps = data.get('persistent-apps')
|
|
927
|
+
if not isinstance(apps, list):
|
|
928
|
+
sys.exit(0)
|
|
929
|
+
|
|
930
|
+
changed = False
|
|
931
|
+
filtered = []
|
|
932
|
+
for item in apps:
|
|
933
|
+
try:
|
|
934
|
+
url = item['tile-data']['file-data']['_CFURLString']
|
|
935
|
+
except (KeyError, TypeError):
|
|
936
|
+
filtered.append(item)
|
|
937
|
+
continue
|
|
938
|
+
|
|
939
|
+
if not isinstance(url, str):
|
|
940
|
+
filtered.append(item)
|
|
941
|
+
continue
|
|
942
|
+
|
|
943
|
+
parsed = urllib.parse.urlparse(url)
|
|
944
|
+
path = urllib.parse.unquote(parsed.path or '')
|
|
945
|
+
if not path:
|
|
946
|
+
filtered.append(item)
|
|
947
|
+
continue
|
|
948
|
+
|
|
949
|
+
candidate = normalise(path)
|
|
950
|
+
if any(candidate == t or candidate.startswith(t + os.sep) for t in targets):
|
|
951
|
+
changed = True
|
|
952
|
+
continue
|
|
953
|
+
|
|
954
|
+
filtered.append(item)
|
|
955
|
+
|
|
956
|
+
if not changed:
|
|
957
|
+
sys.exit(0)
|
|
958
|
+
|
|
959
|
+
data['persistent-apps'] = filtered
|
|
960
|
+
with open(plist_path, 'wb') as fh:
|
|
961
|
+
try:
|
|
962
|
+
plistlib.dump(data, fh, fmt=plistlib.FMT_BINARY)
|
|
963
|
+
except Exception:
|
|
964
|
+
plistlib.dump(data, fh)
|
|
965
|
+
|
|
966
|
+
# Restart Dock to apply changes (ignore errors)
|
|
967
|
+
try:
|
|
968
|
+
subprocess.run(['killall', 'Dock'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
|
|
969
|
+
except Exception:
|
|
970
|
+
pass
|
|
971
|
+
|
|
972
|
+
sys.exit(2)
|
|
973
|
+
PY
|
|
974
|
+
local python_status=$?
|
|
975
|
+
if [[ $python_status -eq 2 ]]; then
|
|
976
|
+
if [[ $target_count -gt 1 ]]; then
|
|
977
|
+
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icons from Dock"
|
|
978
|
+
else
|
|
979
|
+
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed app icon from Dock"
|
|
980
|
+
fi
|
|
981
|
+
return 0
|
|
982
|
+
fi
|
|
983
|
+
return $python_status
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
map_uninstall_reason() {
|
|
987
|
+
# Args: reason_token
|
|
988
|
+
case "$1" in
|
|
989
|
+
still*running*) echo "was not removed; it remains running and resisted termination." ;;
|
|
990
|
+
remove*failed*) echo "was not removed due to a removal failure (permissions or protection)." ;;
|
|
991
|
+
permission*) echo "was not removed due to insufficient permissions." ;;
|
|
992
|
+
*) echo "was not removed; $1." ;;
|
|
993
|
+
esac
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
batch_safe_clean() {
|
|
997
|
+
# Usage: batch_safe_clean "Label" path1 path2 ...
|
|
998
|
+
local label="$1"
|
|
999
|
+
shift || true
|
|
1000
|
+
local -a paths=("$@")
|
|
1001
|
+
if [[ ${#paths[@]} -eq 0 ]]; then return 0; fi
|
|
1002
|
+
safe_clean "${paths[@]}" "$label"
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
# Get optimal parallel job count based on CPU cores
|
|
1006
|
+
get_optimal_parallel_jobs() {
|
|
1007
|
+
local operation_type="${1:-default}"
|
|
1008
|
+
local cpu_cores
|
|
1009
|
+
cpu_cores=$(sysctl -n hw.ncpu 2> /dev/null || echo 4)
|
|
1010
|
+
case "$operation_type" in
|
|
1011
|
+
scan | io)
|
|
1012
|
+
echo $((cpu_cores * 2))
|
|
1013
|
+
;;
|
|
1014
|
+
compute)
|
|
1015
|
+
echo "$cpu_cores"
|
|
1016
|
+
;;
|
|
1017
|
+
*)
|
|
1018
|
+
echo $((cpu_cores + 2))
|
|
1019
|
+
;;
|
|
1020
|
+
esac
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
# ============================================================================
|
|
1024
|
+
# Sudo Keepalive Management
|
|
1025
|
+
# ============================================================================
|
|
1026
|
+
|
|
1027
|
+
# Start sudo keepalive process
|
|
1028
|
+
# Returns: PID of the keepalive process
|
|
1029
|
+
start_sudo_keepalive() {
|
|
1030
|
+
(
|
|
1031
|
+
local retry_count=0
|
|
1032
|
+
while true; do
|
|
1033
|
+
if ! sudo -n true 2> /dev/null; then
|
|
1034
|
+
((retry_count++))
|
|
1035
|
+
if [[ $retry_count -ge 3 ]]; then
|
|
1036
|
+
exit 1
|
|
1037
|
+
fi
|
|
1038
|
+
sleep 5
|
|
1039
|
+
continue
|
|
1040
|
+
fi
|
|
1041
|
+
retry_count=0
|
|
1042
|
+
sleep 30
|
|
1043
|
+
kill -0 "$$" 2> /dev/null || exit
|
|
1044
|
+
done
|
|
1045
|
+
) 2> /dev/null &
|
|
1046
|
+
echo $!
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
# Stop sudo keepalive process
|
|
1050
|
+
# Args: $1 - PID of the keepalive process
|
|
1051
|
+
stop_sudo_keepalive() {
|
|
1052
|
+
local pid="${1:-}"
|
|
1053
|
+
if [[ -n "$pid" ]]; then
|
|
1054
|
+
kill "$pid" 2> /dev/null || true
|
|
1055
|
+
wait "$pid" 2> /dev/null || true
|
|
1056
|
+
fi
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
# ============================================================================
|
|
1060
|
+
# Section Management
|
|
1061
|
+
# ============================================================================
|
|
1062
|
+
|
|
1063
|
+
# Section tracking variables
|
|
1064
|
+
TRACK_SECTION=0
|
|
1065
|
+
SECTION_ACTIVITY=0
|
|
1066
|
+
|
|
1067
|
+
# Start a new section
|
|
1068
|
+
start_section() {
|
|
1069
|
+
TRACK_SECTION=1
|
|
1070
|
+
SECTION_ACTIVITY=0
|
|
1071
|
+
echo ""
|
|
1072
|
+
echo -e "${PURPLE}${ICON_ARROW} $1${NC}"
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
# End a section (show "Nothing to tidy" if no activity)
|
|
1076
|
+
end_section() {
|
|
1077
|
+
if [[ $TRACK_SECTION -eq 1 && $SECTION_ACTIVITY -eq 0 ]]; then
|
|
1078
|
+
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Nothing to tidy"
|
|
1079
|
+
fi
|
|
1080
|
+
TRACK_SECTION=0
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
# Mark activity in current section
|
|
1084
|
+
note_activity() {
|
|
1085
|
+
if [[ $TRACK_SECTION -eq 1 ]]; then
|
|
1086
|
+
SECTION_ACTIVITY=1
|
|
1087
|
+
fi
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
# ============================================================================
|
|
1091
|
+
# App Management Functions
|
|
1092
|
+
# ============================================================================
|
|
1093
|
+
|
|
1094
|
+
# System critical components that should NEVER be uninstalled
|
|
1095
|
+
readonly SYSTEM_CRITICAL_BUNDLES=(
|
|
1096
|
+
"com.apple.*" # System essentials
|
|
1097
|
+
"loginwindow"
|
|
1098
|
+
"dock"
|
|
1099
|
+
"systempreferences"
|
|
1100
|
+
"finder"
|
|
1101
|
+
"safari"
|
|
1102
|
+
"keychain*"
|
|
1103
|
+
"security*"
|
|
1104
|
+
"bluetooth*"
|
|
1105
|
+
"wifi*"
|
|
1106
|
+
"network*"
|
|
1107
|
+
"tcc"
|
|
1108
|
+
"notification*"
|
|
1109
|
+
"accessibility*"
|
|
1110
|
+
"universalaccess*"
|
|
1111
|
+
"HIToolbox*"
|
|
1112
|
+
"textinput*"
|
|
1113
|
+
"TextInput*"
|
|
1114
|
+
"keyboard*"
|
|
1115
|
+
"Keyboard*"
|
|
1116
|
+
"inputsource*"
|
|
1117
|
+
"InputSource*"
|
|
1118
|
+
"keylayout*"
|
|
1119
|
+
"KeyLayout*"
|
|
1120
|
+
"GlobalPreferences"
|
|
1121
|
+
".GlobalPreferences"
|
|
1122
|
+
# Input methods (critical for international users)
|
|
1123
|
+
"com.tencent.inputmethod.QQInput"
|
|
1124
|
+
"com.sogou.inputmethod.*"
|
|
1125
|
+
"com.baidu.inputmethod.*"
|
|
1126
|
+
"com.apple.inputmethod.*"
|
|
1127
|
+
"com.googlecode.rimeime.*"
|
|
1128
|
+
"im.rime.*"
|
|
1129
|
+
"org.pqrs.Karabiner*"
|
|
1130
|
+
"*.inputmethod"
|
|
1131
|
+
"*.InputMethod"
|
|
1132
|
+
"*IME"
|
|
1133
|
+
"com.apple.inputsource*"
|
|
1134
|
+
"com.apple.TextInputMenuAgent"
|
|
1135
|
+
"com.apple.TextInputSwitcher"
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
# Apps with important data/licenses - protect during cleanup but allow uninstall
|
|
1139
|
+
readonly DATA_PROTECTED_BUNDLES=(
|
|
1140
|
+
# ============================================================================
|
|
1141
|
+
# System Utilities & Cleanup Tools
|
|
1142
|
+
# ============================================================================
|
|
1143
|
+
"com.nektony.*" # App Cleaner & Uninstaller
|
|
1144
|
+
"com.macpaw.*" # CleanMyMac, CleanMaster
|
|
1145
|
+
"com.freemacsoft.AppCleaner" # AppCleaner
|
|
1146
|
+
"com.omnigroup.omnidisksweeper" # OmniDiskSweeper
|
|
1147
|
+
"com.daisydiskapp.*" # DaisyDisk
|
|
1148
|
+
"com.tunabellysoftware.*" # Disk Utility apps
|
|
1149
|
+
"com.grandperspectiv.*" # GrandPerspective
|
|
1150
|
+
"com.binaryfruit.*" # FusionCast
|
|
1151
|
+
|
|
1152
|
+
# ============================================================================
|
|
1153
|
+
# Password Managers & Security
|
|
1154
|
+
# ============================================================================
|
|
1155
|
+
"com.1password.*" # 1Password
|
|
1156
|
+
"com.agilebits.*" # 1Password legacy
|
|
1157
|
+
"com.lastpass.*" # LastPass
|
|
1158
|
+
"com.dashlane.*" # Dashlane
|
|
1159
|
+
"com.bitwarden.*" # Bitwarden
|
|
1160
|
+
"com.keepassx.*" # KeePassXC
|
|
1161
|
+
"org.keepassx.*" # KeePassX
|
|
1162
|
+
"com.authy.*" # Authy
|
|
1163
|
+
"com.yubico.*" # YubiKey Manager
|
|
1164
|
+
|
|
1165
|
+
# ============================================================================
|
|
1166
|
+
# Development Tools - IDEs & Editors
|
|
1167
|
+
# ============================================================================
|
|
1168
|
+
"com.jetbrains.*" # JetBrains IDEs (IntelliJ, DataGrip, etc.)
|
|
1169
|
+
"JetBrains*" # JetBrains Application Support folders
|
|
1170
|
+
"com.microsoft.VSCode" # Visual Studio Code
|
|
1171
|
+
"com.visualstudio.code.*" # VS Code variants
|
|
1172
|
+
"com.sublimetext.*" # Sublime Text
|
|
1173
|
+
"com.sublimehq.*" # Sublime Merge
|
|
1174
|
+
"com.microsoft.VSCodeInsiders" # VS Code Insiders
|
|
1175
|
+
"com.apple.dt.Xcode" # Xcode (keep settings)
|
|
1176
|
+
"com.coteditor.CotEditor" # CotEditor
|
|
1177
|
+
"com.macromates.TextMate" # TextMate
|
|
1178
|
+
"com.panic.Nova" # Nova
|
|
1179
|
+
"abnerworks.Typora" # Typora (Markdown editor)
|
|
1180
|
+
"com.uranusjr.macdown" # MacDown
|
|
1181
|
+
|
|
1182
|
+
# ============================================================================
|
|
1183
|
+
# Development Tools - Database Clients
|
|
1184
|
+
# ============================================================================
|
|
1185
|
+
"com.sequelpro.*" # Sequel Pro
|
|
1186
|
+
"com.sequel-ace.*" # Sequel Ace
|
|
1187
|
+
"com.tinyapp.*" # TablePlus
|
|
1188
|
+
"com.dbeaver.*" # DBeaver
|
|
1189
|
+
"com.navicat.*" # Navicat
|
|
1190
|
+
"com.mongodb.compass" # MongoDB Compass
|
|
1191
|
+
"com.redis.RedisInsight" # Redis Insight
|
|
1192
|
+
"com.pgadmin.pgadmin4" # pgAdmin
|
|
1193
|
+
"com.eggerapps.Sequel-Pro" # Sequel Pro legacy
|
|
1194
|
+
"com.valentina-db.Valentina-Studio" # Valentina Studio
|
|
1195
|
+
"com.dbvis.DbVisualizer" # DbVisualizer
|
|
1196
|
+
|
|
1197
|
+
# ============================================================================
|
|
1198
|
+
# Development Tools - API & Network
|
|
1199
|
+
# ============================================================================
|
|
1200
|
+
"com.postmanlabs.mac" # Postman
|
|
1201
|
+
"com.konghq.insomnia" # Insomnia
|
|
1202
|
+
"com.CharlesProxy.*" # Charles Proxy
|
|
1203
|
+
"com.proxyman.*" # Proxyman
|
|
1204
|
+
"com.getpaw.*" # Paw
|
|
1205
|
+
"com.luckymarmot.Paw" # Paw legacy
|
|
1206
|
+
"com.charlesproxy.charles" # Charles
|
|
1207
|
+
"com.telerik.Fiddler" # Fiddler
|
|
1208
|
+
"com.usebruno.app" # Bruno (API client)
|
|
1209
|
+
|
|
1210
|
+
# ============================================================================
|
|
1211
|
+
# Development Tools - Git & Version Control
|
|
1212
|
+
# ============================================================================
|
|
1213
|
+
"com.github.GitHubDesktop" # GitHub Desktop
|
|
1214
|
+
"com.sublimemerge" # Sublime Merge
|
|
1215
|
+
"com.torusknot.SourceTreeNotMAS" # SourceTree
|
|
1216
|
+
"com.git-tower.Tower*" # Tower
|
|
1217
|
+
"com.gitfox.GitFox" # GitFox
|
|
1218
|
+
"com.github.Gitify" # Gitify
|
|
1219
|
+
"com.fork.Fork" # Fork
|
|
1220
|
+
"com.axosoft.gitkraken" # GitKraken
|
|
1221
|
+
|
|
1222
|
+
# ============================================================================
|
|
1223
|
+
# Development Tools - Terminal & Shell
|
|
1224
|
+
# ============================================================================
|
|
1225
|
+
"com.googlecode.iterm2" # iTerm2
|
|
1226
|
+
"net.kovidgoyal.kitty" # Kitty
|
|
1227
|
+
"io.alacritty" # Alacritty
|
|
1228
|
+
"com.github.wez.wezterm" # WezTerm
|
|
1229
|
+
"com.hyper.Hyper" # Hyper
|
|
1230
|
+
"com.mizage.divvy" # Divvy
|
|
1231
|
+
"com.fig.Fig" # Fig (terminal assistant)
|
|
1232
|
+
"dev.warp.Warp-Stable" # Warp
|
|
1233
|
+
"com.termius-dmg" # Termius (SSH client)
|
|
1234
|
+
|
|
1235
|
+
# ============================================================================
|
|
1236
|
+
# Development Tools - Docker & Virtualization
|
|
1237
|
+
# ============================================================================
|
|
1238
|
+
"com.docker.docker" # Docker Desktop
|
|
1239
|
+
"com.getutm.UTM" # UTM
|
|
1240
|
+
"com.vmware.fusion" # VMware Fusion
|
|
1241
|
+
"com.parallels.desktop.*" # Parallels Desktop
|
|
1242
|
+
"org.virtualbox.app.VirtualBox" # VirtualBox
|
|
1243
|
+
"com.vagrant.*" # Vagrant
|
|
1244
|
+
"com.orbstack.OrbStack" # OrbStack
|
|
1245
|
+
|
|
1246
|
+
# ============================================================================
|
|
1247
|
+
# System Monitoring & Performance
|
|
1248
|
+
# ============================================================================
|
|
1249
|
+
"com.bjango.istatmenus*" # iStat Menus
|
|
1250
|
+
"eu.exelban.Stats" # Stats
|
|
1251
|
+
"com.monitorcontrol.*" # MonitorControl
|
|
1252
|
+
"com.bresink.system-toolkit.*" # TinkerTool System
|
|
1253
|
+
"com.mediaatelier.MenuMeters" # MenuMeters
|
|
1254
|
+
"com.activity-indicator.app" # Activity Indicator
|
|
1255
|
+
"net.cindori.sensei" # Sensei
|
|
1256
|
+
|
|
1257
|
+
# ============================================================================
|
|
1258
|
+
# Window Management & Productivity
|
|
1259
|
+
# ============================================================================
|
|
1260
|
+
"com.macitbetter.*" # BetterTouchTool, BetterSnapTool
|
|
1261
|
+
"com.hegenberg.*" # BetterTouchTool legacy
|
|
1262
|
+
"com.manytricks.*" # Moom, Witch, Name Mangler, Resolutionator
|
|
1263
|
+
"com.divisiblebyzero.*" # Spectacle
|
|
1264
|
+
"com.koingdev.*" # Koingg apps
|
|
1265
|
+
"com.if.Amphetamine" # Amphetamine
|
|
1266
|
+
"com.lwouis.alt-tab-macos" # AltTab
|
|
1267
|
+
"net.matthewpalmer.Vanilla" # Vanilla
|
|
1268
|
+
"com.lightheadsw.Caffeine" # Caffeine
|
|
1269
|
+
"com.contextual.Contexts" # Contexts
|
|
1270
|
+
"com.amethyst.Amethyst" # Amethyst
|
|
1271
|
+
"com.knollsoft.Rectangle" # Rectangle
|
|
1272
|
+
"com.knollsoft.Hookshot" # Hookshot
|
|
1273
|
+
"com.surteesstudios.Bartender" # Bartender
|
|
1274
|
+
"com.gaosun.eul" # eul (system monitor)
|
|
1275
|
+
"com.pointum.hazeover" # HazeOver
|
|
1276
|
+
|
|
1277
|
+
# ============================================================================
|
|
1278
|
+
# Launcher & Automation
|
|
1279
|
+
# ============================================================================
|
|
1280
|
+
"com.runningwithcrayons.Alfred" # Alfred
|
|
1281
|
+
"com.raycast.macos" # Raycast
|
|
1282
|
+
"com.blacktree.Quicksilver" # Quicksilver
|
|
1283
|
+
"com.stairways.keyboardmaestro.*" # Keyboard Maestro
|
|
1284
|
+
"com.manytricks.Butler" # Butler
|
|
1285
|
+
"com.happenapps.Quitter" # Quitter
|
|
1286
|
+
"com.pilotmoon.scroll-reverser" # Scroll Reverser
|
|
1287
|
+
"org.pqrs.Karabiner-Elements" # Karabiner-Elements
|
|
1288
|
+
"com.apple.Automator" # Automator (system, but keep user workflows)
|
|
1289
|
+
|
|
1290
|
+
# ============================================================================
|
|
1291
|
+
# Note-Taking & Documentation
|
|
1292
|
+
# ============================================================================
|
|
1293
|
+
"com.bear-writer.*" # Bear
|
|
1294
|
+
"com.typora.*" # Typora
|
|
1295
|
+
"com.ulyssesapp.*" # Ulysses
|
|
1296
|
+
"com.literatureandlatte.*" # Scrivener
|
|
1297
|
+
"com.dayoneapp.*" # Day One
|
|
1298
|
+
"notion.id" # Notion
|
|
1299
|
+
"md.obsidian" # Obsidian
|
|
1300
|
+
"com.logseq.logseq" # Logseq
|
|
1301
|
+
"com.evernote.Evernote" # Evernote
|
|
1302
|
+
"com.onenote.mac" # OneNote
|
|
1303
|
+
"com.omnigroup.OmniOutliner*" # OmniOutliner
|
|
1304
|
+
"net.shinyfrog.bear" # Bear legacy
|
|
1305
|
+
"com.goodnotes.GoodNotes" # GoodNotes
|
|
1306
|
+
"com.marginnote.MarginNote*" # MarginNote
|
|
1307
|
+
"com.roamresearch.*" # Roam Research
|
|
1308
|
+
"com.reflect.ReflectApp" # Reflect
|
|
1309
|
+
"com.inkdrop.*" # Inkdrop
|
|
1310
|
+
|
|
1311
|
+
# ============================================================================
|
|
1312
|
+
# Design & Creative Tools
|
|
1313
|
+
# ============================================================================
|
|
1314
|
+
"com.adobe.*" # Adobe Creative Suite
|
|
1315
|
+
"com.bohemiancoding.*" # Sketch
|
|
1316
|
+
"com.figma.*" # Figma
|
|
1317
|
+
"com.framerx.*" # Framer
|
|
1318
|
+
"com.zeplin.*" # Zeplin
|
|
1319
|
+
"com.invisionapp.*" # InVision
|
|
1320
|
+
"com.principle.*" # Principle
|
|
1321
|
+
"com.pixelmatorteam.*" # Pixelmator
|
|
1322
|
+
"com.affinitydesigner.*" # Affinity Designer
|
|
1323
|
+
"com.affinityphoto.*" # Affinity Photo
|
|
1324
|
+
"com.affinitypublisher.*" # Affinity Publisher
|
|
1325
|
+
"com.linearity.curve" # Linearity Curve
|
|
1326
|
+
"com.canva.CanvaDesktop" # Canva
|
|
1327
|
+
"com.maxon.cinema4d" # Cinema 4D
|
|
1328
|
+
"com.autodesk.*" # Autodesk products
|
|
1329
|
+
"com.sketchup.*" # SketchUp
|
|
1330
|
+
|
|
1331
|
+
# ============================================================================
|
|
1332
|
+
# Communication & Collaboration
|
|
1333
|
+
# ============================================================================
|
|
1334
|
+
"com.tencent.xinWeChat" # WeChat (Chinese users)
|
|
1335
|
+
"com.tencent.qq" # QQ
|
|
1336
|
+
"com.alibaba.DingTalkMac" # DingTalk
|
|
1337
|
+
"com.alibaba.AliLang.osx" # AliLang (retain login/config data)
|
|
1338
|
+
"com.alibaba.alilang3.osx.ShipIt" # AliLang updater component
|
|
1339
|
+
"com.alibaba.AlilangMgr.QueryNetworkInfo" # AliLang network helper
|
|
1340
|
+
"us.zoom.xos" # Zoom
|
|
1341
|
+
"com.microsoft.teams*" # Microsoft Teams
|
|
1342
|
+
"com.slack.Slack" # Slack
|
|
1343
|
+
"com.hnc.Discord" # Discord
|
|
1344
|
+
"org.telegram.desktop" # Telegram
|
|
1345
|
+
"ru.keepcoder.Telegram" # Telegram legacy
|
|
1346
|
+
"net.whatsapp.WhatsApp" # WhatsApp
|
|
1347
|
+
"com.skype.skype" # Skype
|
|
1348
|
+
"com.cisco.webexmeetings" # Webex
|
|
1349
|
+
"com.ringcentral.RingCentral" # RingCentral
|
|
1350
|
+
"com.readdle.smartemail-Mac" # Spark Email
|
|
1351
|
+
"com.airmail.*" # Airmail
|
|
1352
|
+
"com.postbox-inc.postbox" # Postbox
|
|
1353
|
+
"com.tinyspeck.slackmacgap" # Slack legacy
|
|
1354
|
+
|
|
1355
|
+
# ============================================================================
|
|
1356
|
+
# Task Management & Productivity
|
|
1357
|
+
# ============================================================================
|
|
1358
|
+
"com.omnigroup.OmniFocus*" # OmniFocus
|
|
1359
|
+
"com.culturedcode.*" # Things
|
|
1360
|
+
"com.todoist.*" # Todoist
|
|
1361
|
+
"com.any.do.*" # Any.do
|
|
1362
|
+
"com.ticktick.*" # TickTick
|
|
1363
|
+
"com.microsoft.to-do" # Microsoft To Do
|
|
1364
|
+
"com.trello.trello" # Trello
|
|
1365
|
+
"com.asana.nativeapp" # Asana
|
|
1366
|
+
"com.clickup.*" # ClickUp
|
|
1367
|
+
"com.monday.desktop" # Monday.com
|
|
1368
|
+
"com.airtable.airtable" # Airtable
|
|
1369
|
+
"com.notion.id" # Notion (also note-taking)
|
|
1370
|
+
"com.linear.linear" # Linear
|
|
1371
|
+
|
|
1372
|
+
# ============================================================================
|
|
1373
|
+
# File Transfer & Sync
|
|
1374
|
+
# ============================================================================
|
|
1375
|
+
"com.panic.transmit*" # Transmit (FTP/SFTP)
|
|
1376
|
+
"com.binarynights.ForkLift*" # ForkLift
|
|
1377
|
+
"com.noodlesoft.Hazel" # Hazel
|
|
1378
|
+
"com.cyberduck.Cyberduck" # Cyberduck
|
|
1379
|
+
"io.filezilla.FileZilla" # FileZilla
|
|
1380
|
+
"com.apple.Xcode.CloudDocuments" # Xcode Cloud Documents
|
|
1381
|
+
"com.synology.*" # Synology apps
|
|
1382
|
+
|
|
1383
|
+
# ============================================================================
|
|
1384
|
+
# Screenshot & Recording
|
|
1385
|
+
# ============================================================================
|
|
1386
|
+
"com.cleanshot.*" # CleanShot X
|
|
1387
|
+
"com.xnipapp.xnip" # Xnip
|
|
1388
|
+
"com.reincubate.camo" # Camo
|
|
1389
|
+
"com.tunabellysoftware.ScreenFloat" # ScreenFloat
|
|
1390
|
+
"net.telestream.screenflow*" # ScreenFlow
|
|
1391
|
+
"com.techsmith.snagit*" # Snagit
|
|
1392
|
+
"com.techsmith.camtasia*" # Camtasia
|
|
1393
|
+
"com.obsidianapp.screenrecorder" # Screen Recorder
|
|
1394
|
+
"com.kap.Kap" # Kap
|
|
1395
|
+
"com.getkap.*" # Kap legacy
|
|
1396
|
+
"com.linebreak.CloudApp" # CloudApp
|
|
1397
|
+
"com.droplr.droplr-mac" # Droplr
|
|
1398
|
+
|
|
1399
|
+
# ============================================================================
|
|
1400
|
+
# Media & Entertainment
|
|
1401
|
+
# ============================================================================
|
|
1402
|
+
"com.spotify.client" # Spotify
|
|
1403
|
+
"com.apple.Music" # Apple Music
|
|
1404
|
+
"com.apple.podcasts" # Apple Podcasts
|
|
1405
|
+
"com.apple.FinalCutPro" # Final Cut Pro
|
|
1406
|
+
"com.apple.Motion" # Motion
|
|
1407
|
+
"com.apple.Compressor" # Compressor
|
|
1408
|
+
"com.blackmagic-design.*" # DaVinci Resolve
|
|
1409
|
+
"com.colliderli.iina" # IINA
|
|
1410
|
+
"org.videolan.vlc" # VLC
|
|
1411
|
+
"io.mpv" # MPV
|
|
1412
|
+
"com.noodlesoft.Hazel" # Hazel (automation)
|
|
1413
|
+
"tv.plex.player.desktop" # Plex
|
|
1414
|
+
"com.netease.163music" # NetEase Music
|
|
1415
|
+
|
|
1416
|
+
# ============================================================================
|
|
1417
|
+
# License Management & App Stores
|
|
1418
|
+
# ============================================================================
|
|
1419
|
+
"com.paddle.Paddle*" # Paddle (license management)
|
|
1420
|
+
"com.setapp.DesktopClient" # Setapp
|
|
1421
|
+
"com.devmate.*" # DevMate (license framework)
|
|
1422
|
+
"org.sparkle-project.Sparkle" # Sparkle (update framework)
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
# Legacy function - preserved for backward compatibility
|
|
1426
|
+
# Use should_protect_from_uninstall() or should_protect_data() instead
|
|
1427
|
+
readonly PRESERVED_BUNDLE_PATTERNS=("${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}")
|
|
1428
|
+
should_preserve_bundle() {
|
|
1429
|
+
local bundle_id="$1"
|
|
1430
|
+
for pattern in "${PRESERVED_BUNDLE_PATTERNS[@]}"; do
|
|
1431
|
+
# Use case for safer glob matching
|
|
1432
|
+
case "$bundle_id" in
|
|
1433
|
+
"$pattern") return 0 ;;
|
|
1434
|
+
esac
|
|
1435
|
+
done
|
|
1436
|
+
return 1
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
# Check if app is a system component that should never be uninstalled
|
|
1440
|
+
should_protect_from_uninstall() {
|
|
1441
|
+
local bundle_id="$1"
|
|
1442
|
+
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do
|
|
1443
|
+
# Use case for safer glob matching
|
|
1444
|
+
case "$bundle_id" in
|
|
1445
|
+
"$pattern") return 0 ;;
|
|
1446
|
+
esac
|
|
1447
|
+
done
|
|
1448
|
+
return 1
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
# Check if app data should be protected during cleanup (but app can be uninstalled)
|
|
1452
|
+
should_protect_data() {
|
|
1453
|
+
local bundle_id="$1"
|
|
1454
|
+
# Protect both system critical and data protected bundles during cleanup
|
|
1455
|
+
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do
|
|
1456
|
+
# Use case for safer glob matching
|
|
1457
|
+
case "$bundle_id" in
|
|
1458
|
+
"$pattern") return 0 ;;
|
|
1459
|
+
esac
|
|
1460
|
+
done
|
|
1461
|
+
return 1
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
# Find and list app-related files (consolidated from duplicates)
|
|
1465
|
+
find_app_files() {
|
|
1466
|
+
local bundle_id="$1"
|
|
1467
|
+
local app_name="$2"
|
|
1468
|
+
local -a files_to_clean=()
|
|
1469
|
+
|
|
1470
|
+
# ============================================================================
|
|
1471
|
+
# User-level files (no sudo required)
|
|
1472
|
+
# ============================================================================
|
|
1473
|
+
|
|
1474
|
+
# Application Support
|
|
1475
|
+
[[ -d ~/Library/Application\ Support/"$app_name" ]] && files_to_clean+=("$HOME/Library/Application Support/$app_name")
|
|
1476
|
+
[[ -d ~/Library/Application\ Support/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Application Support/$bundle_id")
|
|
1477
|
+
|
|
1478
|
+
# Caches
|
|
1479
|
+
[[ -d ~/Library/Caches/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Caches/$bundle_id")
|
|
1480
|
+
[[ -d ~/Library/Caches/"$app_name" ]] && files_to_clean+=("$HOME/Library/Caches/$app_name")
|
|
1481
|
+
|
|
1482
|
+
# Preferences
|
|
1483
|
+
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
|
1484
|
+
while IFS= read -r -d '' pref; do
|
|
1485
|
+
files_to_clean+=("$pref")
|
|
1486
|
+
done < <(find ~/Library/Preferences/ByHost \( -name "$bundle_id*.plist" \) -print0 2> /dev/null)
|
|
1487
|
+
|
|
1488
|
+
# Logs
|
|
1489
|
+
[[ -d ~/Library/Logs/"$app_name" ]] && files_to_clean+=("$HOME/Library/Logs/$app_name")
|
|
1490
|
+
[[ -d ~/Library/Logs/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Logs/$bundle_id")
|
|
1491
|
+
|
|
1492
|
+
# Saved Application State
|
|
1493
|
+
[[ -d ~/Library/Saved\ Application\ State/"$bundle_id".savedState ]] && files_to_clean+=("$HOME/Library/Saved Application State/$bundle_id.savedState")
|
|
1494
|
+
|
|
1495
|
+
# Containers (sandboxed apps)
|
|
1496
|
+
[[ -d ~/Library/Containers/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Containers/$bundle_id")
|
|
1497
|
+
|
|
1498
|
+
# Group Containers
|
|
1499
|
+
while IFS= read -r -d '' container; do
|
|
1500
|
+
files_to_clean+=("$container")
|
|
1501
|
+
done < <(find ~/Library/Group\ Containers -type d \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
|
1502
|
+
|
|
1503
|
+
# WebKit data
|
|
1504
|
+
[[ -d ~/Library/WebKit/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/WebKit/$bundle_id")
|
|
1505
|
+
[[ -d ~/Library/WebKit/com.apple.WebKit.WebContent/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/WebKit/com.apple.WebKit.WebContent/$bundle_id")
|
|
1506
|
+
|
|
1507
|
+
# HTTP Storage
|
|
1508
|
+
[[ -d ~/Library/HTTPStorages/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/HTTPStorages/$bundle_id")
|
|
1509
|
+
|
|
1510
|
+
# Cookies
|
|
1511
|
+
[[ -f ~/Library/Cookies/"$bundle_id".binarycookies ]] && files_to_clean+=("$HOME/Library/Cookies/$bundle_id.binarycookies")
|
|
1512
|
+
|
|
1513
|
+
# Launch Agents (user-level)
|
|
1514
|
+
[[ -f ~/Library/LaunchAgents/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/LaunchAgents/$bundle_id.plist")
|
|
1515
|
+
|
|
1516
|
+
# Application Scripts
|
|
1517
|
+
[[ -d ~/Library/Application\ Scripts/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Application Scripts/$bundle_id")
|
|
1518
|
+
|
|
1519
|
+
# Services
|
|
1520
|
+
[[ -d ~/Library/Services/"$app_name".workflow ]] && files_to_clean+=("$HOME/Library/Services/$app_name.workflow")
|
|
1521
|
+
|
|
1522
|
+
# Internet Plug-Ins
|
|
1523
|
+
while IFS= read -r -d '' plugin; do
|
|
1524
|
+
files_to_clean+=("$plugin")
|
|
1525
|
+
done < <(find ~/Library/Internet\ Plug-Ins \( -name "$bundle_id*" -o -name "$app_name*" \) -print0 2> /dev/null)
|
|
1526
|
+
|
|
1527
|
+
# QuickLook Plugins
|
|
1528
|
+
[[ -d ~/Library/QuickLook/"$app_name".qlgenerator ]] && files_to_clean+=("$HOME/Library/QuickLook/$app_name.qlgenerator")
|
|
1529
|
+
|
|
1530
|
+
# Preference Panes
|
|
1531
|
+
[[ -d ~/Library/PreferencePanes/"$app_name".prefPane ]] && files_to_clean+=("$HOME/Library/PreferencePanes/$app_name.prefPane")
|
|
1532
|
+
|
|
1533
|
+
# Screen Savers
|
|
1534
|
+
[[ -d ~/Library/Screen\ Savers/"$app_name".saver ]] && files_to_clean+=("$HOME/Library/Screen Savers/$app_name.saver")
|
|
1535
|
+
|
|
1536
|
+
# Frameworks
|
|
1537
|
+
[[ -d ~/Library/Frameworks/"$app_name".framework ]] && files_to_clean+=("$HOME/Library/Frameworks/$app_name.framework")
|
|
1538
|
+
|
|
1539
|
+
# CoreData
|
|
1540
|
+
while IFS= read -r -d '' coredata; do
|
|
1541
|
+
files_to_clean+=("$coredata")
|
|
1542
|
+
done < <(find ~/Library/CoreData \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2> /dev/null)
|
|
1543
|
+
|
|
1544
|
+
# Autosave Information
|
|
1545
|
+
[[ -d ~/Library/Autosave\ Information/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Autosave Information/$bundle_id")
|
|
1546
|
+
|
|
1547
|
+
# Contextual Menu Items
|
|
1548
|
+
[[ -d ~/Library/Contextual\ Menu\ Items/"$app_name".plugin ]] && files_to_clean+=("$HOME/Library/Contextual Menu Items/$app_name.plugin")
|
|
1549
|
+
|
|
1550
|
+
# Receipts (user-level)
|
|
1551
|
+
while IFS= read -r -d '' receipt; do
|
|
1552
|
+
files_to_clean+=("$receipt")
|
|
1553
|
+
done < <(find ~/Library/Receipts \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2> /dev/null)
|
|
1554
|
+
|
|
1555
|
+
# Spotlight Plugins
|
|
1556
|
+
[[ -d ~/Library/Spotlight/"$app_name".mdimporter ]] && files_to_clean+=("$HOME/Library/Spotlight/$app_name.mdimporter")
|
|
1557
|
+
|
|
1558
|
+
# Scripting Additions
|
|
1559
|
+
while IFS= read -r -d '' scripting; do
|
|
1560
|
+
files_to_clean+=("$scripting")
|
|
1561
|
+
done < <(find ~/Library/ScriptingAdditions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1562
|
+
|
|
1563
|
+
# Color Pickers
|
|
1564
|
+
[[ -d ~/Library/ColorPickers/"$app_name".colorPicker ]] && files_to_clean+=("$HOME/Library/ColorPickers/$app_name.colorPicker")
|
|
1565
|
+
|
|
1566
|
+
# Quartz Compositions
|
|
1567
|
+
while IFS= read -r -d '' composition; do
|
|
1568
|
+
files_to_clean+=("$composition")
|
|
1569
|
+
done < <(find ~/Library/Compositions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1570
|
+
|
|
1571
|
+
# Address Book Plug-Ins
|
|
1572
|
+
while IFS= read -r -d '' plugin; do
|
|
1573
|
+
files_to_clean+=("$plugin")
|
|
1574
|
+
done < <(find ~/Library/Address\ Book\ Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1575
|
+
|
|
1576
|
+
# Mail Bundles
|
|
1577
|
+
while IFS= read -r -d '' bundle; do
|
|
1578
|
+
files_to_clean+=("$bundle")
|
|
1579
|
+
done < <(find ~/Library/Mail/Bundles \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1580
|
+
|
|
1581
|
+
# Input Managers (app-specific only)
|
|
1582
|
+
while IFS= read -r -d '' manager; do
|
|
1583
|
+
files_to_clean+=("$manager")
|
|
1584
|
+
done < <(find ~/Library/InputManagers \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1585
|
+
|
|
1586
|
+
# Custom Sounds
|
|
1587
|
+
while IFS= read -r -d '' sound; do
|
|
1588
|
+
files_to_clean+=("$sound")
|
|
1589
|
+
done < <(find ~/Library/Sounds \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1590
|
+
|
|
1591
|
+
# Plugins
|
|
1592
|
+
while IFS= read -r -d '' plugin; do
|
|
1593
|
+
files_to_clean+=("$plugin")
|
|
1594
|
+
done < <(find ~/Library/Plugins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1595
|
+
|
|
1596
|
+
# Private Frameworks
|
|
1597
|
+
while IFS= read -r -d '' framework; do
|
|
1598
|
+
files_to_clean+=("$framework")
|
|
1599
|
+
done < <(find ~/Library/PrivateFrameworks \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1600
|
+
|
|
1601
|
+
# Only print if array has elements to avoid unbound variable error
|
|
1602
|
+
if [[ ${#files_to_clean[@]} -gt 0 ]]; then
|
|
1603
|
+
printf '%s\n' "${files_to_clean[@]}"
|
|
1604
|
+
fi
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
# Find system-level app files (requires sudo)
|
|
1608
|
+
find_app_system_files() {
|
|
1609
|
+
local bundle_id="$1"
|
|
1610
|
+
local app_name="$2"
|
|
1611
|
+
local -a system_files=()
|
|
1612
|
+
|
|
1613
|
+
# System Application Support
|
|
1614
|
+
[[ -d /Library/Application\ Support/"$app_name" ]] && system_files+=("/Library/Application Support/$app_name")
|
|
1615
|
+
[[ -d /Library/Application\ Support/"$bundle_id" ]] && system_files+=("/Library/Application Support/$bundle_id")
|
|
1616
|
+
|
|
1617
|
+
# System Launch Agents
|
|
1618
|
+
[[ -f /Library/LaunchAgents/"$bundle_id".plist ]] && system_files+=("/Library/LaunchAgents/$bundle_id.plist")
|
|
1619
|
+
|
|
1620
|
+
# System Launch Daemons
|
|
1621
|
+
[[ -f /Library/LaunchDaemons/"$bundle_id".plist ]] && system_files+=("/Library/LaunchDaemons/$bundle_id.plist")
|
|
1622
|
+
|
|
1623
|
+
# Privileged Helper Tools
|
|
1624
|
+
while IFS= read -r -d '' helper; do
|
|
1625
|
+
system_files+=("$helper")
|
|
1626
|
+
done < <(find /Library/PrivilegedHelperTools \( -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1627
|
+
|
|
1628
|
+
# System Preferences
|
|
1629
|
+
[[ -f /Library/Preferences/"$bundle_id".plist ]] && system_files+=("/Library/Preferences/$bundle_id.plist")
|
|
1630
|
+
|
|
1631
|
+
# Installation Receipts
|
|
1632
|
+
while IFS= read -r -d '' receipt; do
|
|
1633
|
+
system_files+=("$receipt")
|
|
1634
|
+
done < <(find /private/var/db/receipts \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
|
1635
|
+
|
|
1636
|
+
# System Logs
|
|
1637
|
+
[[ -d /Library/Logs/"$app_name" ]] && system_files+=("/Library/Logs/$app_name")
|
|
1638
|
+
[[ -d /Library/Logs/"$bundle_id" ]] && system_files+=("/Library/Logs/$bundle_id")
|
|
1639
|
+
|
|
1640
|
+
# System Frameworks
|
|
1641
|
+
[[ -d /Library/Frameworks/"$app_name".framework ]] && system_files+=("/Library/Frameworks/$app_name.framework")
|
|
1642
|
+
|
|
1643
|
+
# System Internet Plug-Ins
|
|
1644
|
+
while IFS= read -r -d '' plugin; do
|
|
1645
|
+
system_files+=("$plugin")
|
|
1646
|
+
done < <(find /Library/Internet\ Plug-Ins \( -name "$bundle_id*" -o -name "$app_name*" \) -print0 2> /dev/null)
|
|
1647
|
+
|
|
1648
|
+
# System QuickLook Plugins
|
|
1649
|
+
[[ -d /Library/QuickLook/"$app_name".qlgenerator ]] && system_files+=("/Library/QuickLook/$app_name.qlgenerator")
|
|
1650
|
+
|
|
1651
|
+
# System Receipts
|
|
1652
|
+
while IFS= read -r -d '' receipt; do
|
|
1653
|
+
system_files+=("$receipt")
|
|
1654
|
+
done < <(find /Library/Receipts \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2> /dev/null)
|
|
1655
|
+
|
|
1656
|
+
# System Spotlight Plugins
|
|
1657
|
+
[[ -d /Library/Spotlight/"$app_name".mdimporter ]] && system_files+=("/Library/Spotlight/$app_name.mdimporter")
|
|
1658
|
+
|
|
1659
|
+
# System Scripting Additions
|
|
1660
|
+
while IFS= read -r -d '' scripting; do
|
|
1661
|
+
system_files+=("$scripting")
|
|
1662
|
+
done < <(find /Library/ScriptingAdditions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1663
|
+
|
|
1664
|
+
# System Color Pickers
|
|
1665
|
+
[[ -d /Library/ColorPickers/"$app_name".colorPicker ]] && system_files+=("/Library/ColorPickers/$app_name.colorPicker")
|
|
1666
|
+
|
|
1667
|
+
# System Quartz Compositions
|
|
1668
|
+
while IFS= read -r -d '' composition; do
|
|
1669
|
+
system_files+=("$composition")
|
|
1670
|
+
done < <(find /Library/Compositions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1671
|
+
|
|
1672
|
+
# System Address Book Plug-Ins
|
|
1673
|
+
while IFS= read -r -d '' plugin; do
|
|
1674
|
+
system_files+=("$plugin")
|
|
1675
|
+
done < <(find /Library/Address\ Book\ Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1676
|
+
|
|
1677
|
+
# System Mail Bundles
|
|
1678
|
+
while IFS= read -r -d '' bundle; do
|
|
1679
|
+
system_files+=("$bundle")
|
|
1680
|
+
done < <(find /Library/Mail/Bundles \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1681
|
+
|
|
1682
|
+
# System Input Managers
|
|
1683
|
+
while IFS= read -r -d '' manager; do
|
|
1684
|
+
system_files+=("$manager")
|
|
1685
|
+
done < <(find /Library/InputManagers \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1686
|
+
|
|
1687
|
+
# System Sounds
|
|
1688
|
+
while IFS= read -r -d '' sound; do
|
|
1689
|
+
system_files+=("$sound")
|
|
1690
|
+
done < <(find /Library/Sounds \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1691
|
+
|
|
1692
|
+
# System Contextual Menu Items
|
|
1693
|
+
while IFS= read -r -d '' item; do
|
|
1694
|
+
system_files+=("$item")
|
|
1695
|
+
done < <(find /Library/Contextual\ Menu\ Items \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
1696
|
+
|
|
1697
|
+
# System Preference Panes
|
|
1698
|
+
[[ -d /Library/PreferencePanes/"$app_name".prefPane ]] && system_files+=("/Library/PreferencePanes/$app_name.prefPane")
|
|
1699
|
+
|
|
1700
|
+
# System Screen Savers
|
|
1701
|
+
[[ -d /Library/Screen\ Savers/"$app_name".saver ]] && system_files+=("/Library/Screen Savers/$app_name.saver")
|
|
1702
|
+
|
|
1703
|
+
# System Caches
|
|
1704
|
+
[[ -d /Library/Caches/"$bundle_id" ]] && system_files+=("/Library/Caches/$bundle_id")
|
|
1705
|
+
[[ -d /Library/Caches/"$app_name" ]] && system_files+=("/Library/Caches/$app_name")
|
|
1706
|
+
|
|
1707
|
+
# Only print if array has elements
|
|
1708
|
+
if [[ ${#system_files[@]} -gt 0 ]]; then
|
|
1709
|
+
printf '%s\n' "${system_files[@]}"
|
|
1710
|
+
fi
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
# Calculate total size of files (consolidated from duplicates)
|
|
1714
|
+
calculate_total_size() {
|
|
1715
|
+
local files="$1"
|
|
1716
|
+
local total_kb=0
|
|
1717
|
+
|
|
1718
|
+
while IFS= read -r file; do
|
|
1719
|
+
if [[ -n "$file" && -e "$file" ]]; then
|
|
1720
|
+
local size_kb
|
|
1721
|
+
size_kb=$(du -sk "$file" 2> /dev/null | awk '{print $1}' || echo "0")
|
|
1722
|
+
((total_kb += size_kb))
|
|
1723
|
+
fi
|
|
1724
|
+
done <<< "$files"
|
|
1725
|
+
|
|
1726
|
+
echo "$total_kb"
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
# Get normalized brand name (bash 3.2 compatible using case statement)
|
|
1730
|
+
get_brand_name() {
|
|
1731
|
+
local name="$1"
|
|
1732
|
+
|
|
1733
|
+
# Brand name mapping for better user recognition
|
|
1734
|
+
case "$name" in
|
|
1735
|
+
"qiyimac" | "爱奇艺") echo "iQiyi" ;;
|
|
1736
|
+
"wechat" | "微信") echo "WeChat" ;;
|
|
1737
|
+
"QQ") echo "QQ" ;;
|
|
1738
|
+
"VooV Meeting" | "腾讯会议") echo "VooV Meeting" ;;
|
|
1739
|
+
"dingtalk" | "钉钉") echo "DingTalk" ;;
|
|
1740
|
+
"NeteaseMusic" | "网易云音乐") echo "NetEase Music" ;;
|
|
1741
|
+
"BaiduNetdisk" | "百度网盘") echo "Baidu NetDisk" ;;
|
|
1742
|
+
"alipay" | "支付宝") echo "Alipay" ;;
|
|
1743
|
+
"taobao" | "淘宝") echo "Taobao" ;;
|
|
1744
|
+
"futunn" | "富途牛牛") echo "Futu NiuNiu" ;;
|
|
1745
|
+
"tencent lemon" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;;
|
|
1746
|
+
"keynote" | "Keynote") echo "Keynote" ;;
|
|
1747
|
+
"pages" | "Pages") echo "Pages" ;;
|
|
1748
|
+
"numbers" | "Numbers") echo "Numbers" ;;
|
|
1749
|
+
*) echo "$name" ;; # Return original if no mapping found
|
|
1750
|
+
esac
|
|
1751
|
+
}
|