1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 17:55:08 +00:00
Files
Mole/lib/clean/hints.sh
2026-02-28 11:03:16 +08:00

354 lines
11 KiB
Bash

#!/bin/bash
# Hint notices used by `mo clean` (non-destructive guidance only).
set -euo pipefail
mole_hints_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1090
source "$mole_hints_dir/purge_shared.sh"
# Quick reminder probe for project build artifacts handled by `mo purge`.
# Designed to be very fast: shallow directory checks only, no deep find scans.
# shellcheck disable=SC2329
load_quick_purge_hint_paths() {
local config_file="$HOME/.config/mole/purge_paths"
local -a paths=()
while IFS= read -r line; do
[[ -n "$line" ]] && paths+=("$line")
done < <(mole_purge_read_paths_config "$config_file")
if [[ ${#paths[@]} -eq 0 ]]; then
paths=("${MOLE_PURGE_DEFAULT_SEARCH_PATHS[@]}")
fi
if [[ ${#paths[@]} -gt 0 ]]; then
printf '%s\n' "${paths[@]}"
fi
}
# shellcheck disable=SC2329
hint_get_path_size_kb_with_timeout() {
local path="$1"
local timeout_seconds="${2:-0.8}"
local du_tmp
du_tmp=$(mktemp)
local du_status=0
if run_with_timeout "$timeout_seconds" du -skP "$path" > "$du_tmp" 2> /dev/null; then
du_status=0
else
du_status=$?
fi
if [[ $du_status -ne 0 ]]; then
rm -f "$du_tmp"
return 1
fi
local size_kb
size_kb=$(awk 'NR==1 {print $1; exit}' "$du_tmp")
rm -f "$du_tmp"
[[ "$size_kb" =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "$size_kb"
}
# shellcheck disable=SC2329
record_project_artifact_hint() {
local path="$1"
((PROJECT_ARTIFACT_HINT_COUNT++)) || true
if [[ ${#PROJECT_ARTIFACT_HINT_EXAMPLES[@]} -lt 2 ]]; then
PROJECT_ARTIFACT_HINT_EXAMPLES+=("${path/#$HOME/~}")
fi
local sample_max=3
if [[ $PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES -ge $sample_max ]]; then
PROJECT_ARTIFACT_HINT_ESTIMATE_PARTIAL=true
return 0
fi
local timeout_seconds="0.8"
local size_kb=""
if size_kb=$(hint_get_path_size_kb_with_timeout "$path" "$timeout_seconds"); then
if [[ "$size_kb" =~ ^[0-9]+$ ]]; then
((PROJECT_ARTIFACT_HINT_ESTIMATED_KB += size_kb)) || true
((PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES++)) || true
else
PROJECT_ARTIFACT_HINT_ESTIMATE_PARTIAL=true
fi
else
PROJECT_ARTIFACT_HINT_ESTIMATE_PARTIAL=true
fi
return 0
}
# shellcheck disable=SC2329
is_quick_purge_project_root() {
mole_purge_is_project_root "$1"
}
# shellcheck disable=SC2329
probe_project_artifact_hints() {
PROJECT_ARTIFACT_HINT_DETECTED=false
PROJECT_ARTIFACT_HINT_COUNT=0
PROJECT_ARTIFACT_HINT_TRUNCATED=false
PROJECT_ARTIFACT_HINT_EXAMPLES=()
PROJECT_ARTIFACT_HINT_ESTIMATED_KB=0
PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES=0
PROJECT_ARTIFACT_HINT_ESTIMATE_PARTIAL=false
local max_projects=200
local max_projects_per_root=0
local max_nested_per_project=120
local max_matches=12
local -a target_names=()
while IFS= read -r target_name; do
[[ -n "$target_name" ]] && target_names+=("$target_name")
done < <(mole_purge_quick_hint_target_names)
local -a scan_roots=()
while IFS= read -r path; do
[[ -n "$path" ]] && scan_roots+=("$path")
done < <(load_quick_purge_hint_paths)
[[ ${#scan_roots[@]} -eq 0 ]] && return 0
# Fairness: avoid one very large root exhausting the entire scan budget.
if [[ $max_projects_per_root -le 0 ]]; then
max_projects_per_root=$(((max_projects + ${#scan_roots[@]} - 1) / ${#scan_roots[@]}))
[[ $max_projects_per_root -lt 25 ]] && max_projects_per_root=25
fi
[[ $max_projects_per_root -gt $max_projects ]] && max_projects_per_root=$max_projects
local nullglob_was_set=0
if shopt -q nullglob; then
nullglob_was_set=1
fi
shopt -s nullglob
local scanned_projects=0
local stop_scan=false
local root project_dir nested_dir target_name candidate
for root in "${scan_roots[@]}"; do
[[ -d "$root" ]] || continue
local root_projects_scanned=0
if is_quick_purge_project_root "$root"; then
((scanned_projects++)) || true
((root_projects_scanned++)) || true
if [[ $scanned_projects -gt $max_projects ]]; then
PROJECT_ARTIFACT_HINT_TRUNCATED=true
stop_scan=true
break
fi
for target_name in "${target_names[@]}"; do
candidate="$root/$target_name"
if [[ -d "$candidate" ]]; then
record_project_artifact_hint "$candidate"
fi
done
fi
[[ "$stop_scan" == "true" ]] && break
if [[ $root_projects_scanned -ge $max_projects_per_root ]]; then
PROJECT_ARTIFACT_HINT_TRUNCATED=true
continue
fi
for project_dir in "$root"/*/; do
[[ -d "$project_dir" ]] || continue
project_dir="${project_dir%/}"
local project_name
project_name=$(basename "$project_dir")
[[ "$project_name" == .* ]] && continue
if [[ $root_projects_scanned -ge $max_projects_per_root ]]; then
PROJECT_ARTIFACT_HINT_TRUNCATED=true
break
fi
((scanned_projects++)) || true
((root_projects_scanned++)) || true
if [[ $scanned_projects -gt $max_projects ]]; then
PROJECT_ARTIFACT_HINT_TRUNCATED=true
stop_scan=true
break
fi
for target_name in "${target_names[@]}"; do
candidate="$project_dir/$target_name"
if [[ -d "$candidate" ]]; then
record_project_artifact_hint "$candidate"
fi
done
[[ "$stop_scan" == "true" ]] && break
local nested_count=0
for nested_dir in "$project_dir"/*/; do
[[ -d "$nested_dir" ]] || continue
nested_dir="${nested_dir%/}"
local nested_name
nested_name=$(basename "$nested_dir")
[[ "$nested_name" == .* ]] && continue
case "$nested_name" in
node_modules | target | build | dist | DerivedData | Pods)
continue
;;
esac
((nested_count++)) || true
if [[ $nested_count -gt $max_nested_per_project ]]; then
break
fi
for target_name in "${target_names[@]}"; do
candidate="$nested_dir/$target_name"
if [[ -d "$candidate" ]]; then
record_project_artifact_hint "$candidate"
fi
done
[[ "$stop_scan" == "true" ]] && break
done
[[ "$stop_scan" == "true" ]] && break
done
[[ "$stop_scan" == "true" ]] && break
done
if [[ $nullglob_was_set -eq 0 ]]; then
shopt -u nullglob
fi
if [[ $PROJECT_ARTIFACT_HINT_COUNT -gt 0 ]]; then
PROJECT_ARTIFACT_HINT_DETECTED=true
fi
# Preserve a compact display hint if candidate count is large, but do not
# stop scanning early solely because we exceeded this threshold.
if [[ $PROJECT_ARTIFACT_HINT_COUNT -gt $max_matches ]]; then
PROJECT_ARTIFACT_HINT_TRUNCATED=true
fi
return 0
}
# shellcheck disable=SC2329
show_system_data_hint_notice() {
local min_gb=2
local timeout_seconds="0.8"
local max_hits=3
local threshold_kb=$((min_gb * 1024 * 1024))
local -a clue_labels=()
local -a clue_sizes=()
local -a clue_paths=()
local -a labels=(
"Xcode DerivedData"
"Xcode Archives"
"iPhone backups"
"Simulator data"
"Docker Desktop data"
"Mail data"
)
local -a paths=(
"$HOME/Library/Developer/Xcode/DerivedData"
"$HOME/Library/Developer/Xcode/Archives"
"$HOME/Library/Application Support/MobileSync/Backup"
"$HOME/Library/Developer/CoreSimulator/Devices"
"$HOME/Library/Containers/com.docker.docker/Data"
"$HOME/Library/Mail"
)
local i
for i in "${!paths[@]}"; do
local path="${paths[$i]}"
[[ -d "$path" ]] || continue
local size_kb=""
if size_kb=$(hint_get_path_size_kb_with_timeout "$path" "$timeout_seconds"); then
if [[ "$size_kb" -ge "$threshold_kb" ]]; then
clue_labels+=("${labels[$i]}")
clue_sizes+=("$size_kb")
clue_paths+=("${path/#$HOME/~}")
if [[ ${#clue_labels[@]} -ge $max_hits ]]; then
break
fi
fi
fi
done
if [[ ${#clue_labels[@]} -eq 0 ]]; then
note_activity
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No common System Data clues detected"
return 0
fi
note_activity
for i in "${!clue_labels[@]}"; do
local human_size
human_size=$(bytes_to_human "$((clue_sizes[i] * 1024))")
echo -e " ${GREEN}${ICON_LIST}${NC} ${clue_labels[$i]}: ${human_size}"
echo -e " ${GRAY}${ICON_SUBLIST}${NC} Path: ${GRAY}${clue_paths[$i]}${NC}"
done
echo -e " ${GRAY}${ICON_REVIEW}${NC} Review: mo analyze, Device backups, docker system df"
}
# shellcheck disable=SC2329
show_project_artifact_hint_notice() {
probe_project_artifact_hints
if [[ "$PROJECT_ARTIFACT_HINT_DETECTED" != "true" ]]; then
return 0
fi
note_activity
local hint_count_label="$PROJECT_ARTIFACT_HINT_COUNT"
[[ "$PROJECT_ARTIFACT_HINT_TRUNCATED" == "true" ]] && hint_count_label="${hint_count_label}+"
local example_text=""
if [[ ${#PROJECT_ARTIFACT_HINT_EXAMPLES[@]} -gt 0 ]]; then
example_text="${PROJECT_ARTIFACT_HINT_EXAMPLES[0]}"
if [[ ${#PROJECT_ARTIFACT_HINT_EXAMPLES[@]} -gt 1 ]]; then
example_text+=", ${PROJECT_ARTIFACT_HINT_EXAMPLES[1]}"
fi
fi
if [[ $PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES -gt 0 ]]; then
local estimate_human
estimate_human=$(bytes_to_human "$((PROJECT_ARTIFACT_HINT_ESTIMATED_KB * 1024))")
local estimate_is_partial="$PROJECT_ARTIFACT_HINT_ESTIMATE_PARTIAL"
if [[ "$PROJECT_ARTIFACT_HINT_TRUNCATED" == "true" ]] || [[ $PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES -lt $PROJECT_ARTIFACT_HINT_COUNT ]]; then
estimate_is_partial=true
fi
if [[ "$estimate_is_partial" == "true" ]]; then
echo -e " ${GREEN}${ICON_LIST}${NC} ${GREEN}${hint_count_label}${NC} candidates, at least ${estimate_human} sampled from ${PROJECT_ARTIFACT_HINT_ESTIMATE_SAMPLES} items"
else
echo -e " ${GREEN}${ICON_LIST}${NC} ${GREEN}${hint_count_label}${NC} candidates, sampled ${estimate_human}"
fi
else
echo -e " ${GREEN}${ICON_LIST}${NC} ${GREEN}${hint_count_label}${NC} candidates"
fi
if [[ -n "$example_text" ]]; then
echo -e " ${GRAY}${ICON_SUBLIST}${NC} Examples: ${GRAY}${example_text}${NC}"
fi
echo -e " ${GRAY}${ICON_REVIEW}${NC} Review: mo purge"
}