1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 21:55:08 +00:00
Files
Mole/bin/purge.sh
tw93 837df390a5 fix(purge): rewrite spinner for /dev/tty reliability and fix cursor position
- Capture terminal width in parent before forking; avoids unreliable
  tput calls inside the background subshell
- Write spinner output directly to /dev/tty to prevent stdout state
  contention between parent and background processes
- Use \033[2K (erase entire line) instead of \033[K (erase to EOL)
- Add handle_interrupt() so Ctrl-C cleanly stops spinner, restores
  cursor, and exits 130
- cleanup_monitor now writes \r\033[2K\n so subsequent output starts
  on a clean line rather than on the cleared spinner line
2026-02-26 19:42:34 +08:00

280 lines
8.5 KiB
Bash
Executable File

#!/bin/bash
# Mole - Purge command.
# Cleans heavy project build artifacts.
# Interactive selection by project.
set -euo pipefail
# Fix locale issues (avoid Perl warnings on non-English systems)
export LC_ALL=C
export LANG=C
# Get script directory and source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/core/common.sh"
# Set up cleanup trap for temporary files
trap cleanup_temp_files EXIT INT TERM
source "$SCRIPT_DIR/../lib/core/log.sh"
source "$SCRIPT_DIR/../lib/clean/project.sh"
# Configuration
CURRENT_SECTION=""
# Section management
start_section() {
local section_name="$1"
CURRENT_SECTION="$section_name"
printf '\n'
echo -e "${BLUE}━━━ ${section_name} ━━━${NC}"
}
end_section() {
CURRENT_SECTION=""
}
# Note activity for export list
note_activity() {
if [[ -n "$CURRENT_SECTION" ]]; then
printf '%s\n' "$CURRENT_SECTION" >> "$EXPORT_LIST_FILE"
fi
}
# Main purge function
start_purge() {
# Set current command for operation logging
export MOLE_CURRENT_COMMAND="purge"
log_operation_session_start "purge"
# Clear screen for better UX
if [[ -t 1 ]]; then
printf '\033[2J\033[H'
fi
# Initialize stats file in user cache directory
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
ensure_user_dir "$stats_dir"
ensure_user_file "$stats_dir/purge_stats"
ensure_user_file "$stats_dir/purge_count"
ensure_user_file "$stats_dir/purge_scanning"
echo "0" > "$stats_dir/purge_stats"
echo "0" > "$stats_dir/purge_count"
echo "" > "$stats_dir/purge_scanning"
}
# Perform the purge
perform_purge() {
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
local monitor_pid=""
# Cleanup function - use flag to prevent duplicate execution
_cleanup_done=false
cleanup_monitor() {
# Prevent multiple cleanup executions from trap conflicts
[[ "$_cleanup_done" == "true" ]] && return
_cleanup_done=true
# Remove scanning file to stop monitor
rm -f "$stats_dir/purge_scanning" 2> /dev/null || true
if [[ -n "$monitor_pid" ]]; then
kill "$monitor_pid" 2> /dev/null || true
wait "$monitor_pid" 2> /dev/null || true
fi
if [[ -t 1 ]]; then
printf '\r\033[2K\n' > /dev/tty 2> /dev/null || true
fi
}
# Ensure Ctrl-C/TERM always stops spinner(s) and exits immediately.
handle_interrupt() {
cleanup_monitor
stop_inline_spinner 2> /dev/null || true
show_cursor 2> /dev/null || true
printf '\n' >&2
exit 130
}
# Set up trap for cleanup + abort
trap handle_interrupt INT TERM
# Show scanning with spinner below the title line
if [[ -t 1 ]]; then
# Print title ONCE with newline; spinner occupies the line below
printf '%s\n' "${PURPLE_BOLD}Purge Project Artifacts${NC}"
# Capture terminal width in parent (most reliable before forking)
local _parent_cols=80
local _stty_out
if _stty_out=$(stty size < /dev/tty 2> /dev/null); then
_parent_cols="${_stty_out##* }" # "rows cols" -> take cols
else
_parent_cols=$(tput cols 2> /dev/null || echo 80)
fi
[[ "$_parent_cols" =~ ^[0-9]+$ && $_parent_cols -gt 0 ]] || _parent_cols=80
# Start background monitor: writes directly to /dev/tty to avoid stdout state issues
(
local spinner_chars="|/-\\"
local spinner_idx=0
local last_path=""
# Use parent-captured width; never refresh inside the loop (avoids unreliable tput in bg)
local term_cols="$_parent_cols"
# Visible prefix "| Scanning " = 11 chars; reserve 25 total for safety margin
local max_path_len=$((term_cols - 25))
((max_path_len < 5)) && max_path_len=5
# 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")
last_path="$display_path"
fi
local spin_char="${spinner_chars:$spinner_idx:1}"
spinner_idx=$(((spinner_idx + 1) % ${#spinner_chars}))
# Write directly to /dev/tty: \033[2K clears entire current line, \r goes to start
if [[ -n "$last_path" ]]; then
printf '\r\033[2K%s %sScanning %s%s' \
"${BLUE}${spin_char}${NC}" \
"${GRAY}" "$last_path" "${NC}" > /dev/tty 2> /dev/null
else
printf '\r\033[2K%s %sScanning...%s' \
"${BLUE}${spin_char}${NC}" \
"${GRAY}" "${NC}" > /dev/tty 2> /dev/null
fi
sleep 0.05
done
printf '\r\033[2K' > /dev/tty 2> /dev/null
exit 0
) &
monitor_pid=$!
else
echo -e "${PURPLE_BOLD}Purge Project Artifacts${NC}"
fi
clean_project_artifacts
local exit_code=$?
# Clean up
trap - INT TERM
cleanup_monitor
# Exit codes:
# 0 = success, show summary
# 1 = user cancelled
# 2 = nothing to clean
if [[ $exit_code -ne 0 ]]; then
return 0
fi
# Final summary (matching clean.sh format)
echo ""
local summary_heading="Purge complete"
local -a summary_details=()
local total_size_cleaned=0
local total_items_cleaned=0
if [[ -f "$stats_dir/purge_stats" ]]; then
total_size_cleaned=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
rm -f "$stats_dir/purge_stats"
fi
if [[ -f "$stats_dir/purge_count" ]]; then
total_items_cleaned=$(cat "$stats_dir/purge_count" 2> /dev/null || echo "0")
rm -f "$stats_dir/purge_count"
fi
if [[ $total_size_cleaned -gt 0 ]]; then
local freed_gb
freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}')
local summary_line="Space freed: ${GREEN}${freed_gb}GB${NC}"
[[ $total_items_cleaned -gt 0 ]] && summary_line+=" | Items: $total_items_cleaned"
summary_line+=" | Free: $(get_free_space)"
summary_details+=("$summary_line")
else
summary_details+=("No old project artifacts to clean.")
summary_details+=("Free space: $(get_free_space)")
fi
# Log session end
log_operation_session_end "purge" "${total_items_cleaned:-0}" "${total_size_cleaned:-0}"
print_summary_block "$summary_heading" "${summary_details[@]}"
printf '\n'
}
# Show help message
show_help() {
echo -e "${PURPLE_BOLD}Mole Purge${NC}, Clean old project build artifacts"
echo ""
echo -e "${YELLOW}Usage:${NC} mo purge [options]"
echo ""
echo -e "${YELLOW}Options:${NC}"
echo " --paths Edit custom scan directories"
echo " --debug Enable debug logging"
echo " --help Show this help message"
echo ""
echo -e "${YELLOW}Default Paths:${NC}"
for path in "${DEFAULT_PURGE_SEARCH_PATHS[@]}"; do
echo " * $path"
done
}
# Main entry point
main() {
# Set up signal handling
trap 'show_cursor; exit 130' INT TERM
# Parse arguments
for arg in "$@"; do
case "$arg" in
"--paths")
source "$SCRIPT_DIR/../lib/manage/purge_paths.sh"
manage_purge_paths
exit 0
;;
"--help")
show_help
exit 0
;;
"--debug")
export MO_DEBUG=1
;;
*)
echo "Unknown option: $arg"
echo "Use 'mo purge --help' for usage information"
exit 1
;;
esac
done
start_purge
hide_cursor
perform_purge
show_cursor
}
main "$@"