diff --git a/bin/purge.sh b/bin/purge.sh index cd373bd..03a7dc9 100755 --- a/bin/purge.sh +++ b/bin/purge.sh @@ -40,6 +40,53 @@ note_activity() { fi } +# Keep the most specific tail of a long purge path visible on the live scan line. +compact_purge_scan_path() { + local path="$1" + local max_path_len="${2:-0}" + + if ! [[ "$max_path_len" =~ ^[0-9]+$ ]] || [[ "$max_path_len" -lt 4 ]]; then + max_path_len=4 + fi + + if [[ ${#path} -le $max_path_len ]]; then + echo "$path" + return + fi + + local suffix_len=$((max_path_len - 3)) + local suffix="${path: -$suffix_len}" + local path_tail="" + local remainder="$path" + + while [[ "$remainder" == */* ]]; do + local segment="/${remainder##*/}" + remainder="${remainder%/*}" + + if [[ -z "$path_tail" ]]; then + if [[ ${#segment} -le $suffix_len ]]; then + path_tail="$segment" + else + break + fi + continue + fi + + if [[ $((${#segment} + ${#path_tail})) -le $suffix_len ]]; then + path_tail="${segment}${path_tail}" + else + break + fi + done + + if [[ -n "$path_tail" ]]; then + echo "...${path_tail}" + return + fi + + echo "...$suffix" +} + # Main purge function start_purge() { # Set current command for operation logging @@ -127,24 +174,13 @@ perform_purge() { # Set up trap to exit cleanly (erase the spinner line via /dev/tty) trap 'printf "\r\033[2K" >/dev/tty 2>/dev/null; exit 0' INT TERM - # Truncate path to guaranteed fit - truncate_path() { - local path="$1" - if [[ ${#path} -le $max_path_len ]]; then - echo "$path" - return - fi - local side_len=$(((max_path_len - 3) / 2)) - echo "${path:0:$side_len}...${path: -$side_len}" - } - while [[ -f "$stats_dir/purge_scanning" ]]; do local current_path current_path=$(cat "$stats_dir/purge_scanning" 2> /dev/null || echo "") if [[ -n "$current_path" ]]; then local display_path="${current_path/#$HOME/~}" - display_path=$(truncate_path "$display_path") + display_path=$(compact_purge_scan_path "$display_path" "$max_path_len") last_path="$display_path" fi @@ -291,4 +327,12 @@ main() { show_cursor } +if [[ "${MOLE_SKIP_MAIN:-0}" == "1" ]]; then + if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + return 0 + else + exit 0 + fi +fi + main "$@" diff --git a/lib/clean/project.sh b/lib/clean/project.sh index a7628b1..d7a73d3 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -26,6 +26,7 @@ readonly PURGE_CONFIG_FILE="$HOME/.config/mole/purge_paths" # Resolved search paths. PURGE_SEARCH_PATHS=() +PURGE_CATEGORY_FULL_PATHS_ARRAY=() # Project indicators for container detection. # Monorepo indicators (higher priority) @@ -155,6 +156,53 @@ load_purge_config() { # Initialize paths on script load. load_purge_config +format_purge_target_path() { + local path="$1" + echo "${path/#$HOME/~}" +} + +compact_purge_menu_path() { + local path="$1" + local max_width="${2:-0}" + + if ! [[ "$max_width" =~ ^[0-9]+$ ]] || [[ "$max_width" -lt 4 ]]; then + max_width=4 + fi + + local path_width + path_width=$(get_display_width "$path") + if [[ $path_width -le $max_width ]]; then + echo "$path" + return + fi + + local tail="" + local remainder="$path" + local prefix_width=3 + + while [[ "$remainder" == */* ]]; do + local segment="/${remainder##*/}" + remainder="${remainder%/*}" + + local candidate="${segment}${tail}" + local candidate_width + candidate_width=$(get_display_width "$candidate") + if [[ $((candidate_width + prefix_width)) -le $max_width ]]; then + tail="$candidate" + else + break + fi + done + + if [[ -n "$tail" ]]; then + echo "...${tail}" + return + fi + + local suffix_len=$((max_width - 3)) + echo "...${path: -$suffix_len}" +} + # Args: $1 - directory path # Determine whether a directory is a project root. # This is used to safely allow cleaning direct-child artifacts when @@ -542,7 +590,7 @@ select_purge_categories() { term_height=24 fi fi - local reserved=6 + local reserved=8 local available=$((term_height - reserved)) if [[ $available -lt 3 ]]; then echo 3 @@ -680,6 +728,13 @@ select_purge_categories() { # Keep one blank line between the list and footer tips. printf "%s\n" "$clear_line" + local current_index=$((top_index + cursor_pos)) + local current_full_path="${PURGE_CATEGORY_FULL_PATHS_ARRAY[current_index]:-}" + if [[ -n "$current_full_path" ]]; then + printf "%s${GRAY}Full path:${NC} %s\n" "$clear_line" "$current_full_path" + printf "%s\n" "$clear_line" + fi + # Adaptive footer hints — mirrors menu_paginated.sh pattern local _term_w _term_w=$(tput cols 2> /dev/null || echo 80) @@ -821,6 +876,7 @@ confirm_purge_cleanup() { local item_count="${1:-0}" local total_size_kb="${2:-0}" local unknown_count="${3:-0}" + local -a selected_paths=("${@:4}") [[ "$item_count" =~ ^[0-9]+$ ]] || item_count=0 [[ "$total_size_kb" =~ ^[0-9]+$ ]] || total_size_kb=0 @@ -839,6 +895,15 @@ confirm_purge_cleanup() { unknown_hint=", ${unknown_count} ${unknown_text}" fi + if [[ ${#selected_paths[@]} -gt 0 ]]; then + echo "" + echo -e "${GRAY}Selected paths:${NC}" + local selected_path="" + for selected_path in "${selected_paths[@]}"; do + echo " $selected_path" + done + fi + echo -ne "${PURPLE}${ICON_ARROW}${NC} Remove ${item_count} ${item_text}, ${size_display}${unknown_hint} ${GREEN}Enter${NC} confirm, ${GRAY}ESC${NC} cancel: " drain_pending_input local key="" @@ -1179,7 +1244,7 @@ clean_project_artifacts() { # Truncate project path if needed local truncated_path - truncated_path=$(truncate_by_display_width "$project_path" "$available_width") + truncated_path=$(compact_purge_menu_path "$project_path" "$available_width") local current_width current_width=$(get_display_width "$truncated_path") local char_count=${#truncated_path} @@ -1193,6 +1258,7 @@ clean_project_artifacts() { # Sizes are read from pre-computed results (parallel du calls launched above). local -a raw_project_paths=() local -a raw_artifact_types=() + local -a item_display_paths=() local _sz_idx=0 for item in "${safe_to_clean[@]}"; do local project_path @@ -1232,6 +1298,7 @@ clean_project_artifacts() { raw_project_paths+=("$project_path") raw_artifact_types+=("$artifact_type") item_paths+=("$item") + item_display_paths+=("$(format_purge_target_path "$item")") item_sizes+=("$size_kb") item_size_unknown_flags+=("$size_unknown") item_recent_flags+=("$is_recent") @@ -1349,8 +1416,10 @@ clean_project_artifacts() { ) # Interactive selection (only if terminal is available) PURGE_SELECTION_RESULT="" + PURGE_CATEGORY_FULL_PATHS_ARRAY=("${item_display_paths[@]}") if [[ -t 0 ]]; then if ! select_purge_categories "${menu_options[@]}"; then + PURGE_CATEGORY_FULL_PATHS_ARRAY=() unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT return 1 fi @@ -1367,12 +1436,14 @@ clean_project_artifacts() { echo "" echo -e "${GRAY}No items selected${NC}" printf '\n' + PURGE_CATEGORY_FULL_PATHS_ARRAY=() unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT return 0 fi IFS=',' read -r -a selected_indices <<< "$PURGE_SELECTION_RESULT" local selected_total_kb=0 local selected_unknown_count=0 + local -a selected_display_paths=() for idx in "${selected_indices[@]}"; do local selected_size_kb="${item_sizes[idx]:-0}" [[ "$selected_size_kb" =~ ^[0-9]+$ ]] || selected_size_kb=0 @@ -1380,16 +1451,19 @@ clean_project_artifacts() { if [[ "${item_size_unknown_flags[idx]:-false}" == "true" ]]; then selected_unknown_count=$((selected_unknown_count + 1)) fi + selected_display_paths+=("${item_display_paths[idx]}") done if [[ -t 0 ]]; then - if ! confirm_purge_cleanup "${#selected_indices[@]}" "$selected_total_kb" "$selected_unknown_count"; then + if ! confirm_purge_cleanup "${#selected_indices[@]}" "$selected_total_kb" "$selected_unknown_count" "${selected_display_paths[@]}"; then echo -e "${GRAY}Purge cancelled${NC}" printf '\n' + PURGE_CATEGORY_FULL_PATHS_ARRAY=() unset PURGE_CATEGORY_SIZES PURGE_RECENT_CATEGORIES PURGE_SELECTION_RESULT return 1 fi fi + PURGE_CATEGORY_FULL_PATHS_ARRAY=() # Clean selected items echo "" @@ -1398,8 +1472,8 @@ clean_project_artifacts() { local dry_run_mode="${MOLE_DRY_RUN:-0}" for idx in "${selected_indices[@]}"; do local item_path="${item_paths[idx]}" - local artifact_type=$(basename "$item_path") - local project_path=$(get_project_path "$item_path") + local display_item_path + display_item_path=$(format_purge_target_path "$item_path") local size_kb="${item_sizes[idx]}" local size_unknown="${item_size_unknown_flags[idx]:-false}" local size_human @@ -1413,7 +1487,7 @@ clean_project_artifacts() { continue fi if [[ -t 1 ]]; then - start_inline_spinner "Cleaning $project_path/$artifact_type..." + start_inline_spinner "Cleaning $display_item_path..." fi local removal_recorded=false if [[ -e "$item_path" ]]; then @@ -1431,9 +1505,9 @@ clean_project_artifacts() { stop_inline_spinner if [[ "$removal_recorded" == "true" ]]; then if [[ "$dry_run_mode" == "1" ]]; then - echo -e "${GREEN}${ICON_SUCCESS}${NC} [DRY RUN] $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}" + echo -e "${GREEN}${ICON_SUCCESS}${NC} [DRY RUN] $display_item_path${NC}, ${GREEN}$size_human${NC}" else - echo -e "${GREEN}${ICON_SUCCESS}${NC} $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}" + echo -e "${GREEN}${ICON_SUCCESS}${NC} $display_item_path${NC}, ${GREEN}$size_human${NC}" fi fi fi diff --git a/tests/purge.bats b/tests/purge.bats index 9895373..3e0e582 100644 --- a/tests/purge.bats +++ b/tests/purge.bats @@ -109,6 +109,39 @@ setup() { [[ "$result" == "ALLOWED" ]] } +@test "compact_purge_scan_path keeps the tail of long purge paths visible" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" MOLE_SKIP_MAIN=1 bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/bin/purge.sh" +compact_purge_scan_path "$HOME/projects/team/service/very/deep/component/node_modules" 32 +EOF + + [ "$status" -eq 0 ] + [[ "$output" == ".../deep/component/node_modules" ]] +} + +@test "compact_purge_menu_path keeps the project tail visible" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/clean/project.sh" +compact_purge_menu_path "$HOME/projects/team/service/very/deep/component/node_modules" 32 +EOF + + [ "$status" -eq 0 ] + [[ "$output" == ".../deep/component/node_modules" ]] +} + +@test "format_purge_target_path rewrites home with tilde" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/clean/project.sh" +format_purge_target_path "$HOME/www/app/node_modules" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == "~/www/app/node_modules" ]] +} + @test "filter_nested_artifacts: removes nested node_modules" { mkdir -p "$HOME/www/project/node_modules/package/node_modules" @@ -347,14 +380,28 @@ EOF } @test "confirm_purge_cleanup accepts Enter" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/clean/project.sh" drain_pending_input() { :; } confirm_purge_cleanup 2 1024 0 <<< '' EOF - [ "$status" -eq 0 ] + [ "$status" -eq 0 ] +} + +@test "confirm_purge_cleanup shows selected paths" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/clean/project.sh" +drain_pending_input() { :; } +confirm_purge_cleanup 2 1024 0 "~/www/app/node_modules" "~/www/app/dist" <<< '' +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Selected paths:"* ]] + [[ "$output" == *"~/www/app/node_modules"* ]] + [[ "$output" == *"~/www/app/dist"* ]] } @test "confirm_purge_cleanup cancels on ESC" {