diff --git a/bin/clean.sh b/bin/clean.sh index f03c162..f816105 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -363,7 +363,7 @@ start_cleanup() { if [[ -t 0 ]]; then echo "" - echo -ne "${ICON_SETTINGS} ${BLUE}System cleanup?${NC} ${GRAY}Enter to continue, any key to skip${NC} " + echo -ne "${BLUE}${ICON_SETTINGS}${NC} ${BLUE}System cleanup?${NC} ${GRAY}Enter to continue, any key to skip${NC} " # Use IFS= and read without -n to allow Ctrl+C to work properly IFS= read -r -s -n 1 choice @@ -406,7 +406,7 @@ start_cleanup() { else # Any other key = no system cleanup SYSTEM_CLEAN=false - echo -e "Skipped system cleanup, user-level only" + echo -e "${BLUE}${ICON_EMPTY}${NC} Skipped system cleanup, user-level only" fi else SYSTEM_CLEAN=false @@ -1346,68 +1346,80 @@ perform_cleanup() { space_freed_kb=$((space_after - space_before)) echo "" - echo "====================================================================" + + local summary_heading="" + local summary_status="success" if [[ "$DRY_RUN" == "true" ]]; then - echo "DRY RUN COMPLETE!" + summary_heading="Dry run complete" else - echo "CLEANUP COMPLETE!" + summary_heading="Cleanup complete" fi + local -a summary_details=() + if [[ $total_size_cleaned -gt 0 ]]; then - local freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}') + local freed_gb + freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}') + if [[ "$DRY_RUN" == "true" ]]; then - echo "Potential reclaimable space: ${GREEN}${freed_gb}GB${NC} (no changes made) | Free space now: $(get_free_space)" - - # Show file/category stats for dry run - if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then - printf "Files to clean: %s | Categories: %s" "$files_cleaned" "$total_items" - [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" - printf "\n" - elif [[ $files_cleaned -gt 0 ]]; then - printf "Files to clean: %s" "$files_cleaned" - [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" - printf "\n" - elif [[ $total_items -gt 0 ]]; then - printf "Categories: %s" "$total_items" - [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" - printf "\n" - fi - - echo "" - echo "To protect specific cache files from deletion, run: mo clean --whitelist" + summary_details+=("Potential reclaimable space: ${GREEN}${freed_gb}GB${NC}") + summary_details+=("No changes were made in this mode.") else - echo "Space freed: ${GREEN}${freed_gb}GB${NC} | Free space now: $(get_free_space)" + summary_details+=("Space freed: ${GREEN}${freed_gb}GB${NC}") + fi + summary_details+=("Free space now: $(get_free_space)") - # Show file/category stats for actual cleanup - if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then - printf "Files cleaned: %s | Categories: %s" "$files_cleaned" "$total_items" - [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" - printf "\n" - elif [[ $files_cleaned -gt 0 ]]; then - printf "Files cleaned: %s" "$files_cleaned" - [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" - printf "\n" - elif [[ $total_items -gt 0 ]]; then - printf "Categories: %s" "$total_items" - [[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count" - printf "\n" + if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then + local stats + if [[ "$DRY_RUN" == "true" ]]; then + stats="Files to clean: $files_cleaned | Categories: $total_items" + else + stats="Files cleaned: $files_cleaned | Categories: $total_items" fi + [[ $whitelist_skipped_count -gt 0 ]] && stats+=" | Protected: $whitelist_skipped_count" + summary_details+=("$stats") + elif [[ $files_cleaned -gt 0 ]]; then + local stats + if [[ "$DRY_RUN" == "true" ]]; then + stats="Files to clean: $files_cleaned" + else + stats="Files cleaned: $files_cleaned" + fi + [[ $whitelist_skipped_count -gt 0 ]] && stats+=" | Protected: $whitelist_skipped_count" + summary_details+=("$stats") + elif [[ $total_items -gt 0 ]]; then + local stats + if [[ "$DRY_RUN" == "true" ]]; then + stats="Categories to review: $total_items" + else + stats="Categories: $total_items" + fi + [[ $whitelist_skipped_count -gt 0 ]] && stats+=" | Protected: $whitelist_skipped_count" + summary_details+=("$stats") + fi + if [[ "$DRY_RUN" == "true" ]]; then + summary_details+=("Protect specific caches anytime with: mo clean --whitelist") + else if [[ $(echo "$freed_gb" | awk '{print ($1 >= 1) ? 1 : 0}') -eq 1 ]]; then - local movies=$(echo "$freed_gb" | awk '{printf "%.0f", $1/4.5}') + local movies + movies=$(echo "$freed_gb" | awk '{printf "%.0f", $1/4.5}') if [[ $movies -gt 0 ]]; then - echo "That's like ~$movies 4K movies worth of space!" + summary_details+=("Equivalent to ~$movies 4K movies of storage.") fi fi fi else + summary_status="info" if [[ "$DRY_RUN" == "true" ]]; then - echo "No significant reclaimable space detected (already clean) | Free space: $(get_free_space)" + summary_details+=("No significant reclaimable space detected (system already clean).") else - echo "No significant space was freed (system was already clean) | Free space: $(get_free_space)" + summary_details+=("System was already clean; no additional space freed.") fi + summary_details+=("Free space now: $(get_free_space)") fi - printf "====================================================================\n" + + print_summary_block "$summary_status" "$summary_heading" "${summary_details[@]}" } diff --git a/lib/batch_uninstall.sh b/lib/batch_uninstall.sh index 6cd6cdc..1c80ecd 100755 --- a/lib/batch_uninstall.sh +++ b/lib/batch_uninstall.sh @@ -175,37 +175,47 @@ batch_uninstall_applications() { done # Summary - local freed_display=$(bytes_to_human "$((total_size_freed * 1024))") - local bar="================================================================================" - echo "$bar" + local freed_display + freed_display=$(bytes_to_human "$((total_size_freed * 1024))") + + local summary_status="success" + local -a summary_details=() + if [[ $success_count -gt 0 ]]; then local success_list="${success_items[*]}" - echo -e "Removed: ${GREEN}${success_list}${NC} | Freed: ${GREEN}${freed_display}${NC}" + summary_details+=("Removed: ${GREEN}${success_list}${NC}") + summary_details+=("Freed space: ${GREEN}${freed_display}${NC}") fi + if [[ $failed_count -gt 0 ]]; then + summary_status="warn" + local failed_names=() - local reason_summary="" for item in "${failed_items[@]}"; do local name=${item%%:*} failed_names+=("$name") done local failed_list="${failed_names[*]}" - # Determine primary reason + local reason_summary="could not be removed" if [[ $failed_count -eq 1 ]]; then local first_reason=${failed_items[0]#*:} case "$first_reason" in - still*running*) reason_summary="still running" ;; + still*running*) reason_summary="is still running" ;; remove*failed*) reason_summary="could not be removed" ;; permission*) reason_summary="permission denied" ;; *) reason_summary="$first_reason" ;; esac - echo -e "Failed: ${RED}${failed_list}${NC} ${reason_summary}" - else - echo -e "Failed: ${RED}${failed_list}${NC} could not be removed" fi + summary_details+=("Failed: ${RED}${failed_list}${NC} ${reason_summary}") fi - echo "$bar" + + if [[ $success_count -eq 0 && $failed_count -eq 0 ]]; then + summary_status="info" + summary_details+=("No applications were uninstalled.") + fi + + print_summary_block "$summary_status" "Uninstall complete" "${summary_details[@]}" if [[ ${#dock_cleanup_paths[@]} -gt 0 ]]; then remove_apps_from_dock "${dock_cleanup_paths[@]}" diff --git a/lib/common.sh b/lib/common.sh index 47140f3..e57c5eb 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -22,7 +22,7 @@ readonly NC="${ESC}[0m" # Icon definitions readonly ICON_CONFIRM="◎" # Confirm operation -readonly ICON_ADMIN="●" # Admin permission +readonly ICON_ADMIN="⚙" # Admin permission readonly ICON_SUCCESS="✓" # Success readonly ICON_ERROR="✗" # Error readonly ICON_EMPTY="○" # Empty state @@ -116,6 +116,39 @@ icon_menu() { echo -e "${BLUE}${ICON_MENU} ${num}. ${text}${NC}" } +# Consistent summary blocks for command results +print_summary_block() { + local status="info" + local heading="" + + if [[ $# -gt 0 ]]; then + status="$1" + shift + fi + + if [[ $# -gt 0 ]]; then + heading="$1" + shift + fi + + local -a details=("$@") + local divider="======================================================================" + + local color="$BLUE" + + local indent=" " + + echo "$divider" + if [[ -n "$heading" ]]; then + echo -e "${BLUE}${heading}${NC}" + fi + for detail in "${details[@]}"; do + [[ -z "$detail" ]] && continue + echo -e "${detail}" + done + echo "$divider" +} + # System detection detect_architecture() { if [[ "$(uname -m)" == "arm64" ]]; then @@ -343,7 +376,7 @@ request_sudo_access() { # If Touch ID is supported and not forced to use password if [[ "$force_password" != "true" ]] && check_touchid_support; then - echo -e "${BLUE}${ICON_ADMIN}${NC} ${prompt_msg} ${GRAY}(Touch ID or password)${NC}" + echo -e "${GRAY}${ICON_ADMIN}${NC} ${GRAY}${prompt_msg} (Touch ID or password)${NC}" if sudo -v 2>/dev/null; then return 0 else @@ -351,8 +384,8 @@ request_sudo_access() { fi else # Traditional password method - echo -e "${BLUE}${ICON_ADMIN}${NC} ${prompt_msg}" - echo -ne "${BLUE}${ICON_MENU}${NC} Password: " + echo -e "${GRAY}${ICON_ADMIN}${NC} ${GRAY}${prompt_msg}${NC}" + echo -ne "${GRAY}${ICON_MENU}${NC} Password: " read -s password echo "" if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh index 50ff8eb..8563ec5 100755 --- a/lib/paginated_menu.sh +++ b/lib/paginated_menu.sh @@ -25,9 +25,8 @@ paginated_multi_select() { local total_items=${#items[@]} local items_per_page=15 - local total_pages=$(( (total_items + items_per_page - 1) / items_per_page )) - local current_page=0 local cursor_pos=0 + local top_index=0 local -a selected=() # Initialize selection array @@ -113,30 +112,48 @@ paginated_multi_select() { # Clear each line as we go instead of clearing entire screen local clear_line="\r\033[2K" - # Header - compute underline length without external seq dependency - local title_clean="${title//[^[:print:]]/}" - local underline_len=${#title_clean} - [[ $underline_len -lt 10 ]] && underline_len=10 - # Build underline robustly (no seq); printf width then translate spaces to '=' - local underline - underline=$(printf '%*s' "$underline_len" '' | tr ' ' '=') - printf "${clear_line}${PURPLE}%s${NC}\n" "$title" >&2 - printf "${clear_line}%s\n" "$underline" >&2 - - # Status + # Count selections for header display local selected_count=0 for ((i = 0; i < total_items; i++)); do [[ ${selected[i]} == true ]] && ((selected_count++)) done - printf "${clear_line}Page %d/%d │ Total: %d │ Selected: %d\n\n" \ - $((current_page + 1)) $total_pages $total_items $selected_count >&2 - # Items for current page - local start_idx=$((current_page * items_per_page)) - local end_idx=$((start_idx + items_per_page - 1)) + # Header + printf "${clear_line}${PURPLE}%s${NC} ${GRAY}%d/%d selected${NC}\n" "${title}" "$selected_count" "$total_items" >&2 + + if [[ $total_items -eq 0 ]]; then + printf "${clear_line}${GRAY}No items available${NC}\n" >&2 + printf "${clear_line}\n" >&2 + printf "${clear_line}${GRAY}Q/ESC${NC} Quit\n" >&2 + printf "${clear_line}" >&2 + return + fi + + if [[ $top_index -gt $((total_items - 1)) ]]; then + if [[ $total_items -gt $items_per_page ]]; then + top_index=$((total_items - items_per_page)) + else + top_index=0 + fi + fi + + local visible_count=$((total_items - top_index)) + [[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page + [[ $visible_count -le 0 ]] && visible_count=1 + if [[ $cursor_pos -ge $visible_count ]]; then + cursor_pos=$((visible_count - 1)) + [[ $cursor_pos -lt 0 ]] && cursor_pos=0 + fi + + printf "${clear_line}\n" >&2 + + # Items for current window + local start_idx=$top_index + local end_idx=$((top_index + items_per_page - 1)) [[ $end_idx -ge $total_items ]] && end_idx=$((total_items - 1)) for ((i = start_idx; i <= end_idx; i++)); do + [[ $i -lt 0 ]] && continue local is_current=false [[ $((i - start_idx)) -eq $cursor_pos ]] && is_current=true render_item $i $is_current @@ -144,13 +161,14 @@ paginated_multi_select() { # Fill empty slots to clear previous content local items_shown=$((end_idx - start_idx + 1)) + [[ $items_shown -lt 0 ]] && items_shown=0 for ((i = items_shown; i < items_per_page; i++)); do printf "${clear_line}\n" >&2 done # Clear any remaining lines at bottom printf "${clear_line}\n" >&2 - printf "${clear_line}${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit ${GRAY}|${NC} ○ off ● on\n" >&2 + printf "${clear_line}${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit\n" >&2 # Clear one more line to ensure no artifacts printf "${clear_line}" >&2 @@ -184,31 +202,38 @@ EOF return 1 ;; "UP") - if [[ $cursor_pos -gt 0 ]]; then + if [[ $total_items -eq 0 ]]; then + : + elif [[ $cursor_pos -gt 0 ]]; then ((cursor_pos--)) - elif [[ $current_page -gt 0 ]]; then - ((current_page--)) - # Calculate cursor position for new page - local start_idx=$((current_page * items_per_page)) - local items_on_page=$((total_items - start_idx)) - [[ $items_on_page -gt $items_per_page ]] && items_on_page=$items_per_page - cursor_pos=$((items_on_page - 1)) + elif [[ $top_index -gt 0 ]]; then + ((top_index--)) fi ;; "DOWN") - local start_idx=$((current_page * items_per_page)) - local items_on_page=$((total_items - start_idx)) - [[ $items_on_page -gt $items_per_page ]] && items_on_page=$items_per_page + if [[ $total_items -eq 0 ]]; then + : + else + local absolute_index=$((top_index + cursor_pos)) + if [[ $absolute_index -lt $((total_items - 1)) ]]; then + local visible_count=$((total_items - top_index)) + [[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page - if [[ $cursor_pos -lt $((items_on_page - 1)) ]]; then - ((cursor_pos++)) - elif [[ $current_page -lt $((total_pages - 1)) ]]; then - ((current_page++)) - cursor_pos=0 + if [[ $cursor_pos -lt $((visible_count - 1)) ]]; then + ((cursor_pos++)) + elif [[ $((top_index + visible_count)) -lt $total_items ]]; then + ((top_index++)) + visible_count=$((total_items - top_index)) + [[ $visible_count -gt $items_per_page ]] && visible_count=$items_per_page + if [[ $cursor_pos -ge $visible_count ]]; then + cursor_pos=$((visible_count - 1)) + fi + fi + fi fi ;; "SPACE") - local idx=$((current_page * items_per_page + cursor_pos)) + local idx=$((top_index + cursor_pos)) if [[ $idx -lt $total_items ]]; then if [[ ${selected[idx]} == true ]]; then selected[idx]=false diff --git a/lib/whitelist_manager.sh b/lib/whitelist_manager.sh index a23ab3f..e1c3fa7 100755 --- a/lib/whitelist_manager.sh +++ b/lib/whitelist_manager.sh @@ -280,9 +280,9 @@ manage_whitelist_categories() { save_whitelist_patterns fi - echo "" - echo -e "${GREEN}✓${NC} Protected ${#selected_patterns[@]} cache(s)" - echo -e "${GRAY}Config: ${WHITELIST_CONFIG}${NC}" + print_summary_block "success" \ + "Protected ${#selected_patterns[@]} cache(s)" \ + "Saved to ${WHITELIST_CONFIG}" } diff --git a/mole b/mole index 7db83e4..f13b1cd 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/common.sh" # Version info -VERSION="1.7.4" +VERSION="1.7.5" MOLE_TAGLINE="can dig deep to clean your Mac." # Get latest version from remote repository @@ -38,10 +38,10 @@ check_for_updates() { local msg_cache="$HOME/.cache/mole/update_message" mkdir -p "$(dirname "$cache")" 2>/dev/null - # Skip if checked within 24 hours + # Skip if checked within 3 hours if [[ -f "$cache" ]]; then local age=$(($(date +%s) - $(stat -f%m "$cache" 2>/dev/null || echo 0))) - [[ $age -lt 86400 ]] && return + [[ $age -lt 10800 ]] && return fi # Background version check (save to file, don't output)