mirror of
https://github.com/tw93/Mole.git
synced 2026-02-12 21:35:14 +00:00
Code support format detection
This commit is contained in:
342
bin/analyze.sh
342
bin/analyze.sh
@@ -19,9 +19,9 @@ source "$LIB_DIR/common.sh"
|
||||
# Constants
|
||||
readonly CACHE_DIR="${HOME}/.config/mole/cache"
|
||||
readonly TEMP_PREFIX="/tmp/mole_analyze_$$"
|
||||
readonly MIN_LARGE_FILE_SIZE="1000000000" # 1GB
|
||||
readonly MIN_MEDIUM_FILE_SIZE="100000000" # 100MB
|
||||
readonly MIN_SMALL_FILE_SIZE="10000000" # 10MB
|
||||
readonly MIN_LARGE_FILE_SIZE="1000000000" # 1GB
|
||||
readonly MIN_MEDIUM_FILE_SIZE="100000000" # 100MB
|
||||
readonly MIN_SMALL_FILE_SIZE="10000000" # 10MB
|
||||
|
||||
# Emoji badges for list displays only
|
||||
readonly BADGE_DIR="🍞"
|
||||
@@ -42,16 +42,16 @@ declare CURRENT_DEPTH=1
|
||||
|
||||
# UI State
|
||||
declare CURSOR_POS=0
|
||||
declare SORT_MODE="size" # size, name, time
|
||||
declare VIEW_MODE="overview" # overview, detail, files
|
||||
declare SORT_MODE="size" # size, name, time
|
||||
declare VIEW_MODE="overview" # overview, detail, files
|
||||
|
||||
# Cleanup on exit
|
||||
cleanup() {
|
||||
show_cursor
|
||||
# Cleanup temp files using glob pattern (analyze uses many temp files)
|
||||
rm -f "$TEMP_PREFIX"* 2>/dev/null || true
|
||||
if [[ -n "$SCAN_PID" ]] && kill -0 "$SCAN_PID" 2>/dev/null; then
|
||||
kill "$SCAN_PID" 2>/dev/null || true
|
||||
rm -f "$TEMP_PREFIX"* 2> /dev/null || true
|
||||
if [[ -n "$SCAN_PID" ]] && kill -0 "$SCAN_PID" 2> /dev/null; then
|
||||
kill "$SCAN_PID" 2> /dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ scan_large_files() {
|
||||
local target_path="$1"
|
||||
local output_file="$2"
|
||||
|
||||
if ! command -v mdfind &>/dev/null; then
|
||||
if ! command -v mdfind &> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -75,10 +75,10 @@ scan_large_files() {
|
||||
while IFS= read -r file; do
|
||||
if [[ -f "$file" ]]; then
|
||||
local size
|
||||
size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||
size=$(stat -f%z "$file" 2> /dev/null || echo "0")
|
||||
echo "$size|$file"
|
||||
fi
|
||||
done < <(mdfind -onlyin "$target_path" "kMDItemFSSize > $MIN_LARGE_FILE_SIZE" 2>/dev/null) | \
|
||||
done < <(mdfind -onlyin "$target_path" "kMDItemFSSize > $MIN_LARGE_FILE_SIZE" 2> /dev/null) |
|
||||
sort -t'|' -k1 -rn > "$output_file"
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ scan_medium_files() {
|
||||
local target_path="$1"
|
||||
local output_file="$2"
|
||||
|
||||
if ! command -v mdfind &>/dev/null; then
|
||||
if ! command -v mdfind &> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -95,11 +95,11 @@ scan_medium_files() {
|
||||
while IFS= read -r file; do
|
||||
if [[ -f "$file" ]]; then
|
||||
local size
|
||||
size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||
size=$(stat -f%z "$file" 2> /dev/null || echo "0")
|
||||
echo "$size|$file"
|
||||
fi
|
||||
done < <(mdfind -onlyin "$target_path" \
|
||||
"kMDItemFSSize > $MIN_MEDIUM_FILE_SIZE && kMDItemFSSize < $MIN_LARGE_FILE_SIZE" 2>/dev/null) | \
|
||||
"kMDItemFSSize > $MIN_MEDIUM_FILE_SIZE && kMDItemFSSize < $MIN_LARGE_FILE_SIZE" 2> /dev/null) |
|
||||
sort -t'|' -k1 -rn > "$output_file"
|
||||
}
|
||||
|
||||
@@ -110,18 +110,18 @@ scan_directories() {
|
||||
local depth="${3:-1}"
|
||||
|
||||
# Check if we can use parallel processing
|
||||
if command -v xargs &>/dev/null && [[ $depth -eq 1 ]]; then
|
||||
if command -v xargs &> /dev/null && [[ $depth -eq 1 ]]; then
|
||||
# Fast parallel scan for depth 1
|
||||
find "$target_path" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | \
|
||||
xargs -0 -P 4 -I {} du -sk {} 2>/dev/null | \
|
||||
sort -rn | \
|
||||
find "$target_path" -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null |
|
||||
xargs -0 -P 4 -I {} du -sk {} 2> /dev/null |
|
||||
sort -rn |
|
||||
while IFS=$'\t' read -r size path; do
|
||||
echo "$((size * 1024))|$path"
|
||||
done > "$output_file"
|
||||
else
|
||||
# Standard du scan
|
||||
du -d "$depth" -k "$target_path" 2>/dev/null | \
|
||||
sort -rn | \
|
||||
du -d "$depth" -k "$target_path" 2> /dev/null |
|
||||
sort -rn |
|
||||
while IFS=$'\t' read -r size path; do
|
||||
# Skip if path is the target itself at depth > 0
|
||||
if [[ "$path" != "$target_path" ]]; then
|
||||
@@ -161,21 +161,21 @@ aggregate_by_directory() {
|
||||
get_cache_file() {
|
||||
local target_path="$1"
|
||||
local path_hash
|
||||
path_hash=$(echo "$target_path" | md5 2>/dev/null || echo "$target_path" | shasum | cut -d' ' -f1)
|
||||
path_hash=$(echo "$target_path" | md5 2> /dev/null || echo "$target_path" | shasum | cut -d' ' -f1)
|
||||
echo "$CACHE_DIR/scan_${path_hash}.cache"
|
||||
}
|
||||
|
||||
# Check if cache is valid (less than 1 hour old)
|
||||
is_cache_valid() {
|
||||
local cache_file="$1"
|
||||
local max_age="${2:-3600}" # Default 1 hour
|
||||
local max_age="${2:-3600}" # Default 1 hour
|
||||
|
||||
if [[ ! -f "$cache_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local cache_age
|
||||
cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
||||
cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2> /dev/null || echo 0)))
|
||||
if [[ $cache_age -lt $max_age ]]; then
|
||||
return 0
|
||||
fi
|
||||
@@ -192,7 +192,7 @@ save_to_cache() {
|
||||
local temp_agg="$TEMP_PREFIX.agg"
|
||||
|
||||
# Create cache directory
|
||||
mkdir -p "$(dirname "$cache_file")" 2>/dev/null || return 1
|
||||
mkdir -p "$(dirname "$cache_file")" 2> /dev/null || return 1
|
||||
|
||||
# Bundle all scan results into cache file
|
||||
{
|
||||
@@ -204,7 +204,7 @@ save_to_cache() {
|
||||
[[ -f "$temp_dirs" ]] && cat "$temp_dirs"
|
||||
echo "### AGG ###"
|
||||
[[ -f "$temp_agg" ]] && cat "$temp_agg"
|
||||
} > "$cache_file" 2>/dev/null
|
||||
} > "$cache_file" 2> /dev/null
|
||||
}
|
||||
|
||||
# Load scan results from cache
|
||||
@@ -283,7 +283,7 @@ perform_scan() {
|
||||
)
|
||||
local msg_idx=0
|
||||
|
||||
while kill -0 "$SCAN_PID" 2>/dev/null; do
|
||||
while kill -0 "$SCAN_PID" 2> /dev/null; do
|
||||
# Show different messages based on elapsed time
|
||||
local current_msg=""
|
||||
if [[ $elapsed -lt 5 ]]; then
|
||||
@@ -299,12 +299,12 @@ perform_scan() {
|
||||
printf "\r${BLUE}%s${NC} %s" \
|
||||
"${spinner_chars:$i:1}" "$current_msg"
|
||||
|
||||
i=$(( (i + 1) % 10 ))
|
||||
i=$(((i + 1) % 10))
|
||||
((elapsed++))
|
||||
sleep 0.1
|
||||
done
|
||||
wait "$SCAN_PID" 2>/dev/null || true
|
||||
printf "\r%80s\r" "" # Clear spinner line
|
||||
wait "$SCAN_PID" 2> /dev/null || true
|
||||
printf "\r%80s\r" "" # Clear spinner line
|
||||
show_cursor
|
||||
|
||||
# Aggregate results
|
||||
@@ -508,7 +508,7 @@ display_directories_compact() {
|
||||
|
||||
# Simple bar (10 chars)
|
||||
local bar_width=10
|
||||
local percentage_int=${percentage%.*} # Remove decimal part
|
||||
local percentage_int=${percentage%.*} # Remove decimal part
|
||||
local filled
|
||||
filled=$((percentage_int * bar_width / 100))
|
||||
[[ $filled -gt $bar_width ]] && filled=$bar_width
|
||||
@@ -622,8 +622,8 @@ display_cleanup_suggestions_compact() {
|
||||
if [[ "$CURRENT_PATH" == "$HOME/Library/Caches"* ]] || [[ "$CURRENT_PATH" == "$HOME/Library"* ]]; then
|
||||
if [[ -d "$HOME/Library/Caches" ]]; then
|
||||
local cache_size
|
||||
cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1)
|
||||
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
||||
cache_size=$(du -sk "$HOME/Library/Caches" 2> /dev/null | cut -f1)
|
||||
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
||||
local human
|
||||
human=$(bytes_to_human $((cache_size * 1024)))
|
||||
top_suggestion="Clear app caches ($human)"
|
||||
@@ -637,7 +637,7 @@ display_cleanup_suggestions_compact() {
|
||||
# Check Downloads folder (only if analyzing Downloads)
|
||||
if [[ "$CURRENT_PATH" == "$HOME/Downloads"* ]]; then
|
||||
local old_files
|
||||
old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ')
|
||||
old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ $old_files -gt 0 ]]; then
|
||||
[[ -z "$top_suggestion" ]] && top_suggestion="$old_files files older than 90 days found"
|
||||
[[ -z "$action_command" ]] && action_command="manually review old files"
|
||||
@@ -646,13 +646,13 @@ display_cleanup_suggestions_compact() {
|
||||
fi
|
||||
|
||||
# Check for large disk images in current path
|
||||
if command -v mdfind &>/dev/null; then
|
||||
if command -v mdfind &> /dev/null; then
|
||||
local dmg_count=$(mdfind -onlyin "$CURRENT_PATH" \
|
||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | wc -l | tr -d ' ')
|
||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ $dmg_count -gt 0 ]]; then
|
||||
local dmg_size=$(mdfind -onlyin "$CURRENT_PATH" \
|
||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | \
|
||||
xargs stat -f%z 2>/dev/null | awk '{sum+=$1} END {print sum}')
|
||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2> /dev/null |
|
||||
xargs stat -f%z 2> /dev/null | awk '{sum+=$1} END {print sum}')
|
||||
local dmg_human
|
||||
dmg_human=$(bytes_to_human "$dmg_size")
|
||||
[[ -z "$top_suggestion" ]] && top_suggestion="$dmg_count DMG files ($dmg_human) can be removed"
|
||||
@@ -665,7 +665,7 @@ display_cleanup_suggestions_compact() {
|
||||
# Check Xcode (only if in developer paths)
|
||||
if [[ "$CURRENT_PATH" == "$HOME/Library/Developer"* ]] && [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then
|
||||
local xcode_size
|
||||
xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1)
|
||||
xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2> /dev/null | cut -f1)
|
||||
if [[ $xcode_size -gt 10485760 ]]; then
|
||||
local xcode_human
|
||||
xcode_human=$(bytes_to_human $((xcode_size * 1024)))
|
||||
@@ -677,9 +677,9 @@ display_cleanup_suggestions_compact() {
|
||||
fi
|
||||
|
||||
# Check for duplicates in current path
|
||||
if command -v mdfind &>/dev/null; then
|
||||
local dup_count=$(mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \
|
||||
xargs -I {} stat -f "%z" {} 2>/dev/null | sort | uniq -d | wc -l | tr -d ' ')
|
||||
if command -v mdfind &> /dev/null; then
|
||||
local dup_count=$(mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2> /dev/null |
|
||||
xargs -I {} stat -f "%z" {} 2> /dev/null | sort | uniq -d | wc -l | tr -d ' ')
|
||||
if [[ $dup_count -gt 5 ]]; then
|
||||
[[ -z "$top_suggestion" ]] && top_suggestion="$dup_count potential duplicate files detected"
|
||||
((suggestions_count++))
|
||||
@@ -720,8 +720,8 @@ display_cleanup_suggestions() {
|
||||
# Check common cache locations
|
||||
if [[ -d "$HOME/Library/Caches" ]]; then
|
||||
local cache_size
|
||||
cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1)
|
||||
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
||||
cache_size=$(du -sk "$HOME/Library/Caches" 2> /dev/null | cut -f1)
|
||||
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
||||
local human
|
||||
human=$(bytes_to_human $((cache_size * 1024)))
|
||||
suggestions+=(" Clear application caches: $human")
|
||||
@@ -731,16 +731,16 @@ display_cleanup_suggestions() {
|
||||
# Check Downloads folder
|
||||
if [[ -d "$HOME/Downloads" ]]; then
|
||||
local old_files
|
||||
old_files=$(find "$HOME/Downloads" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ')
|
||||
old_files=$(find "$HOME/Downloads" -type f -mtime +90 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ $old_files -gt 0 ]]; then
|
||||
suggestions+=(" Clean old downloads: $old_files files older than 90 days")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for large disk images
|
||||
if command -v mdfind &>/dev/null; then
|
||||
if command -v mdfind &> /dev/null; then
|
||||
local dmg_count=$(mdfind -onlyin "$HOME" \
|
||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | wc -l | tr -d ' ')
|
||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ $dmg_count -gt 0 ]]; then
|
||||
suggestions+=(" Remove disk images: $dmg_count DMG files >500MB")
|
||||
fi
|
||||
@@ -749,8 +749,8 @@ display_cleanup_suggestions() {
|
||||
# Check Xcode derived data
|
||||
if [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then
|
||||
local xcode_size
|
||||
xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1)
|
||||
if [[ $xcode_size -gt 10485760 ]]; then # > 10GB
|
||||
xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2> /dev/null | cut -f1)
|
||||
if [[ $xcode_size -gt 10485760 ]]; then # > 10GB
|
||||
local human
|
||||
human=$(bytes_to_human $((xcode_size * 1024)))
|
||||
suggestions+=(" Clear Xcode cache: $human")
|
||||
@@ -760,8 +760,8 @@ display_cleanup_suggestions() {
|
||||
# Check iOS device backups
|
||||
if [[ -d "$HOME/Library/Application Support/MobileSync/Backup" ]]; then
|
||||
local backup_size
|
||||
backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2>/dev/null | cut -f1)
|
||||
if [[ $backup_size -gt 5242880 ]]; then # > 5GB
|
||||
backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2> /dev/null | cut -f1)
|
||||
if [[ $backup_size -gt 5242880 ]]; then # > 5GB
|
||||
local human
|
||||
human=$(bytes_to_human $((backup_size * 1024)))
|
||||
suggestions+=(" 📱 Review iOS backups: $human")
|
||||
@@ -769,13 +769,13 @@ display_cleanup_suggestions() {
|
||||
fi
|
||||
|
||||
# Check for duplicate files (by size, quick heuristic)
|
||||
if command -v mdfind &>/dev/null; then
|
||||
if command -v mdfind &> /dev/null; then
|
||||
local temp_dup="$TEMP_PREFIX.dup_check"
|
||||
mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \
|
||||
xargs -I {} stat -f "%z" {} 2>/dev/null | \
|
||||
sort | uniq -d | wc -l | tr -d ' ' > "$temp_dup" 2>/dev/null || echo "0" > "$temp_dup"
|
||||
mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2> /dev/null |
|
||||
xargs -I {} stat -f "%z" {} 2> /dev/null |
|
||||
sort | uniq -d | wc -l | tr -d ' ' > "$temp_dup" 2> /dev/null || echo "0" > "$temp_dup"
|
||||
local dup_count
|
||||
dup_count=$(cat "$temp_dup" 2>/dev/null || echo "0")
|
||||
dup_count=$(cat "$temp_dup" 2> /dev/null || echo "0")
|
||||
if [[ $dup_count -gt 5 ]]; then
|
||||
suggestions+=(" ♻️ Possible duplicates: $dup_count size matches in large files (>10MB)")
|
||||
fi
|
||||
@@ -804,14 +804,14 @@ display_disk_summary() {
|
||||
local total_dirs_count=0
|
||||
|
||||
if [[ -f "$temp_large" ]]; then
|
||||
total_large_count=$(wc -l < "$temp_large" 2>/dev/null | tr -d ' ')
|
||||
total_large_count=$(wc -l < "$temp_large" 2> /dev/null | tr -d ' ')
|
||||
while IFS='|' read -r size path; do
|
||||
((total_large_size += size))
|
||||
done < "$temp_large"
|
||||
fi
|
||||
|
||||
if [[ -f "$temp_dirs" ]]; then
|
||||
total_dirs_count=$(wc -l < "$temp_dirs" 2>/dev/null | tr -d ' ')
|
||||
total_dirs_count=$(wc -l < "$temp_dirs" 2> /dev/null | tr -d ' ')
|
||||
while IFS='|' read -r size path; do
|
||||
((total_dirs_size += size))
|
||||
done < "$temp_dirs"
|
||||
@@ -841,20 +841,24 @@ get_file_info() {
|
||||
local type="File"
|
||||
|
||||
case "$ext" in
|
||||
dmg|iso|pkg|zip|tar|gz|rar|7z)
|
||||
badge="$BADGE_BUNDLE" ; type="Bundle"
|
||||
dmg | iso | pkg | zip | tar | gz | rar | 7z)
|
||||
badge="$BADGE_BUNDLE"
|
||||
type="Bundle"
|
||||
;;
|
||||
mov|mp4|avi|mkv|webm|jpg|jpeg|png|gif|heic)
|
||||
badge="$BADGE_MEDIA" ; type="Media"
|
||||
mov | mp4 | avi | mkv | webm | jpg | jpeg | png | gif | heic)
|
||||
badge="$BADGE_MEDIA"
|
||||
type="Media"
|
||||
;;
|
||||
pdf|key|ppt|pptx)
|
||||
pdf | key | ppt | pptx)
|
||||
type="Document"
|
||||
;;
|
||||
log)
|
||||
badge="$BADGE_LOG" ; type="Log"
|
||||
badge="$BADGE_LOG"
|
||||
type="Log"
|
||||
;;
|
||||
app)
|
||||
badge="$BADGE_APP" ; type="App"
|
||||
badge="$BADGE_APP"
|
||||
type="App"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -870,7 +874,7 @@ get_file_age() {
|
||||
fi
|
||||
|
||||
local mtime
|
||||
mtime=$(stat -f%m "$path" 2>/dev/null || echo "0")
|
||||
mtime=$(stat -f%m "$path" 2> /dev/null || echo "0")
|
||||
local now
|
||||
now=$(date +%s)
|
||||
local diff
|
||||
@@ -936,8 +940,8 @@ display_large_files_table() {
|
||||
# Color based on file type
|
||||
local color=""
|
||||
case "$ext" in
|
||||
dmg|iso|pkg) color="${RED}" ;;
|
||||
mov|mp4|avi|mkv|webm|zip|tar|gz|rar|7z) color="${YELLOW}" ;;
|
||||
dmg | iso | pkg) color="${RED}" ;;
|
||||
mov | mp4 | avi | mkv | webm | zip | tar | gz | rar | 7z) color="${YELLOW}" ;;
|
||||
log) color="${GRAY}" ;;
|
||||
*) color="${NC}" ;;
|
||||
esac
|
||||
@@ -1104,7 +1108,7 @@ display_recent_large_files() {
|
||||
log_header "Recent Large Files (Last 30 Days)"
|
||||
echo ""
|
||||
|
||||
if ! command -v mdfind &>/dev/null; then
|
||||
if ! command -v mdfind &> /dev/null; then
|
||||
echo " ${YELLOW}Note: mdfind not available${NC}"
|
||||
echo ""
|
||||
return
|
||||
@@ -1114,13 +1118,13 @@ display_recent_large_files() {
|
||||
|
||||
# Find files created in last 30 days, larger than 100MB
|
||||
mdfind -onlyin "$CURRENT_PATH" \
|
||||
"kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2>/dev/null | \
|
||||
"kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2> /dev/null |
|
||||
while IFS= read -r file; do
|
||||
if [[ -f "$file" ]]; then
|
||||
local size
|
||||
size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||
size=$(stat -f%z "$file" 2> /dev/null || echo "0")
|
||||
local mtime
|
||||
mtime=$(stat -f%m "$file" 2>/dev/null || echo "0")
|
||||
mtime=$(stat -f%m "$file" 2> /dev/null || echo "0")
|
||||
echo "$size|$mtime|$file"
|
||||
fi
|
||||
done | sort -t'|' -k1 -rn | head -10 > "$temp_recent"
|
||||
@@ -1140,7 +1144,7 @@ display_recent_large_files() {
|
||||
local dirname
|
||||
dirname=$(dirname "$path" | sed "s|^$HOME|~|")
|
||||
local days_ago
|
||||
days_ago=$(( ($(date +%s) - mtime) / 86400 ))
|
||||
days_ago=$((($(date +%s) - mtime) / 86400))
|
||||
|
||||
local info
|
||||
info=$(get_file_info "$path")
|
||||
@@ -1162,10 +1166,10 @@ get_subdirectories() {
|
||||
local target="$1"
|
||||
local temp_file="$2"
|
||||
|
||||
find "$target" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | \
|
||||
find "$target" -mindepth 1 -maxdepth 1 -type d 2> /dev/null |
|
||||
while IFS= read -r dir; do
|
||||
local size
|
||||
size=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
||||
size=$(du -sk "$dir" 2> /dev/null | cut -f1)
|
||||
echo "$((size * 1024))|$dir"
|
||||
done | sort -t'|' -k1 -rn > "$temp_file"
|
||||
}
|
||||
@@ -1298,7 +1302,7 @@ display_file_types() {
|
||||
log_header "File Types Analysis"
|
||||
echo ""
|
||||
|
||||
if ! command -v mdfind &>/dev/null; then
|
||||
if ! command -v mdfind &> /dev/null; then
|
||||
echo " ${YELLOW}Note: mdfind not available, limited analysis${NC}"
|
||||
return
|
||||
fi
|
||||
@@ -1336,7 +1340,7 @@ display_file_types() {
|
||||
esac
|
||||
|
||||
local files
|
||||
files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null)
|
||||
files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2> /dev/null)
|
||||
local count
|
||||
count=$(echo "$files" | grep -c . || echo "0")
|
||||
local total_size=0
|
||||
@@ -1345,7 +1349,7 @@ display_file_types() {
|
||||
while IFS= read -r file; do
|
||||
if [[ -f "$file" ]]; then
|
||||
local fsize
|
||||
fsize=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||
fsize=$(stat -f%z "$file" 2> /dev/null || echo "0")
|
||||
((total_size += fsize))
|
||||
fi
|
||||
done <<< "$files"
|
||||
@@ -1364,7 +1368,7 @@ display_file_types() {
|
||||
read_single_key() {
|
||||
local key=""
|
||||
# Read single character without waiting for Enter
|
||||
if read -rsn1 key 2>/dev/null; then
|
||||
if read -rsn1 key 2> /dev/null; then
|
||||
echo "$key"
|
||||
else
|
||||
echo "q"
|
||||
@@ -1396,13 +1400,13 @@ scan_directory_contents_fast() {
|
||||
fi
|
||||
|
||||
# Ultra-fast file scanning - batch stat for maximum speed
|
||||
find "$dir_path" -mindepth 1 -maxdepth 1 -type f -print0 2>/dev/null | \
|
||||
xargs -0 -n 20 -P "$num_jobs" stat -f "%z|file|%N" 2>/dev/null > "$temp_files" &
|
||||
find "$dir_path" -mindepth 1 -maxdepth 1 -type f -print0 2> /dev/null |
|
||||
xargs -0 -n 20 -P "$num_jobs" stat -f "%z|file|%N" 2> /dev/null > "$temp_files" &
|
||||
local file_pid=$!
|
||||
|
||||
# Smart directory scanning with aggressive optimization
|
||||
# Strategy: Fast estimation first, accurate on-demand
|
||||
find "$dir_path" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | \
|
||||
find "$dir_path" -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null |
|
||||
xargs -0 -n 1 -P "$num_jobs" sh -c '
|
||||
dir="$1"
|
||||
size=""
|
||||
@@ -1436,7 +1440,7 @@ scan_directory_contents_fast() {
|
||||
[[ -z "$size" ]] || [[ "$size" -eq 0 ]] && size=1
|
||||
fi
|
||||
echo "$((size * 1024))|dir|$dir"
|
||||
' _ > "$temp_dirs" 2>/dev/null &
|
||||
' _ > "$temp_dirs" 2> /dev/null &
|
||||
local dir_pid=$!
|
||||
|
||||
# Show progress while waiting
|
||||
@@ -1448,22 +1452,22 @@ scan_directory_contents_fast() {
|
||||
local spinner_chars
|
||||
spinner_chars="$(mo_spinner_chars)"
|
||||
local chars_len=${#spinner_chars}
|
||||
for ((idx=0; idx<chars_len; idx++)); do
|
||||
for ((idx = 0; idx < chars_len; idx++)); do
|
||||
spinner+=("${spinner_chars:idx:1}")
|
||||
done
|
||||
fi
|
||||
[[ ${#spinner[@]} -eq 0 ]] && spinner=('|' '/' '-' '\\')
|
||||
local i=0
|
||||
local max_wait=30 # Reduced to 30 seconds (fast fail)
|
||||
local max_wait=30 # Reduced to 30 seconds (fast fail)
|
||||
local elapsed=0
|
||||
local tick=0
|
||||
local spin_len=${#spinner[@]}
|
||||
(( spin_len == 0 )) && spinner=('|' '/' '-' '\\') && spin_len=${#spinner[@]}
|
||||
((spin_len == 0)) && spinner=('|' '/' '-' '\\') && spin_len=${#spinner[@]}
|
||||
|
||||
while ( kill -0 "$dir_pid" 2>/dev/null || kill -0 "$file_pid" 2>/dev/null ); do
|
||||
while (kill -0 "$dir_pid" 2> /dev/null || kill -0 "$file_pid" 2> /dev/null); do
|
||||
printf "\r ${BLUE}Scanning${NC} ${spinner[$((i % spin_len))]} (%ds)" "$elapsed" >&2
|
||||
((i++))
|
||||
sleep 0.1 # Faster animation (100ms per frame)
|
||||
sleep 0.1 # Faster animation (100ms per frame)
|
||||
((tick++))
|
||||
|
||||
# Update elapsed seconds every 10 ticks (1 second)
|
||||
@@ -1473,10 +1477,10 @@ scan_directory_contents_fast() {
|
||||
|
||||
# Force kill if taking too long (30 seconds for fast response)
|
||||
if [[ $elapsed -ge $max_wait ]]; then
|
||||
kill -9 "$dir_pid" 2>/dev/null || true
|
||||
kill -9 "$file_pid" 2>/dev/null || true
|
||||
wait "$dir_pid" 2>/dev/null || true
|
||||
wait "$file_pid" 2>/dev/null || true
|
||||
kill -9 "$dir_pid" 2> /dev/null || true
|
||||
kill -9 "$file_pid" 2> /dev/null || true
|
||||
wait "$dir_pid" 2> /dev/null || true
|
||||
wait "$file_pid" 2> /dev/null || true
|
||||
printf "\r ${YELLOW}Large directory - showing estimated sizes${NC}\n" >&2
|
||||
sleep 0.3
|
||||
break
|
||||
@@ -1488,8 +1492,8 @@ scan_directory_contents_fast() {
|
||||
fi
|
||||
|
||||
# Wait for completion (non-blocking if already killed)
|
||||
wait "$file_pid" 2>/dev/null || true
|
||||
wait "$dir_pid" 2>/dev/null || true
|
||||
wait "$file_pid" 2> /dev/null || true
|
||||
wait "$dir_pid" 2> /dev/null || true
|
||||
|
||||
# Small delay only if scan was very fast (let user see the spinner briefly)
|
||||
if [[ "$show_progress" == "true" ]] && [[ ${elapsed:-0} -lt 1 ]]; then
|
||||
@@ -1498,19 +1502,19 @@ scan_directory_contents_fast() {
|
||||
|
||||
# Combine and sort - only keep top items
|
||||
# Ensure we handle empty files gracefully
|
||||
> "$output_file"
|
||||
true > "$output_file"
|
||||
if [[ -f "$temp_dirs" ]] || [[ -f "$temp_files" ]]; then
|
||||
cat "$temp_dirs" "$temp_files" 2>/dev/null | sort -t'|' -k1 -rn | head -"$max_items" > "$output_file" || true
|
||||
cat "$temp_dirs" "$temp_files" 2> /dev/null | sort -t'|' -k1 -rn | head -"$max_items" > "$output_file" || true
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$temp_dirs" "$temp_files" 2>/dev/null
|
||||
rm -f "$temp_dirs" "$temp_files" 2> /dev/null
|
||||
}
|
||||
|
||||
# Calculate directory sizes and update (now only used for deep refresh)
|
||||
calculate_dir_sizes() {
|
||||
local items_file="$1"
|
||||
local max_items="${2:-15}" # Only recalculate first 15 by default
|
||||
local max_items="${2:-15}" # Only recalculate first 15 by default
|
||||
local temp_file="${items_file}.calc"
|
||||
|
||||
# Since we now scan with actual sizes, this function is mainly for refresh
|
||||
@@ -1519,9 +1523,9 @@ calculate_dir_sizes() {
|
||||
|
||||
# Only update if source file still exists (might have been deleted if user quit)
|
||||
if [[ -f "$items_file" ]]; then
|
||||
mv "$temp_file" "$items_file" 2>/dev/null || true
|
||||
mv "$temp_file" "$items_file" 2> /dev/null || true
|
||||
else
|
||||
rm -f "$temp_file" 2>/dev/null || true
|
||||
rm -f "$temp_file" 2> /dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1531,7 +1535,7 @@ combine_initial_scan_results() {
|
||||
local temp_large="$TEMP_PREFIX.large"
|
||||
local temp_dirs="$TEMP_PREFIX.dirs"
|
||||
|
||||
> "$output_file"
|
||||
true > "$output_file"
|
||||
|
||||
# Add directories
|
||||
if [[ -f "$temp_dirs" ]]; then
|
||||
@@ -1572,7 +1576,7 @@ show_volumes_overview() {
|
||||
# External volumes (if any)
|
||||
if [[ -d "/Volumes" ]]; then
|
||||
local vol_priority=500
|
||||
find /Volumes -mindepth 1 -maxdepth 1 -type d 2>/dev/null | while IFS= read -r vol; do
|
||||
find /Volumes -mindepth 1 -maxdepth 1 -type d 2> /dev/null | while IFS= read -r vol; do
|
||||
local vol_name
|
||||
vol_name=$(basename "$vol")
|
||||
echo "$((vol_priority))|$vol|Volume: $vol_name"
|
||||
@@ -1582,17 +1586,17 @@ show_volumes_overview() {
|
||||
} | sort -t'|' -k1 -rn > "$temp_volumes"
|
||||
|
||||
# Setup alternate screen and hide cursor (keep hidden throughout)
|
||||
tput smcup 2>/dev/null || true
|
||||
printf "\033[?25l" >&2 # Hide cursor
|
||||
tput smcup 2> /dev/null || true
|
||||
printf "\033[?25l" >&2 # Hide cursor
|
||||
|
||||
cleanup_volumes() {
|
||||
printf "\033[?25h" >&2 # Show cursor
|
||||
tput rmcup 2>/dev/null || true
|
||||
printf "\033[?25h" >&2 # Show cursor
|
||||
tput rmcup 2> /dev/null || true
|
||||
}
|
||||
trap cleanup_volumes EXIT INT TERM
|
||||
|
||||
# Force cursor hidden at the start
|
||||
stty -echo 2>/dev/null || true
|
||||
stty -echo 2> /dev/null || true
|
||||
|
||||
local cursor=0
|
||||
local total_items
|
||||
@@ -1603,10 +1607,10 @@ show_volumes_overview() {
|
||||
printf "\033[?25l" >&2
|
||||
|
||||
# Drain burst input (trackpad scroll -> many arrows)
|
||||
type drain_pending_input >/dev/null 2>&1 && drain_pending_input
|
||||
type drain_pending_input > /dev/null 2>&1 && drain_pending_input
|
||||
# Build output buffer to reduce flicker
|
||||
local output=""
|
||||
output+="\033[?25l" # Hide cursor
|
||||
output+="\033[?25l" # Hide cursor
|
||||
output+="\033[H\033[J"
|
||||
output+=$'\n'
|
||||
output+="\033[0;35mSelect a location to explore\033[0m"$'\n'
|
||||
@@ -1633,7 +1637,7 @@ show_volumes_overview() {
|
||||
|
||||
# Read key (suppress any escape sequences that might leak)
|
||||
local key
|
||||
key=$(read_key 2>/dev/null || echo "OTHER")
|
||||
key=$(read_key 2> /dev/null || echo "OTHER")
|
||||
|
||||
case "$key" in
|
||||
"UP")
|
||||
@@ -1642,7 +1646,7 @@ show_volumes_overview() {
|
||||
"DOWN")
|
||||
((cursor < total_items - 1)) && ((cursor++))
|
||||
;;
|
||||
"ENTER"|"RIGHT")
|
||||
"ENTER" | "RIGHT")
|
||||
# Get selected path and enter it
|
||||
local selected_path=""
|
||||
idx=0
|
||||
@@ -1679,7 +1683,7 @@ show_volumes_overview() {
|
||||
# In volumes view, LEFT does nothing (already at top level)
|
||||
# User must press q/ESC to quit
|
||||
;;
|
||||
"QUIT"|"q")
|
||||
"QUIT" | "q")
|
||||
# Quit the volumes view
|
||||
break
|
||||
;;
|
||||
@@ -1693,13 +1697,13 @@ show_volumes_overview() {
|
||||
# Interactive drill-down mode
|
||||
interactive_drill_down() {
|
||||
local start_path="$1"
|
||||
local initial_items="${2:-}" # Pre-scanned items for first level
|
||||
local initial_items="${2:-}" # Pre-scanned items for first level
|
||||
local current_path="$start_path"
|
||||
local path_stack=()
|
||||
local cursor=0
|
||||
local scroll_offset=0 # New: for scrolling
|
||||
local scroll_offset=0 # New: for scrolling
|
||||
local need_scan=true
|
||||
local wait_for_calc=false # Don't wait on first load, let user press 'r'
|
||||
local wait_for_calc=false # Don't wait on first load, let user press 'r'
|
||||
local temp_items="$TEMP_PREFIX.items"
|
||||
local status_message=""
|
||||
|
||||
@@ -1711,33 +1715,33 @@ interactive_drill_down() {
|
||||
# Directory cache: store scan results for each visited directory
|
||||
# Use temp files because bash 3.2 doesn't have associative arrays
|
||||
local cache_dir="$TEMP_PREFIX.cache.$$"
|
||||
mkdir -p "$cache_dir" 2>/dev/null || true
|
||||
mkdir -p "$cache_dir" 2> /dev/null || true
|
||||
|
||||
# Note: We're already in alternate screen from show_volumes_overview
|
||||
# Just hide cursor, don't re-enter alternate screen
|
||||
printf "\033[?25l" # Hide cursor
|
||||
printf "\033[?25l" # Hide cursor
|
||||
|
||||
# Save terminal settings and disable echo
|
||||
local old_tty_settings=""
|
||||
if [[ -t 0 ]]; then
|
||||
old_tty_settings=$(stty -g 2>/dev/null || echo "")
|
||||
stty -echo 2>/dev/null || true
|
||||
old_tty_settings=$(stty -g 2> /dev/null || echo "")
|
||||
stty -echo 2> /dev/null || true
|
||||
fi
|
||||
|
||||
# Cleanup on exit (but don't exit alternate screen - may return to menu)
|
||||
cleanup_drill_down() {
|
||||
# Restore terminal settings
|
||||
if [[ -n "${old_tty_settings:-}" ]]; then
|
||||
stty "$old_tty_settings" 2>/dev/null || true
|
||||
stty "$old_tty_settings" 2> /dev/null || true
|
||||
fi
|
||||
printf "\033[?25h" # Show cursor
|
||||
printf "\033[?25h" # Show cursor
|
||||
# Don't call tput rmcup - we may be returning to volumes menu
|
||||
[[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2>/dev/null || true # Clean up cache
|
||||
[[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2> /dev/null || true # Clean up cache
|
||||
}
|
||||
trap cleanup_drill_down EXIT INT TERM
|
||||
|
||||
# Drain any input that accumulated before entering interactive mode
|
||||
type drain_pending_input >/dev/null 2>&1 && drain_pending_input
|
||||
type drain_pending_input > /dev/null 2>&1 && drain_pending_input
|
||||
|
||||
while true; do
|
||||
# Ensure cursor is always hidden during navigation
|
||||
@@ -1747,7 +1751,7 @@ interactive_drill_down() {
|
||||
if [[ "$need_scan" == "true" ]]; then
|
||||
# Generate cache key (use md5 hash of path)
|
||||
local cache_key
|
||||
cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
||||
cache_key=$(echo "$current_path" | md5 2> /dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
||||
local cache_file="$cache_dir/$cache_key"
|
||||
|
||||
# Check if we have cached results for this directory
|
||||
@@ -1760,12 +1764,12 @@ interactive_drill_down() {
|
||||
# Use || true to prevent exit on scan failure
|
||||
scan_directory_contents_fast "$current_path" "$temp_items" 50 true || {
|
||||
# Scan failed - create empty result file
|
||||
> "$temp_items"
|
||||
true > "$temp_items"
|
||||
}
|
||||
|
||||
# Save to cache for next time (only if not empty)
|
||||
if [[ -s "$temp_items" ]]; then
|
||||
cp "$temp_items" "$cache_file" 2>/dev/null || true
|
||||
cp "$temp_items" "$cache_file" 2> /dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1787,7 +1791,7 @@ interactive_drill_down() {
|
||||
scroll_offset=0
|
||||
|
||||
# Drain any input accumulated during scanning
|
||||
type drain_pending_input >/dev/null 2>&1 && drain_pending_input
|
||||
type drain_pending_input > /dev/null 2>&1 && drain_pending_input
|
||||
|
||||
# Check if empty or scan failed
|
||||
if [[ $total_items -eq 0 ]]; then
|
||||
@@ -1800,7 +1804,7 @@ interactive_drill_down() {
|
||||
echo " ${GRAY}Path: $current_path${NC}" >&2
|
||||
echo "" >&2
|
||||
echo " ${GRAY}Press any key to go back...${NC}" >&2
|
||||
read_key >/dev/null 2>&1
|
||||
read_key > /dev/null 2>&1
|
||||
else
|
||||
# Directory exists but scan returned nothing (timeout or empty)
|
||||
printf "\033[H\033[J" >&2
|
||||
@@ -1811,7 +1815,7 @@ interactive_drill_down() {
|
||||
echo " ${GRAY}Press ${NC}${GREEN}R${NC}${GRAY} to retry, any other key to go back${NC}" >&2
|
||||
|
||||
local retry_key
|
||||
retry_key=$(read_key 2>/dev/null || echo "OTHER")
|
||||
retry_key=$(read_key 2> /dev/null || echo "OTHER")
|
||||
|
||||
if [[ "$retry_key" == "RETRY" ]]; then
|
||||
# Retry scan
|
||||
@@ -1842,13 +1846,13 @@ interactive_drill_down() {
|
||||
|
||||
# Build output buffer once for smooth rendering
|
||||
local output=""
|
||||
output+="\033[?25l" # Hide cursor
|
||||
output+="\033[H\033[J" # Clear screen
|
||||
output+="\033[?25l" # Hide cursor
|
||||
output+="\033[H\033[J" # Clear screen
|
||||
output+=$'\n'
|
||||
output+="\033[0;35mDisk space explorer > $(echo "$current_path" | sed "s|^$HOME|~|")\033[0m"$'\n'
|
||||
output+=$'\n'
|
||||
|
||||
local max_show=15 # Show 15 items per page
|
||||
local max_show=15 # Show 15 items per page
|
||||
local page_start=$scroll_offset
|
||||
local page_end
|
||||
page_end=$((scroll_offset + max_show))
|
||||
@@ -1886,8 +1890,10 @@ interactive_drill_down() {
|
||||
local badge="$BADGE_FILE" color="${NC}"
|
||||
if [[ "$type" == "dir" ]]; then
|
||||
badge="$BADGE_DIR" color="${BLUE}"
|
||||
if [[ $size -gt 10737418240 ]]; then color="${RED}"
|
||||
elif [[ $size -gt 1073741824 ]]; then color="${YELLOW}"
|
||||
if [[ $size -gt 10737418240 ]]; then
|
||||
color="${RED}"
|
||||
elif [[ $size -gt 1073741824 ]]; then
|
||||
color="${YELLOW}"
|
||||
fi
|
||||
else
|
||||
local ext="${name##*.}"
|
||||
@@ -1895,10 +1901,10 @@ interactive_drill_down() {
|
||||
info=$(get_file_info "$path")
|
||||
badge="${info%|*}"
|
||||
case "$ext" in
|
||||
dmg|iso|pkg|zip|tar|gz|rar|7z)
|
||||
dmg | iso | pkg | zip | tar | gz | rar | 7z)
|
||||
color="${YELLOW}"
|
||||
;;
|
||||
mov|mp4|avi|mkv|webm|jpg|jpeg|png|gif|heic)
|
||||
mov | mp4 | avi | mkv | webm | jpg | jpeg | png | gif | heic)
|
||||
color="${YELLOW}"
|
||||
;;
|
||||
log)
|
||||
@@ -1945,7 +1951,7 @@ interactive_drill_down() {
|
||||
|
||||
# Read key directly without draining (to preserve all user input)
|
||||
local key
|
||||
key=$(read_key 2>/dev/null || echo "OTHER")
|
||||
key=$(read_key 2> /dev/null || echo "OTHER")
|
||||
|
||||
# Debug: uncomment to see what keys are being received
|
||||
# printf "\rDEBUG: Received key=[%s] " "$key" >&2
|
||||
@@ -1974,7 +1980,7 @@ interactive_drill_down() {
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"ENTER"|"RIGHT")
|
||||
"ENTER" | "RIGHT")
|
||||
# Enter selected item - directory or file
|
||||
if [[ $cursor -lt ${#items[@]} ]]; then
|
||||
local selected="${items[$cursor]}"
|
||||
@@ -1998,7 +2004,7 @@ interactive_drill_down() {
|
||||
|
||||
# For text-like files, use less or fallback to open
|
||||
case "$file_ext" in
|
||||
txt|log|md|json|xml|yaml|yml|conf|cfg|ini|sh|bash|zsh|py|js|ts|go|rs|c|cpp|h|java|rb|php|html|css|sql)
|
||||
txt | log | md | json | xml | yaml | yml | conf | cfg | ini | sh | bash | zsh | py | js | ts | go | rs | c | cpp | h | java | rb | php | html | css | sql)
|
||||
# Clear screen and show loading message
|
||||
printf "\033[H\033[J"
|
||||
echo ""
|
||||
@@ -2006,21 +2012,21 @@ interactive_drill_down() {
|
||||
echo ""
|
||||
|
||||
# Try less first (best for text viewing)
|
||||
if command -v less &>/dev/null; then
|
||||
if command -v less &> /dev/null; then
|
||||
# Exit alternate screen only for less
|
||||
printf "\033[?25h" # Show cursor
|
||||
tput rmcup 2>/dev/null || true
|
||||
printf "\033[?25h" # Show cursor
|
||||
tput rmcup 2> /dev/null || true
|
||||
|
||||
less -F "$selected_path" 2>/dev/null && open_success=true
|
||||
less -F "$selected_path" 2> /dev/null && open_success=true
|
||||
|
||||
# Return to alternate screen
|
||||
tput smcup 2>/dev/null || true
|
||||
printf "\033[?25l" # Hide cursor
|
||||
tput smcup 2> /dev/null || true
|
||||
printf "\033[?25l" # Hide cursor
|
||||
else
|
||||
# Fallback to system open if less is not available
|
||||
echo " ${GRAY}Launching default application...${NC}"
|
||||
if command -v open &>/dev/null; then
|
||||
open "$selected_path" 2>/dev/null && open_success=true
|
||||
if command -v open &> /dev/null; then
|
||||
open "$selected_path" 2> /dev/null && open_success=true
|
||||
if [[ "$open_success" == "true" ]]; then
|
||||
echo ""
|
||||
echo " ${GREEN}${ICON_SUCCESS}${NC} File opened in external app"
|
||||
@@ -2038,8 +2044,8 @@ interactive_drill_down() {
|
||||
echo ""
|
||||
echo " ${GRAY}Launching default application...${NC}"
|
||||
|
||||
if command -v open &>/dev/null; then
|
||||
open "$selected_path" 2>/dev/null && open_success=true
|
||||
if command -v open &> /dev/null; then
|
||||
open "$selected_path" 2> /dev/null && open_success=true
|
||||
|
||||
# Show brief success message
|
||||
if [[ "$open_success" == "true" ]]; then
|
||||
@@ -2059,7 +2065,7 @@ interactive_drill_down() {
|
||||
echo ""
|
||||
echo " ${GRAY}File: $selected_path${NC}"
|
||||
echo " ${GRAY}Press any key to return...${NC}"
|
||||
read -n 1 -s 2>/dev/null
|
||||
read -n 1 -s 2> /dev/null
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -2081,16 +2087,16 @@ interactive_drill_down() {
|
||||
# Already at start path - return to volumes menu
|
||||
# Don't show cursor or exit screen - menu will handle it
|
||||
if [[ -n "${old_tty_settings:-}" ]]; then
|
||||
stty "$old_tty_settings" 2>/dev/null || true
|
||||
stty "$old_tty_settings" 2> /dev/null || true
|
||||
fi
|
||||
[[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2>/dev/null || true
|
||||
[[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2> /dev/null || true
|
||||
trap - EXIT INT TERM
|
||||
return 1 # Return to menu
|
||||
return 1 # Return to menu
|
||||
fi
|
||||
;;
|
||||
"OPEN")
|
||||
if command -v open >/dev/null 2>&1; then
|
||||
if open "$current_path" >/dev/null 2>&1; then
|
||||
if command -v open > /dev/null 2>&1; then
|
||||
if open "$current_path" > /dev/null 2>&1; then
|
||||
status_message="${GREEN}${ICON_SUCCESS}${NC} Finder opened: ${GRAY}$current_path${NC}"
|
||||
else
|
||||
status_message="${YELLOW}Warning:${NC} Could not open ${GRAY}$current_path${NC}"
|
||||
@@ -2155,7 +2161,7 @@ interactive_drill_down() {
|
||||
|
||||
# Read confirmation
|
||||
local confirm
|
||||
confirm=$(read_key 2>/dev/null || echo "QUIT")
|
||||
confirm=$(read_key 2> /dev/null || echo "QUIT")
|
||||
|
||||
if [[ "$confirm" == "ENTER" ]]; then
|
||||
# Request sudo if needed before deletion
|
||||
@@ -2180,11 +2186,11 @@ interactive_drill_down() {
|
||||
# Try to delete with sudo if needed
|
||||
local delete_success=false
|
||||
if [[ "$needs_sudo" == "true" ]]; then
|
||||
if sudo rm -rf "$selected_path" 2>/dev/null; then
|
||||
if sudo rm -rf "$selected_path" 2> /dev/null; then
|
||||
delete_success=true
|
||||
fi
|
||||
else
|
||||
if rm -rf "$selected_path" 2>/dev/null; then
|
||||
if rm -rf "$selected_path" 2> /dev/null; then
|
||||
delete_success=true
|
||||
fi
|
||||
fi
|
||||
@@ -2195,9 +2201,9 @@ interactive_drill_down() {
|
||||
|
||||
# Clear cache to force rescan
|
||||
local cache_key
|
||||
cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
||||
cache_key=$(echo "$current_path" | md5 2> /dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
||||
local cache_file="$cache_dir/$cache_key"
|
||||
rm -f "$cache_file" 2>/dev/null || true
|
||||
rm -f "$cache_file" 2> /dev/null || true
|
||||
|
||||
# Refresh the view
|
||||
need_scan=true
|
||||
@@ -2215,16 +2221,16 @@ interactive_drill_down() {
|
||||
echo " ${ICON_LIST} System protection (SIP) prevents deletion"
|
||||
echo ""
|
||||
echo " ${GRAY}Press any key to continue...${NC}"
|
||||
read_key >/dev/null 2>&1
|
||||
read_key > /dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"QUIT"|"q")
|
||||
"QUIT" | "q")
|
||||
# Quit the explorer
|
||||
cleanup_drill_down
|
||||
trap - EXIT INT TERM
|
||||
return 0 # Return true to indicate normal exit
|
||||
return 0 # Return true to indicate normal exit
|
||||
;;
|
||||
*)
|
||||
# Unknown key - ignore it
|
||||
@@ -2233,7 +2239,7 @@ interactive_drill_down() {
|
||||
done
|
||||
|
||||
# Cleanup is handled by trap
|
||||
return 0 # Normal exit if loop ends
|
||||
return 0 # Normal exit if loop ends
|
||||
}
|
||||
|
||||
# Main interactive loop
|
||||
@@ -2242,7 +2248,7 @@ interactive_mode() {
|
||||
VIEW_MODE="overview"
|
||||
|
||||
while true; do
|
||||
type drain_pending_input >/dev/null 2>&1 && drain_pending_input
|
||||
type drain_pending_input > /dev/null 2>&1 && drain_pending_input
|
||||
display_interactive_menu
|
||||
|
||||
local key
|
||||
@@ -2291,10 +2297,10 @@ interactive_mode() {
|
||||
VIEW_MODE="overview"
|
||||
fi
|
||||
;;
|
||||
"f"|"F")
|
||||
"f" | "F")
|
||||
VIEW_MODE="files"
|
||||
;;
|
||||
"t"|"T")
|
||||
"t" | "T")
|
||||
VIEW_MODE="types"
|
||||
;;
|
||||
"ENTER")
|
||||
@@ -2402,7 +2408,7 @@ main() {
|
||||
# Parse arguments - only support --help
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
-h | --help)
|
||||
echo "Usage: mole analyze"
|
||||
echo ""
|
||||
echo "Interactive disk space explorer - Navigate folders sorted by size"
|
||||
@@ -2446,7 +2452,7 @@ main() {
|
||||
CURRENT_PATH="$target_path"
|
||||
|
||||
# Create cache directory
|
||||
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
||||
mkdir -p "$CACHE_DIR" 2> /dev/null || true
|
||||
|
||||
# Start with volumes overview to let user choose location
|
||||
show_volumes_overview
|
||||
|
||||
220
bin/clean.sh
220
bin/clean.sh
@@ -18,11 +18,11 @@ DRY_RUN=false
|
||||
IS_M_SERIES=$([ "$(uname -m)" = "arm64" ] && echo "true" || echo "false")
|
||||
|
||||
# Constants
|
||||
readonly MAX_PARALLEL_JOBS=15 # Maximum parallel background jobs
|
||||
readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup
|
||||
readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data
|
||||
readonly SIZE_1GB_KB=1048576 # 1GB in kilobytes
|
||||
readonly SIZE_1MB_KB=1024 # 1MB in kilobytes
|
||||
readonly MAX_PARALLEL_JOBS=15 # Maximum parallel background jobs
|
||||
readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup
|
||||
readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data
|
||||
readonly SIZE_1GB_KB=1048576 # 1GB in kilobytes
|
||||
readonly SIZE_1MB_KB=1024 # 1MB in kilobytes
|
||||
# Default whitelist patterns (preselected, user can disable)
|
||||
declare -a DEFAULT_WHITELIST_PATTERNS=(
|
||||
"$HOME/Library/Caches/ms-playwright*"
|
||||
@@ -52,7 +52,7 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
|
||||
|
||||
# Prevent absolute path to critical system directories
|
||||
case "$line" in
|
||||
/System/*|/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*)
|
||||
/System/* | /bin/* | /sbin/* | /usr/bin/* | /usr/sbin/*)
|
||||
WHITELIST_WARNINGS+=("System path: $line")
|
||||
continue
|
||||
;;
|
||||
@@ -104,14 +104,14 @@ cleanup() {
|
||||
|
||||
# Stop all spinners and clear the line
|
||||
if [[ -n "$SPINNER_PID" ]]; then
|
||||
kill "$SPINNER_PID" 2>/dev/null || true
|
||||
wait "$SPINNER_PID" 2>/dev/null || true
|
||||
kill "$SPINNER_PID" 2> /dev/null || true
|
||||
wait "$SPINNER_PID" 2> /dev/null || true
|
||||
SPINNER_PID=""
|
||||
fi
|
||||
|
||||
if [[ -n "$INLINE_SPINNER_PID" ]]; then
|
||||
kill "$INLINE_SPINNER_PID" 2>/dev/null || true
|
||||
wait "$INLINE_SPINNER_PID" 2>/dev/null || true
|
||||
kill "$INLINE_SPINNER_PID" 2> /dev/null || true
|
||||
wait "$INLINE_SPINNER_PID" 2> /dev/null || true
|
||||
INLINE_SPINNER_PID=""
|
||||
fi
|
||||
|
||||
@@ -122,8 +122,8 @@ cleanup() {
|
||||
|
||||
# Stop sudo keepalive
|
||||
if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then
|
||||
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||
wait "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||
kill "$SUDO_KEEPALIVE_PID" 2> /dev/null || true
|
||||
wait "$SUDO_KEEPALIVE_PID" 2> /dev/null || true
|
||||
SUDO_KEEPALIVE_PID=""
|
||||
fi
|
||||
|
||||
@@ -176,8 +176,8 @@ stop_spinner() {
|
||||
fi
|
||||
|
||||
if [[ -n "$SPINNER_PID" ]]; then
|
||||
kill "$SPINNER_PID" 2>/dev/null
|
||||
wait "$SPINNER_PID" 2>/dev/null
|
||||
kill "$SPINNER_PID" 2> /dev/null
|
||||
wait "$SPINNER_PID" 2> /dev/null
|
||||
SPINNER_PID=""
|
||||
printf "\r ${GREEN}${ICON_SUCCESS}${NC} %s\n" "$result_message"
|
||||
else
|
||||
@@ -229,7 +229,7 @@ safe_clean() {
|
||||
if [[ ${#WHITELIST_PATTERNS[@]} -gt 0 ]]; then
|
||||
for w in "${WHITELIST_PATTERNS[@]}"; do
|
||||
# Match both exact path and glob pattern
|
||||
if [[ "$path" == "$w" ]] || [[ "$path" == $w ]]; then
|
||||
if [[ "$path" == "$w" ]] || [[ "$path" == "$w" ]]; then
|
||||
skip=true
|
||||
((skipped_count++))
|
||||
break
|
||||
@@ -239,7 +239,7 @@ safe_clean() {
|
||||
[[ "$skip" == "true" ]] && continue
|
||||
[[ -e "$path" ]] && existing_paths+=("$path")
|
||||
done
|
||||
|
||||
|
||||
# Update global whitelist skip counter
|
||||
if [[ $skipped_count -gt 0 ]]; then
|
||||
((whitelist_skipped_count += skipped_count))
|
||||
@@ -253,31 +253,34 @@ safe_clean() {
|
||||
# Show progress indicator for potentially slow operations
|
||||
if [[ ${#existing_paths[@]} -gt 3 ]]; then
|
||||
if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking items with whitelist safety..."; fi
|
||||
local temp_dir=$(create_temp_dir)
|
||||
local temp_dir
|
||||
temp_dir=$(create_temp_dir)
|
||||
|
||||
# Parallel processing (bash 3.2 compatible)
|
||||
local -a pids=()
|
||||
local idx=0
|
||||
for path in "${existing_paths[@]}"; do
|
||||
(
|
||||
local size=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||
local size
|
||||
size=$(du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
local count
|
||||
count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ')
|
||||
# Use index + PID for unique filename
|
||||
local tmp_file="$temp_dir/result_${idx}.$$"
|
||||
echo "$size $count" > "$tmp_file"
|
||||
mv "$tmp_file" "$temp_dir/result_${idx}" 2>/dev/null || true
|
||||
mv "$tmp_file" "$temp_dir/result_${idx}" 2> /dev/null || true
|
||||
) &
|
||||
pids+=($!)
|
||||
((idx++))
|
||||
|
||||
if (( ${#pids[@]} >= MAX_PARALLEL_JOBS )); then
|
||||
wait "${pids[0]}" 2>/dev/null || true
|
||||
if ((${#pids[@]} >= MAX_PARALLEL_JOBS)); then
|
||||
wait "${pids[0]}" 2> /dev/null || true
|
||||
pids=("${pids[@]:1}")
|
||||
fi
|
||||
done
|
||||
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2> /dev/null || true
|
||||
done
|
||||
|
||||
# Read results using same index
|
||||
@@ -285,10 +288,10 @@ safe_clean() {
|
||||
for path in "${existing_paths[@]}"; do
|
||||
local result_file="$temp_dir/result_${idx}"
|
||||
if [[ -f "$result_file" ]]; then
|
||||
read -r size count < "$result_file" 2>/dev/null || true
|
||||
read -r size count < "$result_file" 2> /dev/null || true
|
||||
if [[ "$count" -gt 0 && "$size" -gt 0 ]]; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
rm -rf "$path" 2>/dev/null || true
|
||||
rm -rf "$path" 2> /dev/null || true
|
||||
fi
|
||||
((total_size_bytes += size))
|
||||
((total_count += count))
|
||||
@@ -304,12 +307,14 @@ safe_clean() {
|
||||
if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking items with whitelist safety..."; fi
|
||||
|
||||
for path in "${existing_paths[@]}"; do
|
||||
local size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||
local size_bytes
|
||||
size_bytes=$(du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
local count
|
||||
count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$count" -gt 0 && "$size_bytes" -gt 0 ]]; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
rm -rf "$path" 2>/dev/null || true
|
||||
rm -rf "$path" 2> /dev/null || true
|
||||
fi
|
||||
((total_size_bytes += size_bytes))
|
||||
((total_count += count))
|
||||
@@ -319,7 +324,10 @@ safe_clean() {
|
||||
fi
|
||||
|
||||
# Clear progress / stop spinner before showing result
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; echo -ne "\r\033[K"; fi
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
echo -ne "\r\033[K"
|
||||
fi
|
||||
|
||||
if [[ $removed_any -eq 1 ]]; then
|
||||
# Convert KB to bytes for bytes_to_human()
|
||||
@@ -335,8 +343,8 @@ safe_clean() {
|
||||
else
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}($size_human)${NC}"
|
||||
fi
|
||||
((files_cleaned+=total_count))
|
||||
((total_size_cleaned+=total_size_bytes))
|
||||
((files_cleaned += total_count))
|
||||
((total_size_cleaned += total_size_bytes))
|
||||
((total_items++))
|
||||
note_activity
|
||||
fi
|
||||
@@ -349,7 +357,7 @@ start_cleanup() {
|
||||
clear
|
||||
printf '\n'
|
||||
echo -e "${PURPLE}Clean Your Mac${NC}"
|
||||
|
||||
|
||||
if [[ "$DRY_RUN" != "true" && -t 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}Tip:${NC} Safety first—run 'mo clean --dry-run'. Important Macs should stop."
|
||||
@@ -384,7 +392,7 @@ start_cleanup() {
|
||||
|
||||
# Enter = yes, do system cleanup
|
||||
if [[ -z "$choice" ]] || [[ "$choice" == $'\n' ]]; then
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
if request_sudo_access "System cleanup requires admin access"; then
|
||||
SYSTEM_CLEAN=true
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Admin access granted"
|
||||
@@ -393,7 +401,7 @@ start_cleanup() {
|
||||
(
|
||||
local retry_count=0
|
||||
while true; do
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
if ! sudo -n true 2> /dev/null; then
|
||||
((retry_count++))
|
||||
if [[ $retry_count -ge 3 ]]; then
|
||||
exit 1
|
||||
@@ -403,9 +411,9 @@ start_cleanup() {
|
||||
fi
|
||||
retry_count=0
|
||||
sleep 30
|
||||
kill -0 "$$" 2>/dev/null || exit
|
||||
kill -0 "$$" 2> /dev/null || exit
|
||||
done
|
||||
) 2>/dev/null &
|
||||
) 2> /dev/null &
|
||||
SUDO_KEEPALIVE_PID=$!
|
||||
else
|
||||
SYSTEM_CLEAN=false
|
||||
@@ -430,7 +438,7 @@ start_cleanup() {
|
||||
|
||||
perform_cleanup() {
|
||||
echo -e "${BLUE}${ICON_ADMIN}${NC} $(detect_architecture) | Free space: $(get_free_space)"
|
||||
|
||||
|
||||
# Show whitelist info if patterns are active
|
||||
local active_count=${#WHITELIST_PATTERNS[@]}
|
||||
if [[ $active_count -gt 2 ]]; then
|
||||
@@ -453,25 +461,25 @@ perform_cleanup() {
|
||||
start_section "Deep system-level cleanup"
|
||||
|
||||
# Clean system caches more safely
|
||||
sudo find /Library/Caches -name "*.cache" -delete 2>/dev/null || true
|
||||
sudo find /Library/Caches -name "*.tmp" -delete 2>/dev/null || true
|
||||
sudo find /Library/Caches -type f -name "*.log" -delete 2>/dev/null || true
|
||||
sudo find /Library/Caches -name "*.cache" -delete 2> /dev/null || true
|
||||
sudo find /Library/Caches -name "*.tmp" -delete 2> /dev/null || true
|
||||
sudo find /Library/Caches -type f -name "*.log" -delete 2> /dev/null || true
|
||||
|
||||
# Clean old temp files only (avoid breaking running processes)
|
||||
local tmp_cleaned=0
|
||||
local tmp_count=$(sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2>/dev/null | wc -l | tr -d ' ')
|
||||
local tmp_count=$(sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ "$tmp_count" -gt 0 ]]; then
|
||||
sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2>/dev/null || true
|
||||
sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2> /dev/null || true
|
||||
tmp_cleaned=1
|
||||
fi
|
||||
local var_tmp_count=$(sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2>/dev/null | wc -l | tr -d ' ')
|
||||
local var_tmp_count=$(sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2> /dev/null | wc -l | tr -d ' ')
|
||||
if [[ "$var_tmp_count" -gt 0 ]]; then
|
||||
sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2>/dev/null || true
|
||||
sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2> /dev/null || true
|
||||
tmp_cleaned=1
|
||||
fi
|
||||
[[ $tmp_cleaned -eq 1 ]] && log_success "Old system temp files (${TEMP_FILE_AGE_DAYS}+ days)"
|
||||
|
||||
sudo rm -rf /Library/Updates/* 2>/dev/null || true
|
||||
sudo rm -rf /Library/Updates/* 2> /dev/null || true
|
||||
log_success "System library caches and updates"
|
||||
|
||||
end_section
|
||||
@@ -497,15 +505,15 @@ perform_cleanup() {
|
||||
[[ -d "$volume" && -d "$volume/.Trashes" && -w "$volume" ]] || continue
|
||||
|
||||
# Skip network volumes
|
||||
local fs_type=$(df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}')
|
||||
local fs_type=$(df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}')
|
||||
case "$fs_type" in
|
||||
nfs|smbfs|afpfs|cifs|webdav) continue ;;
|
||||
nfs | smbfs | afpfs | cifs | webdav) continue ;;
|
||||
esac
|
||||
|
||||
# Verify volume is mounted
|
||||
if mount | grep -q "on $volume "; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2>/dev/null || true
|
||||
find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2> /dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
@@ -526,7 +534,6 @@ perform_cleanup() {
|
||||
safe_clean ~/Downloads/*.part "Incomplete downloads (partial)"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 3. macOS System Caches =====
|
||||
start_section "macOS system caches"
|
||||
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states"
|
||||
@@ -542,7 +549,6 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Application\ Support/CloudDocs/session/db/* "iCloud session cache"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 4. Sandboxed App Caches =====
|
||||
start_section "Sandboxed app caches"
|
||||
# Clean specific high-usage apps first for better user feedback
|
||||
@@ -553,7 +559,6 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Containers/*/Data/Library/Caches/* "Sandboxed app caches"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 5. Browsers =====
|
||||
start_section "Browser cleanup"
|
||||
# Safari (cache only, NOT local storage or databases to preserve login states)
|
||||
@@ -577,7 +582,6 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 6. Cloud Storage =====
|
||||
start_section "Cloud storage caches"
|
||||
# Only cache files, not sync state or login credentials
|
||||
@@ -590,7 +594,6 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Caches/com.microsoft.OneDrive "OneDrive cache"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 7. Office Applications =====
|
||||
start_section "Office applications"
|
||||
safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache"
|
||||
@@ -603,11 +606,10 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Caches/com.apple.mail/* "Apple Mail cache"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 8. Developer tools =====
|
||||
start_section "Developer tools"
|
||||
# Node.js ecosystem
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
if command -v npm > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
clean_tool_cache "npm cache" npm cache clean --force
|
||||
else
|
||||
@@ -622,7 +624,7 @@ perform_cleanup() {
|
||||
safe_clean ~/.bun/install/cache/* "Bun cache"
|
||||
|
||||
# Python ecosystem
|
||||
if command -v pip3 >/dev/null 2>&1; then
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
clean_tool_cache "pip cache" pip3 cache purge
|
||||
else
|
||||
@@ -636,7 +638,7 @@ perform_cleanup() {
|
||||
safe_clean ~/.pyenv/cache/* "pyenv cache"
|
||||
|
||||
# Go ecosystem
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
if command -v go > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
clean_tool_cache "Go cache" bash -c 'go clean -modcache >/dev/null 2>&1 || true; go clean -cache >/dev/null 2>&1 || true'
|
||||
else
|
||||
@@ -652,7 +654,7 @@ perform_cleanup() {
|
||||
safe_clean ~/.cargo/registry/cache/* "Rust cargo cache"
|
||||
|
||||
# Docker (only clean build cache, preserve images and volumes)
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if command -v docker > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
clean_tool_cache "Docker build cache" docker builder prune -af
|
||||
else
|
||||
@@ -674,9 +676,10 @@ perform_cleanup() {
|
||||
safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache"
|
||||
safe_clean /opt/homebrew/var/homebrew/locks/* "Homebrew lock files (M series)"
|
||||
safe_clean /usr/local/var/homebrew/locks/* "Homebrew lock files (Intel)"
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
if command -v brew > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
clean_tool_cache "Homebrew cleanup" brew cleanup
|
||||
# Use -s (scrub cache) for faster cleanup, --prune=all removes old versions
|
||||
MOLE_CMD_TIMEOUT=300 clean_tool_cache "Homebrew cleanup" brew cleanup -s --prune=all
|
||||
else
|
||||
echo -e " ${YELLOW}→${NC} Homebrew (would cleanup)"
|
||||
fi
|
||||
@@ -818,7 +821,6 @@ perform_cleanup() {
|
||||
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 10. Applications =====
|
||||
start_section "Applications"
|
||||
|
||||
@@ -983,7 +985,6 @@ perform_cleanup() {
|
||||
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 11. Virtualization Tools =====
|
||||
start_section "Virtualization tools"
|
||||
safe_clean ~/Library/Caches/com.vmware.fusion "VMware Fusion cache"
|
||||
@@ -992,7 +993,6 @@ perform_cleanup() {
|
||||
safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files"
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 12. Application Support logs cleanup =====
|
||||
start_section "Application Support logs"
|
||||
|
||||
@@ -1003,7 +1003,7 @@ perform_cleanup() {
|
||||
|
||||
# Skip system and protected apps
|
||||
case "$app_name" in
|
||||
com.apple.*|Adobe*|1Password|Claude)
|
||||
com.apple.* | Adobe* | 1Password | Claude)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
@@ -1022,7 +1022,6 @@ perform_cleanup() {
|
||||
|
||||
end_section
|
||||
|
||||
|
||||
# ===== 13. Orphaned app data cleanup =====
|
||||
# Deep cleanup of leftover files from uninstalled apps
|
||||
#
|
||||
@@ -1074,32 +1073,32 @@ perform_cleanup() {
|
||||
if [[ -d "$search_path" ]]; then
|
||||
while IFS= read -r app; do
|
||||
[[ -f "$app/Contents/Info.plist" ]] || continue
|
||||
bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "")
|
||||
bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "")
|
||||
[[ -n "$bundle_id" ]] && echo "$bundle_id" >> "$installed_bundles"
|
||||
done < <(find "$search_path" -maxdepth 3 -type d -name "*.app" 2>/dev/null || true)
|
||||
done < <(find "$search_path" -maxdepth 3 -type d -name "*.app" 2> /dev/null || true)
|
||||
fi
|
||||
done
|
||||
|
||||
# Use Spotlight as fallback to catch apps in unusual locations
|
||||
# This significantly reduces false positives
|
||||
if command -v mdfind >/dev/null 2>&1; then
|
||||
if command -v mdfind > /dev/null 2>&1; then
|
||||
while IFS= read -r app; do
|
||||
[[ -f "$app/Contents/Info.plist" ]] || continue
|
||||
bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "")
|
||||
bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "")
|
||||
[[ -n "$bundle_id" ]] && echo "$bundle_id" >> "$installed_bundles"
|
||||
done < <(mdfind "kMDItemKind == 'Application'" 2>/dev/null | grep "\.app$" || true)
|
||||
done < <(mdfind "kMDItemKind == 'Application'" 2> /dev/null | grep "\.app$" || true)
|
||||
fi
|
||||
|
||||
# Get running applications (if an app is running, it's definitely not orphaned)
|
||||
local running_apps=$(osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2>/dev/null || echo "")
|
||||
local running_apps=$(osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "")
|
||||
echo "$running_apps" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' > "$running_bundles"
|
||||
|
||||
# Check LaunchAgents and LaunchDaemons (if app has launch items, it likely exists)
|
||||
find ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons \
|
||||
-name "*.plist" -type f 2>/dev/null | while IFS= read -r plist; do
|
||||
-name "*.plist" -type f 2> /dev/null | while IFS= read -r plist; do
|
||||
bundle_id=$(basename "$plist" .plist)
|
||||
echo "$bundle_id" >> "$launch_agents"
|
||||
done 2>/dev/null || true
|
||||
done 2> /dev/null || true
|
||||
|
||||
# Combine and deduplicate all bundle IDs
|
||||
sort -u "$installed_bundles" "$running_bundles" "$launch_agents" > "${installed_bundles}.final"
|
||||
@@ -1117,7 +1116,7 @@ perform_cleanup() {
|
||||
# Returns 0 (true) only if we are VERY CONFIDENT the app is uninstalled
|
||||
is_orphaned() {
|
||||
local bundle_id="$1"
|
||||
local directory_path="$2" # The actual directory we're considering deleting
|
||||
local directory_path="$2" # The actual directory we're considering deleting
|
||||
|
||||
# SAFETY CHECK 1: Skip system-critical and protected apps (MOST IMPORTANT)
|
||||
if should_protect_data "$bundle_id"; then
|
||||
@@ -1125,13 +1124,13 @@ perform_cleanup() {
|
||||
fi
|
||||
|
||||
# SAFETY CHECK 2: Check if app bundle exists in our comprehensive scan
|
||||
if grep -q "^$bundle_id$" "$installed_bundles" 2>/dev/null; then
|
||||
if grep -q "^$bundle_id$" "$installed_bundles" 2> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# SAFETY CHECK 3: Extra check for system bundles (belt and suspenders)
|
||||
case "$bundle_id" in
|
||||
com.apple.*|loginwindow|dock|systempreferences|finder|safari)
|
||||
com.apple.* | loginwindow | dock | systempreferences | finder | safari)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
@@ -1139,7 +1138,7 @@ perform_cleanup() {
|
||||
# SAFETY CHECK 4: If it's a very common/important prefix, be extra careful
|
||||
# For major vendors, we NEVER auto-clean (too risky)
|
||||
case "$bundle_id" in
|
||||
com.adobe.*|com.microsoft.*|com.google.*|org.mozilla.*|com.jetbrains.*|com.docker.*)
|
||||
com.adobe.* | com.microsoft.* | com.google.* | org.mozilla.* | com.jetbrains.* | com.docker.*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
@@ -1149,9 +1148,9 @@ perform_cleanup() {
|
||||
# This protects against apps in unusual locations we didn't scan
|
||||
if [[ -e "$directory_path" ]]; then
|
||||
# Get last access time (days ago)
|
||||
local last_access_epoch=$(stat -f%a "$directory_path" 2>/dev/null || echo "0")
|
||||
local last_access_epoch=$(stat -f%a "$directory_path" 2> /dev/null || echo "0")
|
||||
local current_epoch=$(date +%s)
|
||||
local days_since_access=$(( (current_epoch - last_access_epoch) / 86400 ))
|
||||
local days_since_access=$(((current_epoch - last_access_epoch) / 86400))
|
||||
|
||||
# If accessed in the last 60 days, DO NOT DELETE
|
||||
# This means app is likely still installed somewhere
|
||||
@@ -1167,12 +1166,12 @@ perform_cleanup() {
|
||||
# Clean orphaned caches
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned caches..."
|
||||
local cache_found=0
|
||||
if ls ~/Library/Caches/com.* >/dev/null 2>&1; then
|
||||
if ls ~/Library/Caches/com.* > /dev/null 2>&1; then
|
||||
for cache_dir in ~/Library/Caches/com.* ~/Library/Caches/org.* ~/Library/Caches/net.* ~/Library/Caches/io.*; do
|
||||
[[ -d "$cache_dir" ]] || continue
|
||||
local bundle_id=$(basename "$cache_dir")
|
||||
if is_orphaned "$bundle_id" "$cache_dir"; then
|
||||
local size_kb=$(du -sk "$cache_dir" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$cache_dir" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
safe_clean "$cache_dir" "Orphaned cache: $bundle_id"
|
||||
((cache_found++))
|
||||
@@ -1187,12 +1186,12 @@ perform_cleanup() {
|
||||
# Clean orphaned logs
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned logs..."
|
||||
local logs_found=0
|
||||
if ls ~/Library/Logs/com.* >/dev/null 2>&1; then
|
||||
if ls ~/Library/Logs/com.* > /dev/null 2>&1; then
|
||||
for log_dir in ~/Library/Logs/com.* ~/Library/Logs/org.* ~/Library/Logs/net.* ~/Library/Logs/io.*; do
|
||||
[[ -d "$log_dir" ]] || continue
|
||||
local bundle_id=$(basename "$log_dir")
|
||||
if is_orphaned "$bundle_id" "$log_dir"; then
|
||||
local size_kb=$(du -sk "$log_dir" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$log_dir" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
safe_clean "$log_dir" "Orphaned logs: $bundle_id"
|
||||
((logs_found++))
|
||||
@@ -1207,12 +1206,12 @@ perform_cleanup() {
|
||||
# Clean orphaned saved states
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned saved states..."
|
||||
local states_found=0
|
||||
if ls ~/Library/Saved\ Application\ State/*.savedState >/dev/null 2>&1; then
|
||||
if ls ~/Library/Saved\ Application\ State/*.savedState > /dev/null 2>&1; then
|
||||
for state_dir in ~/Library/Saved\ Application\ State/*.savedState; do
|
||||
[[ -d "$state_dir" ]] || continue
|
||||
local bundle_id=$(basename "$state_dir" .savedState)
|
||||
if is_orphaned "$bundle_id" "$state_dir"; then
|
||||
local size_kb=$(du -sk "$state_dir" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$state_dir" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
safe_clean "$state_dir" "Orphaned state: $bundle_id"
|
||||
((states_found++))
|
||||
@@ -1231,13 +1230,13 @@ perform_cleanup() {
|
||||
# To avoid deleting data from installed apps, we skip container cleanup.
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned containers..."
|
||||
local containers_found=0
|
||||
if ls ~/Library/Containers/com.* >/dev/null 2>&1; then
|
||||
if ls ~/Library/Containers/com.* > /dev/null 2>&1; then
|
||||
# Count potential orphaned containers but don't delete them
|
||||
for container_dir in ~/Library/Containers/com.* ~/Library/Containers/org.* ~/Library/Containers/net.* ~/Library/Containers/io.*; do
|
||||
[[ -d "$container_dir" ]] || continue
|
||||
local bundle_id=$(basename "$container_dir")
|
||||
if is_orphaned "$bundle_id" "$container_dir"; then
|
||||
local size_kb=$(du -sk "$container_dir" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$container_dir" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
# DISABLED: safe_clean "$container_dir" "Orphaned container: $bundle_id"
|
||||
((containers_found++))
|
||||
@@ -1252,12 +1251,12 @@ perform_cleanup() {
|
||||
# Clean orphaned WebKit data
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned WebKit data..."
|
||||
local webkit_found=0
|
||||
if ls ~/Library/WebKit/com.* >/dev/null 2>&1; then
|
||||
if ls ~/Library/WebKit/com.* > /dev/null 2>&1; then
|
||||
for webkit_dir in ~/Library/WebKit/com.* ~/Library/WebKit/org.* ~/Library/WebKit/net.* ~/Library/WebKit/io.*; do
|
||||
[[ -d "$webkit_dir" ]] || continue
|
||||
local bundle_id=$(basename "$webkit_dir")
|
||||
if is_orphaned "$bundle_id" "$webkit_dir"; then
|
||||
local size_kb=$(du -sk "$webkit_dir" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$webkit_dir" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
safe_clean "$webkit_dir" "Orphaned WebKit: $bundle_id"
|
||||
((webkit_found++))
|
||||
@@ -1272,12 +1271,12 @@ perform_cleanup() {
|
||||
# Clean orphaned HTTP storages
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned HTTP storages..."
|
||||
local http_found=0
|
||||
if ls ~/Library/HTTPStorages/com.* >/dev/null 2>&1; then
|
||||
if ls ~/Library/HTTPStorages/com.* > /dev/null 2>&1; then
|
||||
for http_dir in ~/Library/HTTPStorages/com.* ~/Library/HTTPStorages/org.* ~/Library/HTTPStorages/net.* ~/Library/HTTPStorages/io.*; do
|
||||
[[ -d "$http_dir" ]] || continue
|
||||
local bundle_id=$(basename "$http_dir")
|
||||
if is_orphaned "$bundle_id" "$http_dir"; then
|
||||
local size_kb=$(du -sk "$http_dir" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$http_dir" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
safe_clean "$http_dir" "Orphaned HTTP storage: $bundle_id"
|
||||
((http_found++))
|
||||
@@ -1292,12 +1291,12 @@ perform_cleanup() {
|
||||
# Clean orphaned cookies
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned cookies..."
|
||||
local cookies_found=0
|
||||
if ls ~/Library/Cookies/*.binarycookies >/dev/null 2>&1; then
|
||||
if ls ~/Library/Cookies/*.binarycookies > /dev/null 2>&1; then
|
||||
for cookie_file in ~/Library/Cookies/*.binarycookies; do
|
||||
[[ -f "$cookie_file" ]] || continue
|
||||
local bundle_id=$(basename "$cookie_file" .binarycookies)
|
||||
if is_orphaned "$bundle_id" "$cookie_file"; then
|
||||
local size_kb=$(du -sk "$cookie_file" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$cookie_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
safe_clean "$cookie_file" "Orphaned cookies: $bundle_id"
|
||||
((cookies_found++))
|
||||
@@ -1340,9 +1339,9 @@ perform_cleanup() {
|
||||
start_section "iOS device backups"
|
||||
backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
||||
if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then
|
||||
backup_kb=$(du -sk "$backup_dir" 2>/dev/null | awk '{print $1}')
|
||||
backup_kb=$(du -sk "$backup_dir" 2> /dev/null | awk '{print $1}')
|
||||
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then
|
||||
backup_human=$(du -sh "$backup_dir" 2>/dev/null | awk '{print $1}')
|
||||
backup_human=$(du -sh "$backup_dir" 2> /dev/null | awk '{print $1}')
|
||||
note_activity
|
||||
echo -e " Found ${GREEN}${backup_human}${NC} iOS backups"
|
||||
echo -e " You can delete them manually: ${backup_dir}"
|
||||
@@ -1361,9 +1360,9 @@ perform_cleanup() {
|
||||
|
||||
# Skip system volume and network volumes
|
||||
[[ "$volume" == "/Volumes/MacintoshHD" || "$volume" == "/" ]] && continue
|
||||
local fs_type=$(df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}')
|
||||
local fs_type=$(df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}')
|
||||
case "$fs_type" in
|
||||
nfs|smbfs|afpfs|cifs|webdav) continue ;;
|
||||
nfs | smbfs | afpfs | cifs | webdav) continue ;;
|
||||
esac
|
||||
|
||||
# Look for HFS+ style backups (Backups.backupdb)
|
||||
@@ -1374,19 +1373,19 @@ perform_cleanup() {
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
|
||||
local size_kb=$(du -sk "$inprogress_file" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$inprogress_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
# Use tmutil to safely delete the failed backup
|
||||
if command -v tmutil >/dev/null 2>&1; then
|
||||
if tmutil delete "$inprogress_file" 2>/dev/null; then
|
||||
if command -v tmutil > /dev/null 2>&1; then
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Failed backup: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned+=size_kb))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
note_activity
|
||||
else
|
||||
@@ -1402,7 +1401,7 @@ perform_cleanup() {
|
||||
note_activity
|
||||
fi
|
||||
fi
|
||||
done < <(find "$backupdb_dir" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2>/dev/null || true)
|
||||
done < <(find "$backupdb_dir" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
|
||||
# Look for APFS style backups (.backupbundle or .sparsebundle)
|
||||
@@ -1414,25 +1413,25 @@ perform_cleanup() {
|
||||
|
||||
# Check if bundle is already mounted by looking at hdiutil info
|
||||
local bundle_name=$(basename "$bundle")
|
||||
local mounted_path=$(hdiutil info 2>/dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
|
||||
local mounted_path=$(hdiutil info 2> /dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
|
||||
|
||||
if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then
|
||||
# Bundle is already mounted, safe to check
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
|
||||
local size_kb=$(du -sk "$inprogress_file" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
local size_kb=$(du -sk "$inprogress_file" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if command -v tmutil >/dev/null 2>&1; then
|
||||
if tmutil delete "$inprogress_file" 2>/dev/null; then
|
||||
if command -v tmutil > /dev/null 2>&1; then
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Failed APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned+=size_kb))
|
||||
((total_size_cleaned += size_kb))
|
||||
((total_items++))
|
||||
note_activity
|
||||
else
|
||||
@@ -1446,7 +1445,7 @@ perform_cleanup() {
|
||||
note_activity
|
||||
fi
|
||||
fi
|
||||
done < <(find "$mounted_path" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2>/dev/null || true)
|
||||
done < <(find "$mounted_path" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -1525,12 +1524,11 @@ perform_cleanup() {
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
|
||||
main() {
|
||||
# Parse args (only dry-run and help for minimal impact)
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
"--dry-run"|"-n")
|
||||
"--dry-run" | "-n")
|
||||
DRY_RUN=true
|
||||
;;
|
||||
"--whitelist")
|
||||
@@ -1538,7 +1536,7 @@ main() {
|
||||
manage_whitelist
|
||||
exit 0
|
||||
;;
|
||||
"--help"|"-h")
|
||||
"--help" | "-h")
|
||||
echo "Mole - Deeper system cleanup"
|
||||
echo "Usage: clean.sh [options]"
|
||||
echo ""
|
||||
|
||||
@@ -20,14 +20,14 @@ is_touchid_configured() {
|
||||
if [[ ! -f "$PAM_SUDO_FILE" ]]; then
|
||||
return 1
|
||||
fi
|
||||
grep -q "pam_tid.so" "$PAM_SUDO_FILE" 2>/dev/null
|
||||
grep -q "pam_tid.so" "$PAM_SUDO_FILE" 2> /dev/null
|
||||
}
|
||||
|
||||
# Check if system supports Touch ID
|
||||
supports_touchid() {
|
||||
# Check if bioutil exists and has Touch ID capability
|
||||
if command -v bioutil &>/dev/null; then
|
||||
bioutil -r 2>/dev/null | grep -q "Touch ID" && return 0
|
||||
if command -v bioutil &> /dev/null; then
|
||||
bioutil -r 2> /dev/null | grep -q "Touch ID" && return 0
|
||||
fi
|
||||
|
||||
# Fallback: check if running on Apple Silicon or modern Intel Mac
|
||||
@@ -39,7 +39,7 @@ supports_touchid() {
|
||||
|
||||
# For Intel Macs, check if it's 2018 or later (approximation)
|
||||
local model_year
|
||||
model_year=$(system_profiler SPHardwareDataType 2>/dev/null | grep "Model Identifier" | grep -o "[0-9]\{4\}" | head -1)
|
||||
model_year=$(system_profiler SPHardwareDataType 2> /dev/null | grep "Model Identifier" | grep -o "[0-9]\{4\}" | head -1)
|
||||
if [[ -n "$model_year" ]] && [[ "$model_year" -ge 2018 ]]; then
|
||||
return 0
|
||||
fi
|
||||
@@ -76,7 +76,7 @@ enable_touchid() {
|
||||
fi
|
||||
|
||||
# Create backup and apply changes
|
||||
if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2>/dev/null; then
|
||||
if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2> /dev/null; then
|
||||
log_error "Failed to create backup"
|
||||
return 1
|
||||
fi
|
||||
@@ -97,12 +97,12 @@ enable_touchid() {
|
||||
' "$PAM_SUDO_FILE" > "$temp_file"
|
||||
|
||||
# Apply the changes
|
||||
if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2>/dev/null; then
|
||||
if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS} Touch ID enabled${NC} ${GRAY}- try: sudo ls${NC}"
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
rm -f "$temp_file" 2>/dev/null || true
|
||||
rm -f "$temp_file" 2> /dev/null || true
|
||||
log_error "Failed to enable Touch ID"
|
||||
return 1
|
||||
fi
|
||||
@@ -116,7 +116,7 @@ disable_touchid() {
|
||||
fi
|
||||
|
||||
# Create backup and remove configuration
|
||||
if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2>/dev/null; then
|
||||
if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2> /dev/null; then
|
||||
log_error "Failed to create backup"
|
||||
return 1
|
||||
fi
|
||||
@@ -126,12 +126,12 @@ disable_touchid() {
|
||||
temp_file=$(mktemp)
|
||||
grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file"
|
||||
|
||||
if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2>/dev/null; then
|
||||
if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled${NC}"
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
rm -f "$temp_file" 2>/dev/null || true
|
||||
rm -f "$temp_file" 2> /dev/null || true
|
||||
log_error "Failed to disable Touch ID"
|
||||
return 1
|
||||
fi
|
||||
@@ -174,11 +174,11 @@ show_menu() {
|
||||
echo ""
|
||||
|
||||
case "$key" in
|
||||
$'\e') # ESC
|
||||
$'\e') # ESC
|
||||
return 0
|
||||
;;
|
||||
""|$'\n'|$'\r') # Enter
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
"" | $'\n' | $'\r') # Enter
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
disable_touchid
|
||||
;;
|
||||
*)
|
||||
@@ -191,11 +191,11 @@ show_menu() {
|
||||
IFS= read -r -s -n1 key || key=""
|
||||
|
||||
case "$key" in
|
||||
$'\e') # ESC
|
||||
$'\e') # ESC
|
||||
return 0
|
||||
;;
|
||||
""|$'\n'|$'\r') # Enter
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
"" | $'\n' | $'\r') # Enter
|
||||
printf "\r\033[K" # Clear the prompt line
|
||||
enable_touchid
|
||||
;;
|
||||
*)
|
||||
@@ -220,7 +220,7 @@ main() {
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
help|--help|-h)
|
||||
help | --help | -h)
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
|
||||
124
bin/uninstall.sh
124
bin/uninstall.sh
@@ -56,10 +56,9 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
fi
|
||||
|
||||
# Initialize global variables
|
||||
selected_apps=() # Global array for app selection
|
||||
selected_apps=() # Global array for app selection
|
||||
declare -a apps_data=()
|
||||
declare -a selection_state=()
|
||||
current_line=0
|
||||
total_items=0
|
||||
files_cleaned=0
|
||||
total_size_cleaned=0
|
||||
@@ -68,16 +67,16 @@ total_size_cleaned=0
|
||||
get_app_last_used() {
|
||||
local app_path="$1"
|
||||
local last_used
|
||||
last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||
last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2> /dev/null)
|
||||
|
||||
if [[ "$last_used" == "(null)" || -z "$last_used" ]]; then
|
||||
echo "Never"
|
||||
else
|
||||
local last_used_epoch
|
||||
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2>/dev/null)
|
||||
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2> /dev/null)
|
||||
local current_epoch
|
||||
current_epoch=$(date "+%s")
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
local days_ago=$(((current_epoch - last_used_epoch) / 86400))
|
||||
|
||||
if [[ $days_ago -eq 0 ]]; then
|
||||
echo "Today"
|
||||
@@ -86,10 +85,10 @@ get_app_last_used() {
|
||||
elif [[ $days_ago -lt 30 ]]; then
|
||||
echo "${days_ago} days ago"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
local months_ago=$(( days_ago / 30 ))
|
||||
local months_ago=$((days_ago / 30))
|
||||
echo "${months_ago} month(s) ago"
|
||||
else
|
||||
local years_ago=$(( days_ago / 365 ))
|
||||
local years_ago=$((days_ago / 365))
|
||||
echo "${years_ago} year(s) ago"
|
||||
fi
|
||||
fi
|
||||
@@ -101,22 +100,24 @@ scan_applications() {
|
||||
local cache_dir="$HOME/.cache/mole"
|
||||
local cache_file="$cache_dir/app_scan_cache"
|
||||
local cache_meta="$cache_dir/app_scan_meta"
|
||||
local cache_ttl=3600 # 1 hour cache validity
|
||||
local cache_ttl=3600 # 1 hour cache validity
|
||||
|
||||
mkdir -p "$cache_dir" 2>/dev/null
|
||||
mkdir -p "$cache_dir" 2> /dev/null
|
||||
|
||||
# Quick count of current apps (system + user directories)
|
||||
local current_app_count
|
||||
current_app_count=$(
|
||||
(find /Applications -name "*.app" -maxdepth 1 2>/dev/null;
|
||||
find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' '
|
||||
(
|
||||
find /Applications -name "*.app" -maxdepth 1 2> /dev/null
|
||||
find ~/Applications -name "*.app" -maxdepth 1 2> /dev/null
|
||||
) | wc -l | tr -d ' '
|
||||
)
|
||||
|
||||
# Check if cache is valid unless explicitly disabled
|
||||
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
|
||||
cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
|
||||
cached_app_count=$(cat "$cache_meta" 2> /dev/null || echo "0")
|
||||
|
||||
# Cache is valid if: age < TTL AND app count matches
|
||||
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
|
||||
@@ -149,26 +150,26 @@ scan_applications() {
|
||||
local bundle_id="unknown"
|
||||
local display_name="$app_name"
|
||||
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
||||
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown")
|
||||
bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown")
|
||||
|
||||
# Try to get English name from bundle info
|
||||
local bundle_executable
|
||||
bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null)
|
||||
bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null)
|
||||
|
||||
# Smart display name selection - prefer descriptive names over generic ones
|
||||
local candidates=()
|
||||
|
||||
# Get all potential names
|
||||
local bundle_display_name
|
||||
bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||
bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null)
|
||||
local bundle_name
|
||||
bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
||||
bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null)
|
||||
|
||||
# Check if executable name is generic/technical (should be avoided)
|
||||
local is_generic_executable=false
|
||||
if [[ -n "$bundle_executable" ]]; then
|
||||
case "$bundle_executable" in
|
||||
"pake"|"Electron"|"electron"|"nwjs"|"node"|"helper"|"main"|"app"|"binary")
|
||||
"pake" | "Electron" | "electron" | "nwjs" | "node" | "helper" | "main" | "app" | "binary")
|
||||
is_generic_executable=true
|
||||
;;
|
||||
esac
|
||||
@@ -219,19 +220,19 @@ scan_applications() {
|
||||
app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}")
|
||||
done < <(
|
||||
# Scan both system and user application directories
|
||||
find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null
|
||||
find ~/Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null
|
||||
find /Applications -name "*.app" -maxdepth 1 -print0 2> /dev/null
|
||||
find ~/Applications -name "*.app" -maxdepth 1 -print0 2> /dev/null
|
||||
)
|
||||
|
||||
# Second pass: process each app with parallel size calculation
|
||||
local app_count=0
|
||||
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 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
|
||||
printf "\033[H" >&2 # Position cursor at top of screen
|
||||
fi
|
||||
|
||||
# Process app metadata extraction function
|
||||
@@ -245,7 +246,7 @@ scan_applications() {
|
||||
# Parallel size calculation
|
||||
local app_size="N/A"
|
||||
if [[ -d "$app_path" ]]; then
|
||||
app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A")
|
||||
app_size=$(du -sh "$app_path" 2> /dev/null | cut -f1 || echo "N/A")
|
||||
fi
|
||||
|
||||
# Get real last used date from macOS metadata
|
||||
@@ -254,13 +255,13 @@ scan_applications() {
|
||||
|
||||
if [[ -d "$app_path" ]]; then
|
||||
local metadata_date
|
||||
metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||
metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2> /dev/null)
|
||||
|
||||
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
||||
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0")
|
||||
last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2> /dev/null || echo "0")
|
||||
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
local days_ago=$(((current_epoch - last_used_epoch) / 86400))
|
||||
|
||||
if [[ $days_ago -eq 0 ]]; then
|
||||
last_used="Today"
|
||||
@@ -269,21 +270,21 @@ scan_applications() {
|
||||
elif [[ $days_ago -lt 7 ]]; then
|
||||
last_used="${days_ago} days ago"
|
||||
elif [[ $days_ago -lt 30 ]]; then
|
||||
local weeks_ago=$(( days_ago / 7 ))
|
||||
local weeks_ago=$((days_ago / 7))
|
||||
[[ $weeks_ago -eq 1 ]] && last_used="1 week ago" || last_used="${weeks_ago} weeks ago"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
local months_ago=$(( days_ago / 30 ))
|
||||
local months_ago=$((days_ago / 30))
|
||||
[[ $months_ago -eq 1 ]] && last_used="1 month ago" || last_used="${months_ago} months ago"
|
||||
else
|
||||
local years_ago=$(( days_ago / 365 ))
|
||||
local years_ago=$((days_ago / 365))
|
||||
[[ $years_ago -eq 1 ]] && last_used="1 year ago" || last_used="${years_ago} years ago"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Fallback to file modification time
|
||||
last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0")
|
||||
last_used_epoch=$(stat -f%m "$app_path" 2> /dev/null || echo "0")
|
||||
if [[ $last_used_epoch -gt 0 ]]; then
|
||||
local days_ago=$(( (current_epoch - last_used_epoch) / 86400 ))
|
||||
local days_ago=$(((current_epoch - last_used_epoch) / 86400))
|
||||
if [[ $days_ago -lt 30 ]]; then
|
||||
last_used="Recent"
|
||||
elif [[ $days_ago -lt 365 ]]; then
|
||||
@@ -319,15 +320,15 @@ scan_applications() {
|
||||
((spinner_idx++))
|
||||
|
||||
# Wait if we've hit max parallel limit
|
||||
if (( ${#pids[@]} >= max_parallel )); then
|
||||
wait "${pids[0]}" 2>/dev/null
|
||||
pids=("${pids[@]:1}") # Remove first pid
|
||||
if ((${#pids[@]} >= max_parallel)); then
|
||||
wait "${pids[0]}" 2> /dev/null
|
||||
pids=("${pids[@]:1}") # Remove first pid
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait for remaining background processes
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" 2>/dev/null
|
||||
wait "$pid" 2> /dev/null
|
||||
done
|
||||
|
||||
# Check if we found any applications
|
||||
@@ -347,12 +348,15 @@ scan_applications() {
|
||||
fi
|
||||
|
||||
# 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"
|
||||
|
||||
# Update cache with app count metadata
|
||||
cp "${temp_file}.sorted" "$cache_file" 2>/dev/null || true
|
||||
echo "$current_app_count" > "$cache_meta" 2>/dev/null || true
|
||||
cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true
|
||||
echo "$current_app_count" > "$cache_meta" 2> /dev/null || true
|
||||
|
||||
# Verify sorted file exists before returning
|
||||
if [[ -f "${temp_file}.sorted" ]]; then
|
||||
@@ -415,12 +419,12 @@ uninstall_applications() {
|
||||
echo ""
|
||||
|
||||
# Check if app is running (use app path for precise matching)
|
||||
if pgrep -f "$app_path" >/dev/null 2>&1; then
|
||||
if pgrep -f "$app_path" > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}${ICON_ERROR} $app_name is currently running${NC}"
|
||||
read -p " Force quit $app_name? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
pkill -f "$app_path" 2>/dev/null || true
|
||||
pkill -f "$app_path" 2> /dev/null || true
|
||||
sleep 2
|
||||
else
|
||||
echo -e " ${BLUE}${ICON_EMPTY}${NC} Skipped $app_name"
|
||||
@@ -438,7 +442,7 @@ uninstall_applications() {
|
||||
|
||||
# Calculate total size
|
||||
local app_size_kb
|
||||
app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||
app_size_kb=$(du -sk "$app_path" 2> /dev/null | awk '{print $1}' || echo "0")
|
||||
local related_size_kb
|
||||
related_size_kb=$(calculate_total_size "$related_files")
|
||||
local system_size_kb
|
||||
@@ -461,12 +465,13 @@ uninstall_applications() {
|
||||
done <<< "$system_files"
|
||||
fi
|
||||
|
||||
if [[ $total_kb -gt 1048576 ]]; then # > 1GB
|
||||
local size_display=$(echo "$total_kb" | awk '{printf "%.2fGB", $1/1024/1024}')
|
||||
elif [[ $total_kb -gt 1024 ]]; then # > 1MB
|
||||
local size_display=$(echo "$total_kb" | awk '{printf "%.1fMB", $1/1024}')
|
||||
local size_display
|
||||
if [[ $total_kb -gt 1048576 ]]; then # > 1GB
|
||||
size_display=$(echo "$total_kb" | awk '{printf "%.2fGB", $1/1024/1024}')
|
||||
elif [[ $total_kb -gt 1024 ]]; then # > 1MB
|
||||
size_display=$(echo "$total_kb" | awk '{printf "%.1fMB", $1/1024}')
|
||||
else
|
||||
local size_display="${total_kb}KB"
|
||||
size_display="${total_kb}KB"
|
||||
fi
|
||||
|
||||
echo -e " ${BLUE}Total size: $size_display${NC}"
|
||||
@@ -477,7 +482,7 @@ uninstall_applications() {
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# Remove the application
|
||||
if rm -rf "$app_path" 2>/dev/null; then
|
||||
if rm -rf "$app_path" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed application"
|
||||
else
|
||||
echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $app_path"
|
||||
@@ -487,7 +492,7 @@ uninstall_applications() {
|
||||
# Remove user-level related files
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" && -e "$file" ]]; then
|
||||
if rm -rf "$file" 2>/dev/null; then
|
||||
if rm -rf "$file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)"
|
||||
fi
|
||||
fi
|
||||
@@ -498,7 +503,7 @@ uninstall_applications() {
|
||||
echo -e " ${BLUE}${ICON_SOLID}${NC} Admin access required for system files"
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" && -e "$file" ]]; then
|
||||
if sudo rm -rf "$file" 2>/dev/null; then
|
||||
if sudo rm -rf "$file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(basename "$file")"
|
||||
else
|
||||
echo -e " ${YELLOW}${ICON_ERROR}${NC} Failed to remove: $file"
|
||||
@@ -521,12 +526,13 @@ uninstall_applications() {
|
||||
echo -e "${PURPLE}${ICON_ARROW} Uninstallation Summary${NC}"
|
||||
|
||||
if [[ $total_size_freed -gt 0 ]]; then
|
||||
if [[ $total_size_freed -gt 1048576 ]]; then # > 1GB
|
||||
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}')
|
||||
elif [[ $total_size_freed -gt 1024 ]]; then # > 1MB
|
||||
local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
|
||||
local freed_display
|
||||
if [[ $total_size_freed -gt 1048576 ]]; then # > 1GB
|
||||
freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}')
|
||||
elif [[ $total_size_freed -gt 1024 ]]; then # > 1MB
|
||||
freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}')
|
||||
else
|
||||
local freed_display="${total_size_freed}KB"
|
||||
freed_display="${total_size_freed}KB"
|
||||
fi
|
||||
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Freed $freed_display of disk space"
|
||||
@@ -544,8 +550,8 @@ cleanup() {
|
||||
unset MOLE_ALT_SCREEN_ACTIVE
|
||||
fi
|
||||
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
||||
kill "$sudo_keepalive_pid" 2>/dev/null || true
|
||||
wait "$sudo_keepalive_pid" 2>/dev/null || true
|
||||
kill "$sudo_keepalive_pid" 2> /dev/null || true
|
||||
wait "$sudo_keepalive_pid" 2> /dev/null || true
|
||||
sudo_keepalive_pid=""
|
||||
fi
|
||||
show_cursor
|
||||
@@ -634,7 +640,9 @@ main() {
|
||||
clear
|
||||
local selection_count=${#selected_apps[@]}
|
||||
if [[ $selection_count -eq 0 ]]; then
|
||||
echo "No apps selected"; rm -f "$apps_file"; return 0
|
||||
echo "No apps selected"
|
||||
rm -f "$apps_file"
|
||||
return 0
|
||||
fi
|
||||
# Show selected apps, max 3 per line
|
||||
echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):"
|
||||
@@ -644,7 +652,7 @@ main() {
|
||||
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
|
||||
local display_item="${app_name}(${size})"
|
||||
|
||||
if (( idx % 3 == 0 )); then
|
||||
if ((idx % 3 == 0)); then
|
||||
# Start new line
|
||||
[[ -n "$line" ]] && echo " $line"
|
||||
line="$display_item"
|
||||
|
||||
Reference in New Issue
Block a user