use anyhow::Result;
use colored::Colorize;
use humansize::{DECIMAL, format_size};
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use std::fs;
use std::sync::{Arc, Mutex};
use crate::executables;
use crate::project::{Project, Projects};
#[derive(Clone, Copy, Debug)]
pub enum RemovalStrategy {
Permanent,
Trash,
}
impl RemovalStrategy {
#[must_use]
pub const fn from_use_trash(use_trash: bool) -> Self {
if use_trash {
Self::Trash
} else {
Self::Permanent
}
}
}
#[derive(Debug)]
pub struct CleanResult {
pub success_count: usize,
pub total_freed: u64,
pub estimated_size: u64,
pub errors: Vec<String>,
}
#[derive(Debug)]
pub struct Cleaner;
impl Cleaner {
#[must_use]
pub const fn new() -> Self {
Self
}
#[must_use]
pub fn clean_projects(
projects: Projects,
keep_executables: bool,
quiet: bool,
removal_strategy: RemovalStrategy,
) -> CleanResult {
let total_projects = projects.len();
let total_size: u64 = projects.get_total_size();
let progress = if quiet {
ProgressBar::hidden()
} else {
let action = match removal_strategy {
RemovalStrategy::Permanent => "Starting cleanup...",
RemovalStrategy::Trash => "Moving to trash...",
};
println!("\n{}", action.cyan());
let pb = ProgressBar::new(total_projects as u64);
if let Ok(style) = ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
{
pb.set_style(style.progress_chars("█▉▊▋▌▍▎▏ "));
}
pb
};
let cleaned_size = Arc::new(Mutex::new(0u64));
let errors = Arc::new(Mutex::new(Vec::new()));
projects.into_par_iter().for_each(|project| {
let result = clean_single_project(&project, keep_executables, removal_strategy);
let action = match removal_strategy {
RemovalStrategy::Permanent => "Cleaned",
RemovalStrategy::Trash => "Trashed",
};
match result {
Ok(freed_size) => {
if let Ok(mut size) = cleaned_size.lock() {
*size += freed_size;
}
progress.set_message(format!(
"{action} {} ({})",
project
.root_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown"),
format_size(freed_size, DECIMAL)
));
}
Err(e) => {
if let Ok(mut errs) = errors.lock() {
errs.push(format!(
"Failed to clean {}: {e}",
project.root_path.display()
));
}
}
}
progress.inc(1);
});
let finish_msg = match removal_strategy {
RemovalStrategy::Permanent => "[OK] Cleanup complete",
RemovalStrategy::Trash => "[OK] Moved to trash",
};
progress.finish_with_message(finish_msg);
let final_cleaned_size = cleaned_size.lock().map_or(0, |s| *s);
let errors = Arc::try_unwrap(errors)
.unwrap_or_else(|arc| {
arc.lock()
.map_or_else(|_| Mutex::new(Vec::new()), |g| Mutex::new(g.clone()))
})
.into_inner()
.unwrap_or_default();
let success_count = total_projects - errors.len();
CleanResult {
success_count,
total_freed: final_cleaned_size,
estimated_size: total_size,
errors,
}
}
pub fn print_summary(result: &CleanResult) {
if !result.errors.is_empty() {
println!("\n{}", "[!] Some errors occurred during cleanup:".yellow());
for error in &result.errors {
eprintln!(" {}", error.red());
}
}
println!("\n{}", "Cleanup Summary:".bold());
println!(
" [OK] Successfully cleaned: {} projects",
result.success_count.to_string().green()
);
if !result.errors.is_empty() {
println!(
" [FAIL] Failed to clean: {} projects",
result.errors.len().to_string().red()
);
}
println!(
" Total space freed: {}",
format_size(result.total_freed, DECIMAL)
.bright_green()
.bold()
);
if result.total_freed != result.estimated_size {
let difference = result.estimated_size.abs_diff(result.total_freed);
println!(
" Difference from estimate: {}",
format_size(difference, DECIMAL).yellow()
);
}
}
}
fn clean_single_project(
project: &Project,
keep_executables: bool,
removal_strategy: RemovalStrategy,
) -> Result<u64> {
if keep_executables {
match executables::preserve_executables(project) {
Ok(preserved) => {
if !preserved.is_empty() {
eprintln!(
" Preserved {} executable(s) from {}",
preserved.len(),
project
.root_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
);
}
}
Err(e) => {
eprintln!(
" Warning: failed to preserve executables for {}: {e}",
project.root_path.display()
);
}
}
}
let mut total_freed = 0u64;
for artifact in &project.build_arts {
let build_dir = &artifact.path;
if !build_dir.exists() {
continue;
}
total_freed += crate::utils::calculate_dir_size(build_dir);
match removal_strategy {
RemovalStrategy::Permanent => fs::remove_dir_all(build_dir)?,
RemovalStrategy::Trash => {
trash::delete(build_dir)
.map_err(|e| anyhow::anyhow!("failed to move to trash: {e}"))?;
}
}
}
Ok(total_freed)
}
impl Default for Cleaner {
fn default() -> Self {
Self::new()
}
}