mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:39:42 +00:00
Complete automated testing
This commit is contained in:
45
.github/workflows/shell-format.yml
vendored
Normal file
45
.github/workflows/shell-format.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Shell Format & Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- '**/*.sh'
|
||||||
|
- mole
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/*.sh'
|
||||||
|
- mole
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format-lint:
|
||||||
|
runs-on: macos-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install shfmt and shellcheck
|
||||||
|
run: brew install shfmt shellcheck
|
||||||
|
|
||||||
|
- name: Run shfmt in diff mode
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mapfile -d '' files < <(find . -type f \( -name '*.sh' -o -name 'mole' \) \
|
||||||
|
! -path './.git/*' ! -path './tests/tmp-*' -print0)
|
||||||
|
if (( ${#files[@]} == 0 )); then
|
||||||
|
echo "No shell files found; skipping shfmt"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
shfmt -i 4 -ci -sr -d "${files[@]}"
|
||||||
|
|
||||||
|
- name: Run shellcheck
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mapfile -d '' files < <(find . -type f \( -name '*.sh' -o -name 'mole' \) \
|
||||||
|
! -path './.git/*' ! -path './tests/tmp-*' -print0)
|
||||||
|
if (( ${#files[@]} == 0 )); then
|
||||||
|
echo "No shell files found; skipping shellcheck"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
shellcheck --rcfile .shellcheckrc "${files[@]}"
|
||||||
10
.shellcheckrc
Normal file
10
.shellcheckrc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Mole project shellcheck configuration
|
||||||
|
#
|
||||||
|
# Keep the lint strict by default. Add rules to disable only when we have a
|
||||||
|
# clear justification.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# disable=SC2034 # unused variables (if intentionally unused)
|
||||||
|
# disable=SC1091 # sourcing files not present in repo (optional)
|
||||||
|
|
||||||
|
# Currently no global disables are required.
|
||||||
39
README.md
39
README.md
@@ -141,10 +141,47 @@ Total: 156.8GB
|
|||||||
└─ 📁 Desktop 12.7GB
|
└─ 📁 Desktop 12.7GB
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
Install development tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install shfmt shellcheck bats-core
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
Format and lint shell scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all scripts
|
||||||
|
./scripts/format.sh
|
||||||
|
|
||||||
|
# Check without modifying
|
||||||
|
./scripts/format.sh --check
|
||||||
|
|
||||||
|
# Install git hooks for auto-formatting
|
||||||
|
./scripts/install-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
See [scripts/README.md](scripts/README.md) for detailed development workflow.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Run automated tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tests/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub Actions automatically runs tests and formatting checks on PRs.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- If Mole reclaimed storage for you, consider starring the repo or sharing it with friends needing a cleaner Mac.
|
- If Mole reclaimed storage for you, consider starring the repo or sharing it with friends needing a cleaner Mac.
|
||||||
- Have ideas or fixes? Open an issue or PR and help shape Mole’s roadmap together with the community.
|
- Have ideas or fixes? Open an issue or PR and help shape Mole's roadmap together with the community.
|
||||||
- Love cats? Treat Tangyuan and Cola to canned food via <a href="https://miaoyan.app/cats.html?name=Mole" target="_blank">this link</a> and keep the mascots purring.
|
- Love cats? Treat Tangyuan and Cola to canned food via <a href="https://miaoyan.app/cats.html?name=Mole" target="_blank">this link</a> and keep the mascots purring.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
330
bin/analyze.sh
330
bin/analyze.sh
@@ -74,7 +74,8 @@ scan_large_files() {
|
|||||||
local file=""
|
local file=""
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
if [[ -f "$file" ]]; then
|
if [[ -f "$file" ]]; then
|
||||||
local size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
local size
|
||||||
|
size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||||
echo "$size|$file"
|
echo "$size|$file"
|
||||||
fi
|
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) | \
|
||||||
@@ -93,7 +94,8 @@ scan_medium_files() {
|
|||||||
local file=""
|
local file=""
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
if [[ -f "$file" ]]; then
|
if [[ -f "$file" ]]; then
|
||||||
local size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
local size
|
||||||
|
size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||||
echo "$size|$file"
|
echo "$size|$file"
|
||||||
fi
|
fi
|
||||||
done < <(mdfind -onlyin "$target_path" \
|
done < <(mdfind -onlyin "$target_path" \
|
||||||
@@ -158,7 +160,8 @@ aggregate_by_directory() {
|
|||||||
# Get cache file path for a directory
|
# Get cache file path for a directory
|
||||||
get_cache_file() {
|
get_cache_file() {
|
||||||
local target_path="$1"
|
local target_path="$1"
|
||||||
local path_hash=$(echo "$target_path" | md5 2>/dev/null || echo "$target_path" | shasum | cut -d' ' -f1)
|
local path_hash
|
||||||
|
path_hash=$(echo "$target_path" | md5 2>/dev/null || echo "$target_path" | shasum | cut -d' ' -f1)
|
||||||
echo "$CACHE_DIR/scan_${path_hash}.cache"
|
echo "$CACHE_DIR/scan_${path_hash}.cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +174,8 @@ is_cache_valid() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
local cache_age
|
||||||
|
cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
||||||
if [[ $cache_age -lt $max_age ]]; then
|
if [[ $cache_age -lt $max_age ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -236,7 +240,8 @@ perform_scan() {
|
|||||||
local force_rescan="${2:-false}"
|
local force_rescan="${2:-false}"
|
||||||
|
|
||||||
# Check cache first
|
# Check cache first
|
||||||
local cache_file=$(get_cache_file "$target_path")
|
local cache_file
|
||||||
|
cache_file=$(get_cache_file "$target_path")
|
||||||
if [[ "$force_rescan" != "true" ]] && is_cache_valid "$cache_file" 3600; then
|
if [[ "$force_rescan" != "true" ]] && is_cache_valid "$cache_file" 3600; then
|
||||||
log_info "Loading cached results for $target_path..."
|
log_info "Loading cached results for $target_path..."
|
||||||
load_from_cache "$cache_file"
|
load_from_cache "$cache_file"
|
||||||
@@ -328,8 +333,10 @@ generate_bar() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local filled=$((current * width / max))
|
local filled
|
||||||
local empty=$((width - filled))
|
filled=$((current * width / max))
|
||||||
|
local empty
|
||||||
|
empty=$((width - filled))
|
||||||
|
|
||||||
# Ensure non-negative
|
# Ensure non-negative
|
||||||
[[ $filled -lt 0 ]] && filled=0
|
[[ $filled -lt 0 ]] && filled=0
|
||||||
@@ -372,7 +379,8 @@ display_large_files_compact() {
|
|||||||
|
|
||||||
local count=0
|
local count=0
|
||||||
local total_size=0
|
local total_size=0
|
||||||
local total_count=$(wc -l < "$temp_large" | tr -d ' ')
|
local total_count
|
||||||
|
total_count=$(wc -l < "$temp_large" | tr -d ' ')
|
||||||
|
|
||||||
# Calculate total size
|
# Calculate total size
|
||||||
while IFS='|' read -r size path; do
|
while IFS='|' read -r size path; do
|
||||||
@@ -385,11 +393,15 @@ display_large_files_compact() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local filename=$(basename "$path")
|
human_size=$(bytes_to_human "$size")
|
||||||
local dirname=$(basename "$(dirname "$path")")
|
local filename
|
||||||
|
filename=$(basename "$path")
|
||||||
|
local dirname
|
||||||
|
dirname=$(basename "$(dirname "$path")")
|
||||||
|
|
||||||
local info=$(get_file_info "$path")
|
local info
|
||||||
|
info=$(get_file_info "$path")
|
||||||
local badge="${info%|*}"
|
local badge="${info%|*}"
|
||||||
printf " ${GREEN}%-8s${NC} %s %-40s ${GRAY}%s${NC}\n" \
|
printf " ${GREEN}%-8s${NC} %s %-40s ${GRAY}%s${NC}\n" \
|
||||||
"$human_size" "$badge" "${filename:0:40}" "$dirname"
|
"$human_size" "$badge" "${filename:0:40}" "$dirname"
|
||||||
@@ -398,7 +410,8 @@ display_large_files_compact() {
|
|||||||
done < "$temp_large"
|
done < "$temp_large"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
local total_human=$(bytes_to_human "$total_size")
|
local total_human
|
||||||
|
total_human=$(bytes_to_human "$total_size")
|
||||||
echo " ${GRAY}Found $total_count large files (>1GB), totaling $total_human${NC}"
|
echo " ${GRAY}Found $total_count large files (>1GB), totaling $total_human${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
@@ -430,13 +443,19 @@ display_large_files() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local percentage=$(calc_percentage "$size" "$max_size")
|
human_size=$(bytes_to_human "$size")
|
||||||
local bar=$(generate_bar "$size" "$max_size" 20)
|
local percentage
|
||||||
local filename=$(basename "$path")
|
percentage=$(calc_percentage "$size" "$max_size")
|
||||||
local dirname=$(dirname "$path" | sed "s|^$HOME|~|")
|
local bar
|
||||||
|
bar=$(generate_bar "$size" "$max_size" 20)
|
||||||
|
local filename
|
||||||
|
filename=$(basename "$path")
|
||||||
|
local dirname
|
||||||
|
dirname=$(dirname "$path" | sed "s|^$HOME|~|")
|
||||||
|
|
||||||
local info=$(get_file_info "$path")
|
local info
|
||||||
|
info=$(get_file_info "$path")
|
||||||
local badge="${info%|*}"
|
local badge="${info%|*}"
|
||||||
printf " %s [${GREEN}%s${NC}] %7s\n" "$bar" "$human_size" ""
|
printf " %s [${GREEN}%s${NC}] %7s\n" "$bar" "$human_size" ""
|
||||||
printf " %s %s\n" "$badge" "$filename"
|
printf " %s %s\n" "$badge" "$filename"
|
||||||
@@ -446,7 +465,8 @@ display_large_files() {
|
|||||||
done < "$temp_large"
|
done < "$temp_large"
|
||||||
|
|
||||||
# Show total count
|
# Show total count
|
||||||
local total_count=$(wc -l < "$temp_large" | tr -d ' ')
|
local total_count
|
||||||
|
total_count=$(wc -l < "$temp_large" | tr -d ' ')
|
||||||
if [[ $total_count -gt 10 ]]; then
|
if [[ $total_count -gt 10 ]]; then
|
||||||
echo " ${GRAY}... and $((total_count - 10)) more files${NC}"
|
echo " ${GRAY}... and $((total_count - 10)) more files${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -479,17 +499,22 @@ display_directories_compact() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local percentage=$(calc_percentage "$size" "$total_size")
|
human_size=$(bytes_to_human "$size")
|
||||||
local dirname=$(basename "$path")
|
local percentage
|
||||||
|
percentage=$(calc_percentage "$size" "$total_size")
|
||||||
|
local dirname
|
||||||
|
dirname=$(basename "$path")
|
||||||
|
|
||||||
# Simple bar (10 chars)
|
# Simple bar (10 chars)
|
||||||
local bar_width=10
|
local bar_width=10
|
||||||
local percentage_int=${percentage%.*} # Remove decimal part
|
local percentage_int=${percentage%.*} # Remove decimal part
|
||||||
local filled=$((percentage_int * bar_width / 100))
|
local filled
|
||||||
|
filled=$((percentage_int * bar_width / 100))
|
||||||
[[ $filled -gt $bar_width ]] && filled=$bar_width
|
[[ $filled -gt $bar_width ]] && filled=$bar_width
|
||||||
[[ $filled -lt 0 ]] && filled=0
|
[[ $filled -lt 0 ]] && filled=0
|
||||||
local empty=$((bar_width - filled))
|
local empty
|
||||||
|
empty=$((bar_width - filled))
|
||||||
[[ $empty -lt 0 ]] && empty=0
|
[[ $empty -lt 0 ]] && empty=0
|
||||||
local bar=""
|
local bar=""
|
||||||
if [[ $filled -gt 0 ]]; then
|
if [[ $filled -gt 0 ]]; then
|
||||||
@@ -538,11 +563,16 @@ display_directories() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local percentage=$(calc_percentage "$size" "$total_size")
|
human_size=$(bytes_to_human "$size")
|
||||||
local bar=$(generate_bar "$size" "$max_size" 20)
|
local percentage
|
||||||
local display_path=$(echo "$path" | sed "s|^$HOME|~|")
|
percentage=$(calc_percentage "$size" "$total_size")
|
||||||
local dirname=$(basename "$path")
|
local bar
|
||||||
|
bar=$(generate_bar "$size" "$max_size" 20)
|
||||||
|
local display_path
|
||||||
|
display_path=$(echo "$path" | sed "s|^$HOME|~|")
|
||||||
|
local dirname
|
||||||
|
dirname=$(basename "$path")
|
||||||
|
|
||||||
printf " %s [${BLUE}%s${NC}] %5s%%\n" "$bar" "$human_size" "$percentage"
|
printf " %s [${BLUE}%s${NC}] %5s%%\n" "$bar" "$human_size" "$percentage"
|
||||||
printf " %s %s\n\n" "$BADGE_DIR" "$display_path"
|
printf " %s %s\n\n" "$BADGE_DIR" "$display_path"
|
||||||
@@ -568,8 +598,10 @@ display_hotspots() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local display_path=$(echo "$path" | sed "s|^$HOME|~|")
|
human_size=$(bytes_to_human "$size")
|
||||||
|
local display_path
|
||||||
|
display_path=$(echo "$path" | sed "s|^$HOME|~|")
|
||||||
|
|
||||||
printf " %s\n" "$display_path"
|
printf " %s\n" "$display_path"
|
||||||
printf " ${GREEN}%s${NC} in ${YELLOW}%d${NC} large files\n\n" \
|
printf " ${GREEN}%s${NC} in ${YELLOW}%d${NC} large files\n\n" \
|
||||||
@@ -589,9 +621,11 @@ display_cleanup_suggestions_compact() {
|
|||||||
# Check common cache locations (only if analyzing Library/Caches or system paths)
|
# Check common cache locations (only if analyzing Library/Caches or system paths)
|
||||||
if [[ "$CURRENT_PATH" == "$HOME/Library/Caches"* ]] || [[ "$CURRENT_PATH" == "$HOME/Library"* ]]; then
|
if [[ "$CURRENT_PATH" == "$HOME/Library/Caches"* ]] || [[ "$CURRENT_PATH" == "$HOME/Library"* ]]; then
|
||||||
if [[ -d "$HOME/Library/Caches" ]]; then
|
if [[ -d "$HOME/Library/Caches" ]]; then
|
||||||
local cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1)
|
local cache_size
|
||||||
|
cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1)
|
||||||
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
||||||
local human=$(bytes_to_human $((cache_size * 1024)))
|
local human
|
||||||
|
human=$(bytes_to_human $((cache_size * 1024)))
|
||||||
top_suggestion="Clear app caches ($human)"
|
top_suggestion="Clear app caches ($human)"
|
||||||
action_command="mole clean"
|
action_command="mole clean"
|
||||||
((potential_space += cache_size * 1024))
|
((potential_space += cache_size * 1024))
|
||||||
@@ -602,7 +636,8 @@ display_cleanup_suggestions_compact() {
|
|||||||
|
|
||||||
# Check Downloads folder (only if analyzing Downloads)
|
# Check Downloads folder (only if analyzing Downloads)
|
||||||
if [[ "$CURRENT_PATH" == "$HOME/Downloads"* ]]; then
|
if [[ "$CURRENT_PATH" == "$HOME/Downloads"* ]]; then
|
||||||
local old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ')
|
local old_files
|
||||||
|
old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ')
|
||||||
if [[ $old_files -gt 0 ]]; then
|
if [[ $old_files -gt 0 ]]; then
|
||||||
[[ -z "$top_suggestion" ]] && top_suggestion="$old_files files older than 90 days found"
|
[[ -z "$top_suggestion" ]] && top_suggestion="$old_files files older than 90 days found"
|
||||||
[[ -z "$action_command" ]] && action_command="manually review old files"
|
[[ -z "$action_command" ]] && action_command="manually review old files"
|
||||||
@@ -618,7 +653,8 @@ display_cleanup_suggestions_compact() {
|
|||||||
local dmg_size=$(mdfind -onlyin "$CURRENT_PATH" \
|
local dmg_size=$(mdfind -onlyin "$CURRENT_PATH" \
|
||||||
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | \
|
"kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | \
|
||||||
xargs stat -f%z 2>/dev/null | awk '{sum+=$1} END {print sum}')
|
xargs stat -f%z 2>/dev/null | awk '{sum+=$1} END {print sum}')
|
||||||
local dmg_human=$(bytes_to_human "$dmg_size")
|
local dmg_human
|
||||||
|
dmg_human=$(bytes_to_human "$dmg_size")
|
||||||
[[ -z "$top_suggestion" ]] && top_suggestion="$dmg_count DMG files ($dmg_human) can be removed"
|
[[ -z "$top_suggestion" ]] && top_suggestion="$dmg_count DMG files ($dmg_human) can be removed"
|
||||||
[[ -z "$action_command" ]] && action_command="manually delete DMG files"
|
[[ -z "$action_command" ]] && action_command="manually delete DMG files"
|
||||||
((potential_space += dmg_size))
|
((potential_space += dmg_size))
|
||||||
@@ -628,9 +664,11 @@ display_cleanup_suggestions_compact() {
|
|||||||
|
|
||||||
# Check Xcode (only if in developer paths)
|
# Check Xcode (only if in developer paths)
|
||||||
if [[ "$CURRENT_PATH" == "$HOME/Library/Developer"* ]] && [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then
|
if [[ "$CURRENT_PATH" == "$HOME/Library/Developer"* ]] && [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then
|
||||||
local xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1)
|
local xcode_size
|
||||||
|
xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1)
|
||||||
if [[ $xcode_size -gt 10485760 ]]; then
|
if [[ $xcode_size -gt 10485760 ]]; then
|
||||||
local xcode_human=$(bytes_to_human $((xcode_size * 1024)))
|
local xcode_human
|
||||||
|
xcode_human=$(bytes_to_human $((xcode_size * 1024)))
|
||||||
[[ -z "$top_suggestion" ]] && top_suggestion="Xcode cache ($xcode_human) can be cleared"
|
[[ -z "$top_suggestion" ]] && top_suggestion="Xcode cache ($xcode_human) can be cleared"
|
||||||
[[ -z "$action_command" ]] && action_command="mole clean"
|
[[ -z "$action_command" ]] && action_command="mole clean"
|
||||||
((potential_space += xcode_size * 1024))
|
((potential_space += xcode_size * 1024))
|
||||||
@@ -656,7 +694,8 @@ display_cleanup_suggestions_compact() {
|
|||||||
echo " ${GRAY}... and $((suggestions_count - 1)) more insights${NC}"
|
echo " ${GRAY}... and $((suggestions_count - 1)) more insights${NC}"
|
||||||
fi
|
fi
|
||||||
if [[ $potential_space -gt 0 ]]; then
|
if [[ $potential_space -gt 0 ]]; then
|
||||||
local space_human=$(bytes_to_human "$potential_space")
|
local space_human
|
||||||
|
space_human=$(bytes_to_human "$potential_space")
|
||||||
echo " ${GREEN}Potential recovery: ~$space_human${NC}"
|
echo " ${GREEN}Potential recovery: ~$space_human${NC}"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
@@ -680,16 +719,19 @@ display_cleanup_suggestions() {
|
|||||||
|
|
||||||
# Check common cache locations
|
# Check common cache locations
|
||||||
if [[ -d "$HOME/Library/Caches" ]]; then
|
if [[ -d "$HOME/Library/Caches" ]]; then
|
||||||
local cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1)
|
local cache_size
|
||||||
|
cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1)
|
||||||
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
if [[ $cache_size -gt 1048576 ]]; then # > 1GB
|
||||||
local human=$(bytes_to_human $((cache_size * 1024)))
|
local human
|
||||||
|
human=$(bytes_to_human $((cache_size * 1024)))
|
||||||
suggestions+=(" Clear application caches: $human")
|
suggestions+=(" Clear application caches: $human")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check Downloads folder
|
# Check Downloads folder
|
||||||
if [[ -d "$HOME/Downloads" ]]; then
|
if [[ -d "$HOME/Downloads" ]]; then
|
||||||
local old_files=$(find "$HOME/Downloads" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ')
|
local old_files
|
||||||
|
old_files=$(find "$HOME/Downloads" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ')
|
||||||
if [[ $old_files -gt 0 ]]; then
|
if [[ $old_files -gt 0 ]]; then
|
||||||
suggestions+=(" Clean old downloads: $old_files files older than 90 days")
|
suggestions+=(" Clean old downloads: $old_files files older than 90 days")
|
||||||
fi
|
fi
|
||||||
@@ -706,18 +748,22 @@ display_cleanup_suggestions() {
|
|||||||
|
|
||||||
# Check Xcode derived data
|
# Check Xcode derived data
|
||||||
if [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then
|
if [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then
|
||||||
local xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1)
|
local xcode_size
|
||||||
|
xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1)
|
||||||
if [[ $xcode_size -gt 10485760 ]]; then # > 10GB
|
if [[ $xcode_size -gt 10485760 ]]; then # > 10GB
|
||||||
local human=$(bytes_to_human $((xcode_size * 1024)))
|
local human
|
||||||
|
human=$(bytes_to_human $((xcode_size * 1024)))
|
||||||
suggestions+=(" Clear Xcode cache: $human")
|
suggestions+=(" Clear Xcode cache: $human")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check iOS device backups
|
# Check iOS device backups
|
||||||
if [[ -d "$HOME/Library/Application Support/MobileSync/Backup" ]]; then
|
if [[ -d "$HOME/Library/Application Support/MobileSync/Backup" ]]; then
|
||||||
local backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2>/dev/null | cut -f1)
|
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
|
if [[ $backup_size -gt 5242880 ]]; then # > 5GB
|
||||||
local human=$(bytes_to_human $((backup_size * 1024)))
|
local human
|
||||||
|
human=$(bytes_to_human $((backup_size * 1024)))
|
||||||
suggestions+=(" 📱 Review iOS backups: $human")
|
suggestions+=(" 📱 Review iOS backups: $human")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -728,7 +774,8 @@ display_cleanup_suggestions() {
|
|||||||
mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \
|
mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \
|
||||||
xargs -I {} stat -f "%z" {} 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"
|
sort | uniq -d | wc -l | tr -d ' ' > "$temp_dup" 2>/dev/null || echo "0" > "$temp_dup"
|
||||||
local dup_count=$(cat "$temp_dup" 2>/dev/null || echo "0")
|
local dup_count
|
||||||
|
dup_count=$(cat "$temp_dup" 2>/dev/null || echo "0")
|
||||||
if [[ $dup_count -gt 5 ]]; then
|
if [[ $dup_count -gt 5 ]]; then
|
||||||
suggestions+=(" ♻️ Possible duplicates: $dup_count size matches in large files (>10MB)")
|
suggestions+=(" ♻️ Possible duplicates: $dup_count size matches in large files (>10MB)")
|
||||||
fi
|
fi
|
||||||
@@ -772,11 +819,13 @@ display_disk_summary() {
|
|||||||
|
|
||||||
log_header "Disk Situation"
|
log_header "Disk Situation"
|
||||||
|
|
||||||
local target_display=$(echo "$CURRENT_PATH" | sed "s|^$HOME|~|")
|
local target_display
|
||||||
|
target_display=$(echo "$CURRENT_PATH" | sed "s|^$HOME|~|")
|
||||||
echo " ${BLUE}Scanning:${NC} $target_display | ${BLUE}Free:${NC} $(get_free_space)"
|
echo " ${BLUE}Scanning:${NC} $target_display | ${BLUE}Free:${NC} $(get_free_space)"
|
||||||
|
|
||||||
if [[ $total_large_count -gt 0 ]]; then
|
if [[ $total_large_count -gt 0 ]]; then
|
||||||
local large_human=$(bytes_to_human "$total_large_size")
|
local large_human
|
||||||
|
large_human=$(bytes_to_human "$total_large_size")
|
||||||
echo " ${BLUE}Large Files:${NC} $total_large_count files ($large_human) | ${BLUE}Total:${NC} $(bytes_to_human "$total_dirs_size") in $total_dirs_count dirs"
|
echo " ${BLUE}Large Files:${NC} $total_large_count files ($large_human) | ${BLUE}Total:${NC} $(bytes_to_human "$total_dirs_size") in $total_dirs_count dirs"
|
||||||
elif [[ $total_dirs_size -gt 0 ]]; then
|
elif [[ $total_dirs_size -gt 0 ]]; then
|
||||||
echo " ${BLUE}Total Scanned:${NC} $(bytes_to_human "$total_dirs_size") across $total_dirs_count directories"
|
echo " ${BLUE}Total Scanned:${NC} $(bytes_to_human "$total_dirs_size") across $total_dirs_count directories"
|
||||||
@@ -820,10 +869,14 @@ get_file_age() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local mtime=$(stat -f%m "$path" 2>/dev/null || echo "0")
|
local mtime
|
||||||
local now=$(date +%s)
|
mtime=$(stat -f%m "$path" 2>/dev/null || echo "0")
|
||||||
local diff=$((now - mtime))
|
local now
|
||||||
local days=$((diff / 86400))
|
now=$(date +%s)
|
||||||
|
local diff
|
||||||
|
diff=$((now - mtime))
|
||||||
|
local days
|
||||||
|
days=$((diff / 86400))
|
||||||
|
|
||||||
if [[ $days -lt 1 ]]; then
|
if [[ $days -lt 1 ]]; then
|
||||||
echo "Today"
|
echo "Today"
|
||||||
@@ -832,10 +885,12 @@ get_file_age() {
|
|||||||
elif [[ $days -lt 30 ]]; then
|
elif [[ $days -lt 30 ]]; then
|
||||||
echo "${days}d"
|
echo "${days}d"
|
||||||
elif [[ $days -lt 365 ]]; then
|
elif [[ $days -lt 365 ]]; then
|
||||||
local months=$((days / 30))
|
local months
|
||||||
|
months=$((days / 30))
|
||||||
echo "${months}mo"
|
echo "${months}mo"
|
||||||
else
|
else
|
||||||
local years=$((days / 365))
|
local years
|
||||||
|
years=$((days / 365))
|
||||||
echo "${years}yr"
|
echo "${years}yr"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -860,13 +915,17 @@ display_large_files_table() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local filename=$(basename "$path")
|
human_size=$(bytes_to_human "$size")
|
||||||
|
local filename
|
||||||
|
filename=$(basename "$path")
|
||||||
local ext="${filename##*.}"
|
local ext="${filename##*.}"
|
||||||
local age=$(get_file_age "$path")
|
local age
|
||||||
|
age=$(get_file_age "$path")
|
||||||
|
|
||||||
# Get file info and badge
|
# Get file info and badge
|
||||||
local info=$(get_file_info "$path")
|
local info
|
||||||
|
info=$(get_file_info "$path")
|
||||||
local badge="${info%|*}"
|
local badge="${info%|*}"
|
||||||
|
|
||||||
# Truncate filename if too long
|
# Truncate filename if too long
|
||||||
@@ -889,7 +948,8 @@ display_large_files_table() {
|
|||||||
((count++))
|
((count++))
|
||||||
done < "$temp_large"
|
done < "$temp_large"
|
||||||
|
|
||||||
local total=$(wc -l < "$temp_large" | tr -d ' ')
|
local total
|
||||||
|
total=$(wc -l < "$temp_large" | tr -d ' ')
|
||||||
if [[ $total -gt 20 ]]; then
|
if [[ $total -gt 20 ]]; then
|
||||||
echo " ${GRAY}... $((total - 20)) more files${NC}"
|
echo " ${GRAY}... $((total - 20)) more files${NC}"
|
||||||
fi
|
fi
|
||||||
@@ -925,19 +985,24 @@ display_unified_directories() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local percentage=$((size * 100 / total_size))
|
local percentage
|
||||||
local bar_width=$((percentage * chart_width / 100))
|
percentage=$((size * 100 / total_size))
|
||||||
|
local bar_width
|
||||||
|
bar_width=$((percentage * chart_width / 100))
|
||||||
[[ $bar_width -lt 1 ]] && bar_width=1
|
[[ $bar_width -lt 1 ]] && bar_width=1
|
||||||
|
|
||||||
local dirname=$(basename "$path")
|
local dirname
|
||||||
local human_size=$(bytes_to_human "$size")
|
dirname=$(basename "$path")
|
||||||
|
local human_size
|
||||||
|
human_size=$(bytes_to_human "$size")
|
||||||
|
|
||||||
# Build compact bar
|
# Build compact bar
|
||||||
local bar=""
|
local bar=""
|
||||||
if [[ $bar_width -gt 0 ]]; then
|
if [[ $bar_width -gt 0 ]]; then
|
||||||
bar=$(printf "%${bar_width}s" "" | tr ' ' '▓')
|
bar=$(printf "%${bar_width}s" "" | tr ' ' '▓')
|
||||||
fi
|
fi
|
||||||
local empty=$((chart_width - bar_width))
|
local empty
|
||||||
|
empty=$((chart_width - bar_width))
|
||||||
if [[ $empty -gt 0 ]]; then
|
if [[ $empty -gt 0 ]]; then
|
||||||
bar="${bar}$(printf "%${empty}s" "" | tr ' ' '░')"
|
bar="${bar}$(printf "%${empty}s" "" | tr ' ' '░')"
|
||||||
fi
|
fi
|
||||||
@@ -1009,12 +1074,16 @@ display_space_chart() {
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local percentage=$((size * 100 / total_size))
|
local percentage
|
||||||
local bar_width=$((percentage * chart_width / 100))
|
percentage=$((size * 100 / total_size))
|
||||||
|
local bar_width
|
||||||
|
bar_width=$((percentage * chart_width / 100))
|
||||||
[[ $bar_width -lt 1 ]] && bar_width=1
|
[[ $bar_width -lt 1 ]] && bar_width=1
|
||||||
|
|
||||||
local dirname=$(basename "$path")
|
local dirname
|
||||||
local human_size=$(bytes_to_human "$size")
|
dirname=$(basename "$path")
|
||||||
|
local human_size
|
||||||
|
human_size=$(bytes_to_human "$size")
|
||||||
|
|
||||||
# Build visual bar
|
# Build visual bar
|
||||||
local bar=""
|
local bar=""
|
||||||
@@ -1048,8 +1117,10 @@ display_recent_large_files() {
|
|||||||
"kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2>/dev/null | \
|
"kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2>/dev/null | \
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
if [[ -f "$file" ]]; then
|
if [[ -f "$file" ]]; then
|
||||||
local size=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
local size
|
||||||
local mtime=$(stat -f%m "$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")
|
||||||
echo "$size|$mtime|$file"
|
echo "$size|$mtime|$file"
|
||||||
fi
|
fi
|
||||||
done | sort -t'|' -k1 -rn | head -10 > "$temp_recent"
|
done | sort -t'|' -k1 -rn | head -10 > "$temp_recent"
|
||||||
@@ -1062,12 +1133,17 @@ display_recent_large_files() {
|
|||||||
|
|
||||||
local count=0
|
local count=0
|
||||||
while IFS='|' read -r size mtime path; do
|
while IFS='|' read -r size mtime path; do
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local filename=$(basename "$path")
|
human_size=$(bytes_to_human "$size")
|
||||||
local dirname=$(dirname "$path" | sed "s|^$HOME|~|")
|
local filename
|
||||||
local days_ago=$(( ($(date +%s) - mtime) / 86400 ))
|
filename=$(basename "$path")
|
||||||
|
local dirname
|
||||||
|
dirname=$(dirname "$path" | sed "s|^$HOME|~|")
|
||||||
|
local days_ago
|
||||||
|
days_ago=$(( ($(date +%s) - mtime) / 86400 ))
|
||||||
|
|
||||||
local info=$(get_file_info "$path")
|
local info
|
||||||
|
info=$(get_file_info "$path")
|
||||||
local badge="${info%|*}"
|
local badge="${info%|*}"
|
||||||
|
|
||||||
printf " %s %s ${GRAY}(%s)${NC}\n" "$badge" "$filename" "$human_size"
|
printf " %s %s ${GRAY}(%s)${NC}\n" "$badge" "$filename" "$human_size"
|
||||||
@@ -1088,7 +1164,8 @@ get_subdirectories() {
|
|||||||
|
|
||||||
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
|
while IFS= read -r dir; do
|
||||||
local size=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
local size
|
||||||
|
size=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
||||||
echo "$((size * 1024))|$dir"
|
echo "$((size * 1024))|$dir"
|
||||||
done | sort -t'|' -k1 -rn > "$temp_file"
|
done | sort -t'|' -k1 -rn > "$temp_file"
|
||||||
}
|
}
|
||||||
@@ -1116,11 +1193,16 @@ display_directory_list() {
|
|||||||
|
|
||||||
# Display with cursor
|
# Display with cursor
|
||||||
while IFS='|' read -r size path; do
|
while IFS='|' read -r size path; do
|
||||||
local human_size=$(bytes_to_human "$size")
|
local human_size
|
||||||
local percentage=$(calc_percentage "$size" "$total_size")
|
human_size=$(bytes_to_human "$size")
|
||||||
local bar=$(generate_bar "$size" "$max_size" 20)
|
local percentage
|
||||||
local display_path=$(echo "$path" | sed "s|^$HOME|~|")
|
percentage=$(calc_percentage "$size" "$total_size")
|
||||||
local dirname=$(basename "$path")
|
local bar
|
||||||
|
bar=$(generate_bar "$size" "$max_size" 20)
|
||||||
|
local display_path
|
||||||
|
display_path=$(echo "$path" | sed "s|^$HOME|~|")
|
||||||
|
local dirname
|
||||||
|
dirname=$(basename "$path")
|
||||||
|
|
||||||
# Highlight selected line
|
# Highlight selected line
|
||||||
if [[ $idx -eq $cursor_pos ]]; then
|
if [[ $idx -eq $cursor_pos ]]; then
|
||||||
@@ -1168,7 +1250,8 @@ count_directories() {
|
|||||||
echo "0"
|
echo "0"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local count=$(wc -l < "$temp_dirs" | tr -d ' ')
|
local count
|
||||||
|
count=$(wc -l < "$temp_dirs" | tr -d ' ')
|
||||||
[[ $count -gt 15 ]] && count=15
|
[[ $count -gt 15 ]] && count=15
|
||||||
echo "$count"
|
echo "$count"
|
||||||
}
|
}
|
||||||
@@ -1252,20 +1335,24 @@ display_file_types() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
local files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null)
|
local files
|
||||||
local count=$(echo "$files" | grep -c . || echo "0")
|
files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null)
|
||||||
|
local count
|
||||||
|
count=$(echo "$files" | grep -c . || echo "0")
|
||||||
local total_size=0
|
local total_size=0
|
||||||
|
|
||||||
if [[ $count -gt 0 ]]; then
|
if [[ $count -gt 0 ]]; then
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
if [[ -f "$file" ]]; then
|
if [[ -f "$file" ]]; then
|
||||||
local fsize=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
local fsize
|
||||||
|
fsize=$(stat -f%z "$file" 2>/dev/null || echo "0")
|
||||||
((total_size += fsize))
|
((total_size += fsize))
|
||||||
fi
|
fi
|
||||||
done <<< "$files"
|
done <<< "$files"
|
||||||
|
|
||||||
if [[ $total_size -gt 0 ]]; then
|
if [[ $total_size -gt 0 ]]; then
|
||||||
local human_size=$(bytes_to_human "$total_size")
|
local human_size
|
||||||
|
human_size=$(bytes_to_human "$total_size")
|
||||||
printf " %s %-12s %8s (%d files)\n" "$badge" "$type_name:" "$human_size" "$count"
|
printf " %s %-12s %8s (%d files)\n" "$badge" "$type_name:" "$human_size" "$count"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1292,7 +1379,8 @@ scan_directory_contents_fast() {
|
|||||||
local show_progress="${4:-true}"
|
local show_progress="${4:-true}"
|
||||||
|
|
||||||
# Auto-detect optimal parallel jobs using common function
|
# Auto-detect optimal parallel jobs using common function
|
||||||
local num_jobs=$(get_optimal_parallel_jobs "io")
|
local num_jobs
|
||||||
|
num_jobs=$(get_optimal_parallel_jobs "io")
|
||||||
# Cap at reasonable limits for I/O operations
|
# Cap at reasonable limits for I/O operations
|
||||||
[[ $num_jobs -gt 24 ]] && num_jobs=24
|
[[ $num_jobs -gt 24 ]] && num_jobs=24
|
||||||
[[ $num_jobs -lt 12 ]] && num_jobs=12
|
[[ $num_jobs -lt 12 ]] && num_jobs=12
|
||||||
@@ -1456,7 +1544,8 @@ combine_initial_scan_results() {
|
|||||||
if [[ -f "$temp_large" ]]; then
|
if [[ -f "$temp_large" ]]; then
|
||||||
while IFS='|' read -r size path; do
|
while IFS='|' read -r size path; do
|
||||||
# Only include if parent directory is the current scan path
|
# Only include if parent directory is the current scan path
|
||||||
local parent=$(dirname "$path")
|
local parent
|
||||||
|
parent=$(dirname "$path")
|
||||||
if [[ "$parent" == "$CURRENT_PATH" ]]; then
|
if [[ "$parent" == "$CURRENT_PATH" ]]; then
|
||||||
echo "$size|file|$path"
|
echo "$size|file|$path"
|
||||||
fi
|
fi
|
||||||
@@ -1484,7 +1573,8 @@ show_volumes_overview() {
|
|||||||
if [[ -d "/Volumes" ]]; then
|
if [[ -d "/Volumes" ]]; then
|
||||||
local vol_priority=500
|
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=$(basename "$vol")
|
local vol_name
|
||||||
|
vol_name=$(basename "$vol")
|
||||||
echo "$((vol_priority))|$vol|Volume: $vol_name"
|
echo "$((vol_priority))|$vol|Volume: $vol_name"
|
||||||
((vol_priority--))
|
((vol_priority--))
|
||||||
done
|
done
|
||||||
@@ -1505,7 +1595,8 @@ show_volumes_overview() {
|
|||||||
stty -echo 2>/dev/null || true
|
stty -echo 2>/dev/null || true
|
||||||
|
|
||||||
local cursor=0
|
local cursor=0
|
||||||
local total_items=$(wc -l < "$temp_volumes" | tr -d ' ')
|
local total_items
|
||||||
|
total_items=$(wc -l < "$temp_volumes" | tr -d ' ')
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# Ensure cursor is always hidden
|
# Ensure cursor is always hidden
|
||||||
@@ -1655,7 +1746,8 @@ interactive_drill_down() {
|
|||||||
# Only scan if needed (directory changed or refresh requested)
|
# Only scan if needed (directory changed or refresh requested)
|
||||||
if [[ "$need_scan" == "true" ]]; then
|
if [[ "$need_scan" == "true" ]]; then
|
||||||
# Generate cache key (use md5 hash of path)
|
# Generate cache key (use md5 hash of path)
|
||||||
local cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
local cache_key
|
||||||
|
cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
||||||
local cache_file="$cache_dir/$cache_key"
|
local cache_file="$cache_dir/$cache_key"
|
||||||
|
|
||||||
# Check if we have cached results for this directory
|
# Check if we have cached results for this directory
|
||||||
@@ -1732,7 +1824,8 @@ interactive_drill_down() {
|
|||||||
if [[ ${#path_stack[@]} -gt 0 ]]; then
|
if [[ ${#path_stack[@]} -gt 0 ]]; then
|
||||||
# Use bash 3.2 compatible way to get last element
|
# Use bash 3.2 compatible way to get last element
|
||||||
local stack_size=${#path_stack[@]}
|
local stack_size=${#path_stack[@]}
|
||||||
local last_index=$((stack_size - 1))
|
local last_index
|
||||||
|
last_index=$((stack_size - 1))
|
||||||
current_path="${path_stack[$last_index]}"
|
current_path="${path_stack[$last_index]}"
|
||||||
unset "path_stack[$last_index]"
|
unset "path_stack[$last_index]"
|
||||||
cursor=0
|
cursor=0
|
||||||
@@ -1757,7 +1850,8 @@ interactive_drill_down() {
|
|||||||
|
|
||||||
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_start=$scroll_offset
|
||||||
local page_end=$((scroll_offset + max_show))
|
local page_end
|
||||||
|
page_end=$((scroll_offset + max_show))
|
||||||
[[ $page_end -gt $total_items ]] && page_end=$total_items
|
[[ $page_end -gt $total_items ]] && page_end=$total_items
|
||||||
|
|
||||||
local display_idx=0
|
local display_idx=0
|
||||||
@@ -1778,7 +1872,8 @@ interactive_drill_down() {
|
|||||||
local rest="${item_info#*|}"
|
local rest="${item_info#*|}"
|
||||||
local type="${rest%%|*}"
|
local type="${rest%%|*}"
|
||||||
local path="${rest#*|}"
|
local path="${rest#*|}"
|
||||||
local name=$(basename "$path")
|
local name
|
||||||
|
name=$(basename "$path")
|
||||||
|
|
||||||
local human_size
|
local human_size
|
||||||
if [[ "$size" -eq 0 ]]; then
|
if [[ "$size" -eq 0 ]]; then
|
||||||
@@ -1796,7 +1891,8 @@ interactive_drill_down() {
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
local ext="${name##*.}"
|
local ext="${name##*.}"
|
||||||
local info=$(get_file_info "$path")
|
local info
|
||||||
|
info=$(get_file_info "$path")
|
||||||
badge="${info%|*}"
|
badge="${info%|*}"
|
||||||
case "$ext" in
|
case "$ext" in
|
||||||
dmg|iso|pkg|zip|tar|gz|rar|7z)
|
dmg|iso|pkg|zip|tar|gz|rar|7z)
|
||||||
@@ -1871,7 +1967,8 @@ interactive_drill_down() {
|
|||||||
if [[ $cursor -lt $((total_items - 1)) ]]; then
|
if [[ $cursor -lt $((total_items - 1)) ]]; then
|
||||||
((cursor++))
|
((cursor++))
|
||||||
# Scroll down if cursor goes below visible area
|
# Scroll down if cursor goes below visible area
|
||||||
local page_end=$((scroll_offset + max_show))
|
local page_end
|
||||||
|
page_end=$((scroll_offset + max_show))
|
||||||
if [[ $cursor -ge $page_end ]]; then
|
if [[ $cursor -ge $page_end ]]; then
|
||||||
scroll_offset=$((cursor - max_show + 1))
|
scroll_offset=$((cursor - max_show + 1))
|
||||||
fi
|
fi
|
||||||
@@ -1895,7 +1992,8 @@ interactive_drill_down() {
|
|||||||
else
|
else
|
||||||
# It's a file - open it for viewing
|
# It's a file - open it for viewing
|
||||||
local file_ext="${selected_path##*.}"
|
local file_ext="${selected_path##*.}"
|
||||||
local filename=$(basename "$selected_path")
|
local filename
|
||||||
|
filename=$(basename "$selected_path")
|
||||||
local open_success=false
|
local open_success=false
|
||||||
|
|
||||||
# For text-like files, use less or fallback to open
|
# For text-like files, use less or fallback to open
|
||||||
@@ -1972,7 +2070,8 @@ interactive_drill_down() {
|
|||||||
# Pop from stack and go back
|
# Pop from stack and go back
|
||||||
# Use bash 3.2 compatible way to get last element
|
# Use bash 3.2 compatible way to get last element
|
||||||
local stack_size=${#path_stack[@]}
|
local stack_size=${#path_stack[@]}
|
||||||
local last_index=$((stack_size - 1))
|
local last_index
|
||||||
|
last_index=$((stack_size - 1))
|
||||||
current_path="${path_stack[$last_index]}"
|
current_path="${path_stack[$last_index]}"
|
||||||
unset "path_stack[$last_index]"
|
unset "path_stack[$last_index]"
|
||||||
cursor=0
|
cursor=0
|
||||||
@@ -2008,8 +2107,10 @@ interactive_drill_down() {
|
|||||||
local rest="${selected#*|}"
|
local rest="${selected#*|}"
|
||||||
local type="${rest%%|*}"
|
local type="${rest%%|*}"
|
||||||
local selected_path="${rest#*|}"
|
local selected_path="${rest#*|}"
|
||||||
local selected_name=$(basename "$selected_path")
|
local selected_name
|
||||||
local human_size=$(bytes_to_human "$size")
|
selected_name=$(basename "$selected_path")
|
||||||
|
local human_size
|
||||||
|
human_size=$(bytes_to_human "$size")
|
||||||
|
|
||||||
# Check if sudo is needed
|
# Check if sudo is needed
|
||||||
local needs_sudo=false
|
local needs_sudo=false
|
||||||
@@ -2034,7 +2135,8 @@ interactive_drill_down() {
|
|||||||
if [[ "$type" == "dir" ]]; then
|
if [[ "$type" == "dir" ]]; then
|
||||||
echo " ${BADGE_DIR} ${YELLOW}$selected_name${NC}"
|
echo " ${BADGE_DIR} ${YELLOW}$selected_name${NC}"
|
||||||
else
|
else
|
||||||
local info=$(get_file_info "$selected_path")
|
local info
|
||||||
|
info=$(get_file_info "$selected_path")
|
||||||
local badge="${info%|*}"
|
local badge="${info%|*}"
|
||||||
echo " $badge ${YELLOW}$selected_name${NC}"
|
echo " $badge ${YELLOW}$selected_name${NC}"
|
||||||
fi
|
fi
|
||||||
@@ -2092,7 +2194,8 @@ interactive_drill_down() {
|
|||||||
sleep 0.8
|
sleep 0.8
|
||||||
|
|
||||||
# Clear cache to force rescan
|
# Clear cache to force rescan
|
||||||
local cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
local cache_key
|
||||||
|
cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1)
|
||||||
local cache_file="$cache_dir/$cache_key"
|
local cache_file="$cache_dir/$cache_key"
|
||||||
rm -f "$cache_file" 2>/dev/null || true
|
rm -f "$cache_file" 2>/dev/null || true
|
||||||
|
|
||||||
@@ -2142,7 +2245,8 @@ interactive_mode() {
|
|||||||
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
|
display_interactive_menu
|
||||||
|
|
||||||
local key=$(read_key)
|
local key
|
||||||
|
key=$(read_key)
|
||||||
case "$key" in
|
case "$key" in
|
||||||
"QUIT")
|
"QUIT")
|
||||||
break
|
break
|
||||||
@@ -2154,14 +2258,16 @@ interactive_mode() {
|
|||||||
;;
|
;;
|
||||||
"DOWN")
|
"DOWN")
|
||||||
if [[ "$VIEW_MODE" == "navigate" ]]; then
|
if [[ "$VIEW_MODE" == "navigate" ]]; then
|
||||||
local max_count=$(count_directories)
|
local max_count
|
||||||
|
max_count=$(count_directories)
|
||||||
((CURSOR_POS < max_count - 1)) && ((CURSOR_POS++))
|
((CURSOR_POS < max_count - 1)) && ((CURSOR_POS++))
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
"RIGHT")
|
"RIGHT")
|
||||||
if [[ "$VIEW_MODE" == "navigate" ]]; then
|
if [[ "$VIEW_MODE" == "navigate" ]]; then
|
||||||
# Enter selected directory
|
# Enter selected directory
|
||||||
local selected_path=$(get_path_at_cursor "$CURSOR_POS")
|
local selected_path
|
||||||
|
selected_path=$(get_path_at_cursor "$CURSOR_POS")
|
||||||
if [[ -n "$selected_path" ]] && [[ -d "$selected_path" ]]; then
|
if [[ -n "$selected_path" ]] && [[ -d "$selected_path" ]]; then
|
||||||
CURRENT_PATH="$selected_path"
|
CURRENT_PATH="$selected_path"
|
||||||
CURSOR_POS=0
|
CURSOR_POS=0
|
||||||
@@ -2194,7 +2300,8 @@ interactive_mode() {
|
|||||||
"ENTER")
|
"ENTER")
|
||||||
if [[ "$VIEW_MODE" == "navigate" ]]; then
|
if [[ "$VIEW_MODE" == "navigate" ]]; then
|
||||||
# Same as RIGHT
|
# Same as RIGHT
|
||||||
local selected_path=$(get_path_at_cursor "$CURSOR_POS")
|
local selected_path
|
||||||
|
selected_path=$(get_path_at_cursor "$CURSOR_POS")
|
||||||
if [[ -n "$selected_path" ]] && [[ -d "$selected_path" ]]; then
|
if [[ -n "$selected_path" ]] && [[ -d "$selected_path" ]]; then
|
||||||
CURRENT_PATH="$selected_path"
|
CURRENT_PATH="$selected_path"
|
||||||
CURSOR_POS=0
|
CURSOR_POS=0
|
||||||
@@ -2231,7 +2338,8 @@ export_to_csv() {
|
|||||||
{
|
{
|
||||||
echo "Size (Bytes),Size (Human),Path"
|
echo "Size (Bytes),Size (Human),Path"
|
||||||
while IFS='|' read -r size path; do
|
while IFS='|' read -r size path; do
|
||||||
local human=$(bytes_to_human "$size")
|
local human
|
||||||
|
human=$(bytes_to_human "$size")
|
||||||
echo "$size,\"$human\",\"$path\""
|
echo "$size,\"$human\",\"$path\""
|
||||||
done < "$temp_dirs"
|
done < "$temp_dirs"
|
||||||
} > "$output_file"
|
} > "$output_file"
|
||||||
@@ -2260,7 +2368,8 @@ export_to_json() {
|
|||||||
while IFS='|' read -r size path; do
|
while IFS='|' read -r size path; do
|
||||||
[[ "$first" == "false" ]] && echo ","
|
[[ "$first" == "false" ]] && echo ","
|
||||||
first=false
|
first=false
|
||||||
local human=$(bytes_to_human "$size")
|
local human
|
||||||
|
human=$(bytes_to_human "$size")
|
||||||
printf ' {"size": %d, "size_human": "%s", "path": "%s"}' "$size" "$human" "$path"
|
printf ' {"size": %d, "size_human": "%s", "path": "%s"}' "$size" "$human" "$path"
|
||||||
done < "$temp_dirs"
|
done < "$temp_dirs"
|
||||||
|
|
||||||
@@ -2273,7 +2382,8 @@ export_to_json() {
|
|||||||
while IFS='|' read -r size path; do
|
while IFS='|' read -r size path; do
|
||||||
[[ "$first" == "false" ]] && echo ","
|
[[ "$first" == "false" ]] && echo ","
|
||||||
first=false
|
first=false
|
||||||
local human=$(bytes_to_human "$size")
|
local human
|
||||||
|
human=$(bytes_to_human "$size")
|
||||||
printf ' {"size": %d, "size_human": "%s", "path": "%s"}' "$size" "$human" "$path"
|
printf ' {"size": %d, "size_human": "%s", "path": "%s"}' "$size" "$human" "$path"
|
||||||
done < "$temp_large"
|
done < "$temp_large"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -211,7 +211,9 @@ safe_clean() {
|
|||||||
description="$1"
|
description="$1"
|
||||||
targets=("$1")
|
targets=("$1")
|
||||||
else
|
else
|
||||||
description="${@: -1}"
|
# Get last argument as description
|
||||||
|
description="${*: -1}"
|
||||||
|
# Get all arguments except last as targets array
|
||||||
targets=("${@:1:$#-1}")
|
targets=("${@:1:$#-1}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,16 @@ total_size_cleaned=0
|
|||||||
# Get app last used date in human readable format
|
# Get app last used date in human readable format
|
||||||
get_app_last_used() {
|
get_app_last_used() {
|
||||||
local app_path="$1"
|
local app_path="$1"
|
||||||
local last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
local last_used
|
||||||
|
last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||||
|
|
||||||
if [[ "$last_used" == "(null)" || -z "$last_used" ]]; then
|
if [[ "$last_used" == "(null)" || -z "$last_used" ]]; then
|
||||||
echo "Never"
|
echo "Never"
|
||||||
else
|
else
|
||||||
local last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2>/dev/null)
|
local last_used_epoch
|
||||||
local current_epoch=$(date "+%s")
|
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
|
if [[ $days_ago -eq 0 ]]; then
|
||||||
@@ -103,7 +106,8 @@ scan_applications() {
|
|||||||
mkdir -p "$cache_dir" 2>/dev/null
|
mkdir -p "$cache_dir" 2>/dev/null
|
||||||
|
|
||||||
# Quick count of current apps (system + user directories)
|
# Quick count of current apps (system + user directories)
|
||||||
local current_app_count=$(
|
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;
|
||||||
find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' '
|
find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' '
|
||||||
)
|
)
|
||||||
@@ -111,7 +115,8 @@ scan_applications() {
|
|||||||
# Check if cache is valid unless explicitly disabled
|
# Check if cache is valid unless explicitly disabled
|
||||||
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
|
if [[ -f "$cache_file" && -f "$cache_meta" ]]; then
|
||||||
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
||||||
local cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
|
local cached_app_count
|
||||||
|
cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
# Cache is valid if: age < TTL AND app count matches
|
# Cache is valid if: age < TTL AND app count matches
|
||||||
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
|
if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then
|
||||||
@@ -121,10 +126,12 @@ scan_applications() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local temp_file=$(create_temp_file)
|
local temp_file
|
||||||
|
temp_file=$(create_temp_file)
|
||||||
|
|
||||||
# Pre-cache current epoch to avoid repeated calls
|
# Pre-cache current epoch to avoid repeated calls
|
||||||
local current_epoch=$(date "+%s")
|
local current_epoch
|
||||||
|
current_epoch=$(date "+%s")
|
||||||
|
|
||||||
# Spinner for scanning feedback (simple ASCII for compatibility)
|
# Spinner for scanning feedback (simple ASCII for compatibility)
|
||||||
local spinner_chars="|/-\\"
|
local spinner_chars="|/-\\"
|
||||||
@@ -135,7 +142,8 @@ scan_applications() {
|
|||||||
while IFS= read -r -d '' app_path; do
|
while IFS= read -r -d '' app_path; do
|
||||||
if [[ ! -e "$app_path" ]]; then continue; fi
|
if [[ ! -e "$app_path" ]]; then continue; fi
|
||||||
|
|
||||||
local app_name=$(basename "$app_path" .app)
|
local app_name
|
||||||
|
app_name=$(basename "$app_path" .app)
|
||||||
|
|
||||||
# Try to get English name from bundle info, fallback to folder name
|
# Try to get English name from bundle info, fallback to folder name
|
||||||
local bundle_id="unknown"
|
local bundle_id="unknown"
|
||||||
@@ -144,14 +152,17 @@ scan_applications() {
|
|||||||
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
|
# Try to get English name from bundle info
|
||||||
local bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null)
|
local bundle_executable
|
||||||
|
bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null)
|
||||||
|
|
||||||
# Smart display name selection - prefer descriptive names over generic ones
|
# Smart display name selection - prefer descriptive names over generic ones
|
||||||
local candidates=()
|
local candidates=()
|
||||||
|
|
||||||
# Get all potential names
|
# Get all potential names
|
||||||
local bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null)
|
local bundle_display_name
|
||||||
local bundle_name=$(plutil -extract CFBundleName 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)
|
||||||
|
|
||||||
# Check if executable name is generic/technical (should be avoided)
|
# Check if executable name is generic/technical (should be avoided)
|
||||||
local is_generic_executable=false
|
local is_generic_executable=false
|
||||||
@@ -242,7 +253,8 @@ scan_applications() {
|
|||||||
local last_used_epoch=0
|
local last_used_epoch=0
|
||||||
|
|
||||||
if [[ -d "$app_path" ]]; then
|
if [[ -d "$app_path" ]]; then
|
||||||
local metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
local metadata_date
|
||||||
|
metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null)
|
||||||
|
|
||||||
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then
|
||||||
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")
|
||||||
@@ -417,15 +429,20 @@ uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Find related files (user-level)
|
# Find related files (user-level)
|
||||||
local related_files=$(find_app_files "$bundle_id" "$app_name")
|
local related_files
|
||||||
|
related_files=$(find_app_files "$bundle_id" "$app_name")
|
||||||
|
|
||||||
# Find system-level files (requires sudo)
|
# Find system-level files (requires sudo)
|
||||||
local system_files=$(find_app_system_files "$bundle_id" "$app_name")
|
local system_files
|
||||||
|
system_files=$(find_app_system_files "$bundle_id" "$app_name")
|
||||||
|
|
||||||
# Calculate total size
|
# Calculate total size
|
||||||
local app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0")
|
local app_size_kb
|
||||||
local related_size_kb=$(calculate_total_size "$related_files")
|
app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
local system_size_kb=$(calculate_total_size "$system_files")
|
local related_size_kb
|
||||||
|
related_size_kb=$(calculate_total_size "$related_files")
|
||||||
|
local system_size_kb
|
||||||
|
system_size_kb=$(calculate_total_size "$system_files")
|
||||||
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
|
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
|
||||||
|
|
||||||
# Show what will be removed
|
# Show what will be removed
|
||||||
@@ -619,20 +636,27 @@ main() {
|
|||||||
if [[ $selection_count -eq 0 ]]; then
|
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
|
fi
|
||||||
# Compact one-line summary (list up to 3 names, aggregate rest)
|
# Show selected apps, max 3 per line
|
||||||
local names=()
|
echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):"
|
||||||
local idx=0
|
local idx=0
|
||||||
|
local line=""
|
||||||
for selected_app in "${selected_apps[@]}"; do
|
for selected_app in "${selected_apps[@]}"; do
|
||||||
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
|
IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app"
|
||||||
if (( idx < 3 )); then
|
local display_item="${app_name}(${size})"
|
||||||
names+=("${app_name}(${size})")
|
|
||||||
|
if (( idx % 3 == 0 )); then
|
||||||
|
# Start new line
|
||||||
|
[[ -n "$line" ]] && echo " $line"
|
||||||
|
line="$display_item"
|
||||||
|
else
|
||||||
|
# Add to current line
|
||||||
|
line="$line, $display_item"
|
||||||
fi
|
fi
|
||||||
((idx++))
|
((idx++))
|
||||||
done
|
done
|
||||||
local extra=$((selection_count-3))
|
# Print the last line
|
||||||
local list="${names[*]}"
|
[[ -n "$line" ]] && echo " $line"
|
||||||
[[ $extra -gt 0 ]] && list+=" +${extra}"
|
echo ""
|
||||||
echo -e "${BLUE}${ICON_CONFIRM}${NC} ${selection_count} apps: ${list}"
|
|
||||||
|
|
||||||
# Execute batch uninstallation (handles confirmation)
|
# Execute batch uninstallation (handles confirmation)
|
||||||
batch_uninstall_applications
|
batch_uninstall_applications
|
||||||
|
|||||||
@@ -192,17 +192,42 @@ batch_uninstall_applications() {
|
|||||||
if [[ -n "$freed_display" ]]; then
|
if [[ -n "$freed_display" ]]; then
|
||||||
success_line+=", freed ${GREEN}${freed_display}${NC}"
|
success_line+=", freed ${GREEN}${freed_display}${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Format app list with max 3 per line
|
||||||
if [[ -n "$success_list" ]]; then
|
if [[ -n "$success_list" ]]; then
|
||||||
local -a formatted_apps=()
|
local idx=0
|
||||||
|
local is_first_line=true
|
||||||
|
local current_line=""
|
||||||
|
|
||||||
for app_name in "${success_items[@]}"; do
|
for app_name in "${success_items[@]}"; do
|
||||||
formatted_apps+=("${GREEN}${app_name}${NC}")
|
local display_item="${GREEN}${app_name}${NC}"
|
||||||
|
|
||||||
|
if (( idx % 3 == 0 )); then
|
||||||
|
# Start new line
|
||||||
|
if [[ -n "$current_line" ]]; then
|
||||||
|
summary_details+=("$current_line")
|
||||||
|
fi
|
||||||
|
if [[ "$is_first_line" == true ]]; then
|
||||||
|
# First line: append to success_line
|
||||||
|
current_line="${success_line}: $display_item"
|
||||||
|
is_first_line=false
|
||||||
|
else
|
||||||
|
# Subsequent lines: just the apps
|
||||||
|
current_line="$display_item"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Add to current line
|
||||||
|
current_line="$current_line, $display_item"
|
||||||
|
fi
|
||||||
|
((idx++))
|
||||||
done
|
done
|
||||||
if [[ ${#formatted_apps[@]} -gt 0 ]]; then
|
# Add the last line
|
||||||
local IFS=', '
|
if [[ -n "$current_line" ]]; then
|
||||||
success_line+=": ${formatted_apps[*]}"
|
summary_details+=("$current_line")
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
summary_details+=("$success_line")
|
||||||
fi
|
fi
|
||||||
summary_details+=("$success_line")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $failed_count -gt 0 ]]; then
|
if [[ $failed_count -gt 0 ]]; then
|
||||||
|
|||||||
@@ -262,14 +262,8 @@ EOF
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ${#selected_indices[@]} -eq 0 ]]; then
|
# Allow empty selection - don't auto-select cursor position
|
||||||
local default_idx=$((top_index + cursor_pos))
|
# This fixes the bug where unselecting all items would still select the last cursor position
|
||||||
if [[ $default_idx -ge 0 && $default_idx -lt $total_items ]]; then
|
|
||||||
selected[default_idx]=true
|
|
||||||
selected_indices=("$default_idx")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
local final_result=""
|
local final_result=""
|
||||||
if [[ ${#selected_indices[@]} -gt 0 ]]; then
|
if [[ ${#selected_indices[@]} -gt 0 ]]; then
|
||||||
local IFS=','
|
local IFS=','
|
||||||
|
|||||||
18
mole
18
mole
@@ -47,7 +47,8 @@ check_for_updates() {
|
|||||||
|
|
||||||
# Background version check (save to file, don't output)
|
# Background version check (save to file, don't output)
|
||||||
(
|
(
|
||||||
local latest=$(get_latest_version)
|
local latest
|
||||||
|
latest=$(get_latest_version)
|
||||||
|
|
||||||
if [[ -n "$latest" && "$VERSION" != "$latest" && "$(printf '%s\n' "$VERSION" "$latest" | sort -V | head -1)" == "$VERSION" ]]; then
|
if [[ -n "$latest" && "$VERSION" != "$latest" && "$(printf '%s\n' "$VERSION" "$latest" | sort -V | head -1)" == "$VERSION" ]]; then
|
||||||
printf "\nUpdate available: %s → %s, run %smo update%s\n\n" "$VERSION" "$latest" "$GREEN" "$NC" > "$msg_cache"
|
printf "\nUpdate available: %s → %s, run %smo update%s\n\n" "$VERSION" "$latest" "$GREEN" "$NC" > "$msg_cache"
|
||||||
@@ -155,7 +156,8 @@ update_mole() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for updates
|
# Check for updates
|
||||||
local latest=$(get_latest_version)
|
local latest
|
||||||
|
latest=$(get_latest_version)
|
||||||
|
|
||||||
if [[ -z "$latest" ]]; then
|
if [[ -z "$latest" ]]; then
|
||||||
log_error "Unable to check for updates. Check network connection."
|
log_error "Unable to check for updates. Check network connection."
|
||||||
@@ -449,9 +451,11 @@ show_main_menu() {
|
|||||||
interactive_main_menu() {
|
interactive_main_menu() {
|
||||||
# Show intro animation only once per terminal tab
|
# Show intro animation only once per terminal tab
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
local tty_name=$(tty 2>/dev/null || echo "")
|
local tty_name
|
||||||
|
tty_name=$(tty 2>/dev/null || echo "")
|
||||||
if [[ -n "$tty_name" ]]; then
|
if [[ -n "$tty_name" ]]; then
|
||||||
local flag_file="/tmp/mole_intro_$(echo "$tty_name" | tr -c '[:alnum:]_' '_')"
|
local flag_file
|
||||||
|
flag_file="/tmp/mole_intro_$(echo "$tty_name" | tr -c '[:alnum:]_' '_')"
|
||||||
if [[ ! -f "$flag_file" ]]; then
|
if [[ ! -f "$flag_file" ]]; then
|
||||||
animate_mole_intro
|
animate_mole_intro
|
||||||
touch "$flag_file" 2>/dev/null || true
|
touch "$flag_file" 2>/dev/null || true
|
||||||
@@ -489,8 +493,10 @@ interactive_main_menu() {
|
|||||||
# Drain any pending input to prevent touchpad scroll issues
|
# Drain any pending input to prevent touchpad scroll issues
|
||||||
drain_pending_input
|
drain_pending_input
|
||||||
|
|
||||||
local key=$(read_key)
|
local key
|
||||||
[[ $? -ne 0 ]] && continue
|
if ! key=$(read_key); then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# shellcheck disable=SC2329
|
|
||||||
# Helper stub definitions for uninstall tests
|
|
||||||
|
|
||||||
setup_uninstall_stubs() {
|
|
||||||
request_sudo_access() { return 0; }
|
|
||||||
start_inline_spinner() { :; }
|
|
||||||
stop_inline_spinner() { :; }
|
|
||||||
enter_alt_screen() { :; }
|
|
||||||
leave_alt_screen() { :; }
|
|
||||||
hide_cursor() { :; }
|
|
||||||
show_cursor() { :; }
|
|
||||||
remove_apps_from_dock() { :; }
|
|
||||||
|
|
||||||
pgrep() { return 1; }
|
|
||||||
pkill() { return 0; }
|
|
||||||
sudo() { return 0; }
|
|
||||||
|
|
||||||
export -f request_sudo_access start_inline_spinner stop_inline_spinner \
|
|
||||||
enter_alt_screen leave_alt_screen hide_cursor show_cursor \
|
|
||||||
remove_apps_from_dock pgrep pkill sudo || true
|
|
||||||
}
|
|
||||||
49
tests/run.sh
49
tests/run.sh
@@ -4,26 +4,41 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
if ! command -v bats >/dev/null 2>&1; then
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
SHELLCHECK_TARGETS=()
|
||||||
|
while IFS= read -r file; do
|
||||||
|
SHELLCHECK_TARGETS+=("$file")
|
||||||
|
done < <(find "$PROJECT_ROOT/tests" -type f \( -name '*.bats' -o -name '*.sh' \) | sort)
|
||||||
|
|
||||||
|
if [[ ${#SHELLCHECK_TARGETS[@]} -gt 0 ]]; then
|
||||||
|
shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${SHELLCHECK_TARGETS[@]}"
|
||||||
|
else
|
||||||
|
echo "No shell files to lint under tests/." >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "shellcheck not found; skipping linting." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v bats >/dev/null 2>&1; then
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
if [[ -z "${TERM:-}" ]]; then
|
||||||
|
export TERM="xterm-256color"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
set -- tests
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
bats -p "$@"
|
||||||
|
else
|
||||||
|
TERM="${TERM:-xterm-256color}" bats --tap "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
cat <<'EOF' >&2
|
cat <<'EOF' >&2
|
||||||
bats is required to run Mole's test suite.
|
bats is required to run Mole's test suite.
|
||||||
Install via Homebrew with 'brew install bats-core' or via npm with 'npm install -g bats'.
|
Install via Homebrew with 'brew install bats-core' or via npm with 'npm install -g bats'.
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
|
|
||||||
if [[ -z "${TERM:-}" ]]; then
|
|
||||||
export TERM="xterm-256color"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $# -eq 0 ]]; then
|
|
||||||
set -- tests
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
bats -p "$@"
|
|
||||||
else
|
|
||||||
TERM="${TERM:-xterm-256color}" bats --tap "$@"
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -77,8 +77,19 @@ EOF
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/common.sh"
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/batch_uninstall.sh"
|
source "$PROJECT_ROOT/lib/batch_uninstall.sh"
|
||||||
source "$PROJECT_ROOT/tests/helpers/uninstall_stubs.sh"
|
|
||||||
setup_uninstall_stubs
|
# Test stubs
|
||||||
|
request_sudo_access() { return 0; }
|
||||||
|
start_inline_spinner() { :; }
|
||||||
|
stop_inline_spinner() { :; }
|
||||||
|
enter_alt_screen() { :; }
|
||||||
|
leave_alt_screen() { :; }
|
||||||
|
hide_cursor() { :; }
|
||||||
|
show_cursor() { :; }
|
||||||
|
remove_apps_from_dock() { :; }
|
||||||
|
pgrep() { return 1; }
|
||||||
|
pkill() { return 0; }
|
||||||
|
sudo() { return 0; }
|
||||||
|
|
||||||
app_bundle="$HOME/Applications/TestApp.app"
|
app_bundle="$HOME/Applications/TestApp.app"
|
||||||
mkdir -p "$app_bundle"
|
mkdir -p "$app_bundle"
|
||||||
|
|||||||
29
tests/z_shellcheck.bats
Normal file
29
tests/z_shellcheck.bats
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "shellcheck passes for test scripts" {
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
skip "shellcheck not installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run env PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
targets=()
|
||||||
|
while IFS= read -r file; do
|
||||||
|
targets+=("$file")
|
||||||
|
done < <(find "$PROJECT_ROOT/tests" -type f \( -name '*.bats' -o -name '*.sh' \) | sort)
|
||||||
|
if [[ ${#targets[@]} -eq 0 ]]; then
|
||||||
|
echo "No test shell files found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${targets[@]}"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
printf '%s\n' "$output" >&3
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user