From 5b51007c3fc4b2fcee60cd1ff5b969fd5f3fac1c Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 13 Jan 2026 14:16:41 +0800 Subject: [PATCH] Fix mo purge UI glitch by correctly clearing the scanning progress line without overwriting the title. --- bin/purge.sh | 23 +++++-- lib/clean/project.sh | 145 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 134 insertions(+), 34 deletions(-) diff --git a/bin/purge.sh b/bin/purge.sh index 96bb788..62a9ecf 100755 --- a/bin/purge.sh +++ b/bin/purge.sh @@ -98,7 +98,12 @@ perform_purge() { # Function to truncate path in the middle truncate_path() { local path="$1" - local max_len=80 + local term_width=$(tput cols 2>/dev/null || echo 80) + # Reserve space: "| Scanning " = 12 chars, spinner = 2 chars, margins = 4 chars + local max_len=$((term_width - 18)) + + # Minimum length to avoid too short + [[ $max_len -lt 40 ]] && max_len=40 if [[ ${#path} -le $max_len ]]; then echo "$path" @@ -128,17 +133,23 @@ perform_purge() { local spin_char="${spinner_chars:$spinner_idx:1}" spinner_idx=$(((spinner_idx + 1) % ${#spinner_chars})) - # Show title on first line, spinner and scanning info on second line + # Clear previous output and redraw + # Important: Must move up THEN to start of line to avoid column offset if [[ -n "$display_path" ]]; then - printf '\r%s\n%s %sScanning %s\033[K\033[A' \ - "${PURPLE_BOLD}Purge Project Artifacts${NC}" \ + # Line 1: Move to start, clear, print title + printf '\r\033[K%s\n' "${PURPLE_BOLD}Purge Project Artifacts${NC}" + # Line 2: Move to start, clear, print scanning info + printf '\r\033[K%s %sScanning %s' \ "${BLUE}${spin_char}${NC}" \ "${GRAY}" "$display_path" + # Move up THEN to start (important order!) + printf '\033[A\r' else - printf '\r%s\n%s %sScanning...\033[K\033[A' \ - "${PURPLE_BOLD}Purge Project Artifacts${NC}" \ + printf '\r\033[K%s\n' "${PURPLE_BOLD}Purge Project Artifacts${NC}" + printf '\r\033[K%s %sScanning...' \ "${BLUE}${spin_char}${NC}" \ "${GRAY}" + printf '\033[A\r' fi sleep 0.05 diff --git a/lib/clean/project.sh b/lib/clean/project.sh index 15908bd..14adde7 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -65,6 +65,14 @@ readonly PURGE_CONFIG_FILE="$HOME/.config/mole/purge_paths" PURGE_SEARCH_PATHS=() # Project indicators for container detection. +# Monorepo indicators (higher priority) +readonly MONOREPO_INDICATORS=( + "lerna.json" + "pnpm-workspace.yaml" + "nx.json" + "rush.json" +) + readonly PROJECT_INDICATORS=( "package.json" "Cargo.toml" @@ -348,7 +356,7 @@ scan_purge_targets() { # Escape regex special characters in target names for fd patterns local escaped_targets=() for target in "${PURGE_TARGETS[@]}"; do - escaped_targets+=("$(printf '%s' "$target" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g')") + escaped_targets+=("^$(printf '%s' "$target" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g')\$") done local pattern="($( IFS='|' @@ -764,6 +772,18 @@ clean_project_artifacts() { for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do wait "$pid" 2> /dev/null || true done + + # Stop the scanning monitor (removes purge_scanning file to signal completion) + local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole" + rm -f "$stats_dir/purge_scanning" 2> /dev/null || true + + # Give monitor process time to exit and clear its output + if [[ -t 1 ]]; then + sleep 0.2 + # Clear the scanning line but preserve the title + printf '\n\033[K' + fi + # Collect all results for scan_output in "${scan_temps[@]+"${scan_temps[@]}"}"; do if [[ -f "$scan_output" ]]; then @@ -805,44 +825,107 @@ clean_project_artifacts() { # Strategy: Find the nearest ancestor directory containing a project indicator file get_project_name() { local path="$1" - local artifact_name - artifact_name=$(basename "$path") - # Start from the parent of the artifact and walk up local current_dir current_dir=$(dirname "$path") + local monorepo_root="" + local project_root="" + # Single pass: check both monorepo and project indicators while [[ "$current_dir" != "/" && "$current_dir" != "$HOME" && -n "$current_dir" ]]; do - # Check if current directory contains any project indicator - for indicator in "${PROJECT_INDICATORS[@]}"; do - if [[ -e "$current_dir/$indicator" ]]; then - # Found a project root, return its name - basename "$current_dir" - return 0 - fi - done - # Move up one level + # First check for monorepo indicators (higher priority) + if [[ -z "$monorepo_root" ]]; then + for indicator in "${MONOREPO_INDICATORS[@]}"; do + if [[ -e "$current_dir/$indicator" ]]; then + monorepo_root="$current_dir" + break + fi + done + fi + + # Then check for project indicators (save first match) + if [[ -z "$project_root" ]]; then + for indicator in "${PROJECT_INDICATORS[@]}"; do + if [[ -e "$current_dir/$indicator" ]]; then + project_root="$current_dir" + break + fi + done + fi + + # If we found monorepo, we can stop (monorepo always wins) + if [[ -n "$monorepo_root" ]]; then + break + fi + + # If we found project but still checking for monorepo above + # (only stop if we're beyond reasonable depth) + local depth=$(echo "${current_dir#$HOME}" | LC_ALL=C tr -cd '/' | wc -c | tr -d ' ') + if [[ -n "$project_root" && $depth -lt 2 ]]; then + break + fi + current_dir=$(dirname "$current_dir") done - # Fallback: try the old logic (first directory under search root) - local search_roots=() - if [[ ${#PURGE_SEARCH_PATHS[@]} -gt 0 ]]; then - search_roots=("${PURGE_SEARCH_PATHS[@]}") + # Determine result: monorepo > project > fallback + local result="" + if [[ -n "$monorepo_root" ]]; then + result=$(basename "$monorepo_root") + elif [[ -n "$project_root" ]]; then + result=$(basename "$project_root") else - search_roots=("$HOME/www" "$HOME/dev" "$HOME/Projects") + # Fallback: first directory under search root + local search_roots=() + if [[ ${#PURGE_SEARCH_PATHS[@]} -gt 0 ]]; then + search_roots=("${PURGE_SEARCH_PATHS[@]}") + else + search_roots=("$HOME/www" "$HOME/dev" "$HOME/Projects") + fi + for root in "${search_roots[@]}"; do + root="${root%/}" + if [[ -n "$root" && "$path" == "$root/"* ]]; then + local relative_path="${path#"$root"/}" + result=$(echo "$relative_path" | cut -d'/' -f1) + break + fi + done + + # Final fallback: use grandparent directory + if [[ -z "$result" ]]; then + result=$(dirname "$(dirname "$path")" | xargs basename) + fi fi - for root in "${search_roots[@]}"; do - root="${root%/}" - if [[ -n "$root" && "$path" == "$root/"* ]]; then - local relative_path="${path#"$root"/}" - echo "$relative_path" | cut -d'/' -f1 - return 0 + + echo "$result" + } + + # Helper to get artifact display name + # For duplicate artifact names within same project, include parent directory for context + get_artifact_display_name() { + local path="$1" + local artifact_name=$(basename "$path") + local project_name=$(get_project_name "$path") + local parent_name=$(basename "$(dirname "$path")") + + # Check if there are other items with same artifact name AND same project + local has_duplicate=false + for other_item in "${safe_to_clean[@]}"; do + if [[ "$other_item" != "$path" && "$(basename "$other_item")" == "$artifact_name" ]]; then + # Same artifact name, check if same project + if [[ "$(get_project_name "$other_item")" == "$project_name" ]]; then + has_duplicate=true + break + fi fi done - # Final fallback: use grandparent directory - dirname "$(dirname "$path")" | xargs basename + # If duplicate exists in same project and parent is not the project itself, show parent/artifact + if [[ "$has_duplicate" == "true" && "$parent_name" != "$project_name" && "$parent_name" != "." && "$parent_name" != "/" ]]; then + echo "$parent_name/$artifact_name" + else + echo "$artifact_name" + fi } # Format display with alignment (like app_selector) format_purge_display() { @@ -851,7 +934,7 @@ clean_project_artifacts() { local size_str="$3" # Terminal width for alignment local terminal_width=$(tput cols 2> /dev/null || echo 80) - local fixed_width=28 # Reserve for type and size + local fixed_width=38 # Reserve for type, size, and potential "| Recent" (28 + 10) local available_width=$((terminal_width - fixed_width)) # Bounds: 24-35 chars for project name [[ $available_width -lt 24 ]] && available_width=24 @@ -868,8 +951,14 @@ clean_project_artifacts() { # Build menu options - one line per artifact for item in "${safe_to_clean[@]}"; do local project_name=$(get_project_name "$item") - local artifact_type=$(basename "$item") + local artifact_type=$(get_artifact_display_name "$item") local size_kb=$(get_dir_size_kb "$item") + + # Skip empty directories (0 bytes) + if [[ $size_kb -eq 0 ]]; then + continue + fi + local size_human=$(bytes_to_human "$((size_kb * 1024))") # Check if recent local is_recent=false