diff --git a/bin/clean.sh b/bin/clean.sh index 058c310..292f5b5 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -699,7 +699,7 @@ start_cleanup() { echo "" if [[ "$DRY_RUN" != "true" && -t 0 ]]; then - echo -e "${GRAY}${ICON_SOLID} Use --dry-run to preview, --whitelist to manage protected paths${NC}" + echo -e "${GRAY}${ICON_WARNING} Use --dry-run to preview, --whitelist to manage protected paths${NC}" fi if [[ "$DRY_RUN" == "true" ]]; then @@ -979,6 +979,11 @@ perform_cleanup() { clean_time_machine_failed_backups end_section + # ===== 16. Large files to review (report only) ===== + start_section "Large files to review" + check_large_file_candidates + end_section + # ===== Final summary ===== echo "" diff --git a/lib/clean/system.sh b/lib/clean/system.sh index 4381970..071eb4a 100644 --- a/lib/clean/system.sh +++ b/lib/clean/system.sh @@ -269,7 +269,7 @@ tm_is_running() { grep -qE '(^|[[:space:]])("Running"|Running)[[:space:]]*=[[:space:]]*1([[:space:]]*;|$)' <<< "$st" } -# Local APFS snapshots (keep the most recent). +# Local APFS snapshots (report only). clean_local_snapshots() { if ! command -v tmutil > /dev/null 2>&1; then return 0 @@ -279,93 +279,25 @@ clean_local_snapshots() { tm_is_running || rc_running=$? if [[ $rc_running -eq 2 ]]; then - echo -e " ${YELLOW}!${NC} Could not determine Time Machine status; skipping snapshot cleanup" + echo -e " ${YELLOW}!${NC} Could not determine Time Machine status; skipping snapshot check" return 0 fi if [[ $rc_running -eq 0 ]]; then - echo -e " ${YELLOW}!${NC} Time Machine is active; skipping snapshot cleanup" + echo -e " ${YELLOW}!${NC} Time Machine is active; skipping snapshot check" return 0 fi start_section_spinner "Checking local snapshots..." local snapshot_list - snapshot_list=$(tmutil listlocalsnapshots / 2> /dev/null) + snapshot_list=$(run_with_timeout 3 tmutil listlocalsnapshots / 2> /dev/null || true) stop_section_spinner [[ -z "$snapshot_list" ]] && return 0 - local cleaned_count=0 - local total_cleaned_size=0 # Estimation not possible without thin - local newest_ts=0 - local newest_name="" - local -a snapshots=() - while IFS= read -r line; do - if [[ "$line" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then - local snap_name="${BASH_REMATCH[0]}" - snapshots+=("$snap_name") - local date_str="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]} ${BASH_REMATCH[4]:0:2}:${BASH_REMATCH[4]:2:2}:${BASH_REMATCH[4]:4:2}" - local snap_ts=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%s" 2> /dev/null || echo "0") - [[ "$snap_ts" == "0" ]] && continue - if [[ "$snap_ts" -gt "$newest_ts" ]]; then - newest_ts="$snap_ts" - newest_name="$snap_name" - fi - fi - done <<< "$snapshot_list" - [[ ${#snapshots[@]} -eq 0 ]] && return 0 - [[ -z "$newest_name" ]] && return 0 - - local deletable_count=$((${#snapshots[@]} - 1)) - [[ $deletable_count -le 0 ]] && return 0 - - if [[ "$DRY_RUN" != "true" ]]; then - if [[ ! -t 0 ]]; then - echo -e " ${YELLOW}!${NC} ${#snapshots[@]} local snapshot(s) found, skipping non-interactive mode" - echo -e " ${GRAY}${ICON_WARNING}${NC} ${GRAY}Tip: Snapshots may cause Disk Utility to show different 'Available' values${NC}" - return 0 - fi - echo -e " ${YELLOW}!${NC} Time Machine local snapshots found" - echo -e " ${GRAY}macOS can recreate them if needed.${NC}" - echo -e " ${GRAY}The most recent snapshot will be kept.${NC}" - echo -ne " ${PURPLE}${ICON_ARROW}${NC} Remove all local snapshots except the most recent one? ${GREEN}Enter${NC} continue, ${GRAY}Space${NC} skip: " - local choice - if type read_key > /dev/null 2>&1; then - choice=$(read_key) - else - IFS= read -r -s -n 1 choice || choice="" - if [[ -z "$choice" || "$choice" == $'\n' || "$choice" == $'\r' ]]; then - choice="ENTER" - fi - fi - if [[ "$choice" == "ENTER" ]]; then - printf "\r\033[K" # Clear the prompt line - else - echo -e " ${GRAY}Skipped${NC}" - return 0 - fi - fi - - local snap_name - for snap_name in "${snapshots[@]}"; do - if [[ "$snap_name" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then - if [[ "${BASH_REMATCH[0]}" != "$newest_name" ]]; then - if [[ "$DRY_RUN" == "true" ]]; then - echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Local snapshot: $snap_name ${YELLOW}dry-run${NC}" - ((cleaned_count++)) - note_activity - else - if sudo tmutil deletelocalsnapshots "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}-${BASH_REMATCH[4]}" > /dev/null 2>&1; then - echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed snapshot: $snap_name" - ((cleaned_count++)) - note_activity - else - echo -e " ${YELLOW}!${NC} Failed to remove: $snap_name" - fi - fi - fi - fi - done - if [[ $cleaned_count -gt 0 && "$DRY_RUN" != "true" ]]; then - log_success "Cleaned $cleaned_count local snapshots, kept latest" + local snapshot_count + snapshot_count=$(echo "$snapshot_list" | grep -Eo 'com\.apple\.TimeMachine\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}' | wc -l | awk '{print $1}') + if [[ "$snapshot_count" =~ ^[0-9]+$ && "$snapshot_count" -gt 0 ]]; then + echo -e " ${YELLOW}${ICON_WARNING}${NC} Time Machine local snapshots: ${GREEN}${snapshot_count}${NC}${GRAY}, Review: tmutil listlocalsnapshots /${NC}" + note_activity fi } diff --git a/lib/clean/user.sh b/lib/clean/user.sh index c457f65..1ba543c 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -600,13 +600,107 @@ check_ios_device_backups() { local backup_human=$(command du -sh "$backup_dir" 2> /dev/null | awk '{print $1}') if [[ -n "$backup_human" ]]; then note_activity - echo -e " Found ${GREEN}${backup_human}${NC} iOS backups" - echo -e " You can delete them manually: ${backup_dir}" + echo -e " ${YELLOW}${ICON_WARNING}${NC} iOS backups: ${GREEN}${backup_human}${NC}${GRAY}, Path: $backup_dir${NC}" fi fi fi return 0 } + +# Large file candidates (report only, no deletion). +check_large_file_candidates() { + local threshold_kb=$((1024 * 1024)) # 1GB + local found_any=false + + local mail_dir="$HOME/Library/Mail" + if [[ -d "$mail_dir" ]]; then + local mail_kb + mail_kb=$(get_path_size_kb "$mail_dir") + if [[ "$mail_kb" -ge "$threshold_kb" ]]; then + local mail_human + mail_human=$(bytes_to_human "$((mail_kb * 1024))") + echo -e " ${YELLOW}${ICON_WARNING}${NC} Mail data: ${GREEN}${mail_human}${NC}${GRAY}, Path: $mail_dir${NC}" + found_any=true + fi + fi + + local mail_downloads="$HOME/Library/Mail Downloads" + if [[ -d "$mail_downloads" ]]; then + local downloads_kb + downloads_kb=$(get_path_size_kb "$mail_downloads") + if [[ "$downloads_kb" -ge "$threshold_kb" ]]; then + local downloads_human + downloads_human=$(bytes_to_human "$((downloads_kb * 1024))") + echo -e " ${YELLOW}${ICON_WARNING}${NC} Mail downloads: ${GREEN}${downloads_human}${NC}${GRAY}, Path: $mail_downloads${NC}" + found_any=true + fi + fi + + local installer_path + for installer_path in /Applications/Install\ macOS*.app; do + if [[ -e "$installer_path" ]]; then + local installer_kb + installer_kb=$(get_path_size_kb "$installer_path") + if [[ "$installer_kb" -gt 0 ]]; then + local installer_human + installer_human=$(bytes_to_human "$((installer_kb * 1024))") + echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS installer: ${GREEN}${installer_human}${NC}${GRAY}, Path: $installer_path${NC}" + found_any=true + fi + fi + done + + local updates_dir="$HOME/Library/Updates" + if [[ -d "$updates_dir" ]]; then + local updates_kb + updates_kb=$(get_path_size_kb "$updates_dir") + if [[ "$updates_kb" -ge "$threshold_kb" ]]; then + local updates_human + updates_human=$(bytes_to_human "$((updates_kb * 1024))") + echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS updates cache: ${GREEN}${updates_human}${NC}${GRAY}, Path: $updates_dir${NC}" + found_any=true + fi + fi + + if [[ "${SYSTEM_CLEAN:-false}" != "true" ]] && command -v tmutil > /dev/null 2>&1; then + local snapshot_list snapshot_count + snapshot_list=$(run_with_timeout 3 tmutil listlocalsnapshots / 2> /dev/null || true) + if [[ -n "$snapshot_list" ]]; then + snapshot_count=$(echo "$snapshot_list" | grep -Eo 'com\.apple\.TimeMachine\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}' | wc -l | awk '{print $1}') + if [[ "$snapshot_count" =~ ^[0-9]+$ && "$snapshot_count" -gt 0 ]]; then + echo -e " ${YELLOW}${ICON_WARNING}${NC} Time Machine local snapshots: ${GREEN}${snapshot_count}${NC}${GRAY}, Review: tmutil listlocalsnapshots /${NC}" + found_any=true + fi + fi + fi + + if command -v docker > /dev/null 2>&1; then + local docker_output + docker_output=$(run_with_timeout 3 docker system df --format '{{.Type}}\t{{.Size}}\t{{.Reclaimable}}' 2> /dev/null || true) + if [[ -n "$docker_output" ]]; then + echo -e " ${YELLOW}${ICON_WARNING}${NC} Docker storage:" + while IFS=$'\t' read -r dtype dsize dreclaim; do + [[ -z "$dtype" ]] && continue + echo -e " ${GRAY}• $dtype: $dsize, Reclaimable: $dreclaim${NC}" + done <<< "$docker_output" + found_any=true + else + docker_output=$(run_with_timeout 3 docker system df 2> /dev/null || true) + if [[ -n "$docker_output" ]]; then + echo -e " ${YELLOW}${ICON_WARNING}${NC} Docker storage:" + echo -e " ${GRAY}• Run: docker system df${NC}" + found_any=true + fi + fi + fi + + if [[ "$found_any" == "false" ]]; then + echo -e " ${GREEN}${ICON_SUCCESS}${NC} No large items detected in common locations" + fi + + note_activity + return 0 +} # Apple Silicon specific caches (IS_M_SERIES). clean_apple_silicon_caches() { if [[ "${IS_M_SERIES:-false}" != "true" ]]; then diff --git a/lib/core/base.sh b/lib/core/base.sh index 99139ee..188f6ba 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -31,7 +31,7 @@ readonly ICON_CONFIRM="◎" readonly ICON_ADMIN="⚙" readonly ICON_SUCCESS="✓" readonly ICON_ERROR="☻" -readonly ICON_WARNING="●" +readonly ICON_WARNING="☉" readonly ICON_EMPTY="○" readonly ICON_SOLID="●" readonly ICON_LIST="•" diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 87014a1..85cc952 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -332,7 +332,7 @@ batch_uninstall_applications() { while IFS= read -r file; do if [[ -n "$file" && -e "$file" ]]; then if [[ $sys_file_count -lt $max_files ]]; then - echo -e " ${BLUE}${ICON_SOLID}${NC} System: $file" + echo -e " ${BLUE}${ICON_WARNING}${NC} System: $file" fi ((sys_file_count++)) fi