use clap::Parser;
use colored::*;
use humansize::{format_size, DECIMAL};
use rayon::prelude::*;
use std::path::PathBuf;
use std::process;
use rudu::{compute_dir_sizes_with_cache, Cache};
#[derive(Parser)]
#[command(name = "rudu", author, version, about)]
struct Cli {
#[arg(default_value = "/")]
path: PathBuf,
#[arg(short = 'n', long = "number", default_value_t = 10)]
top: usize,
#[arg(short = 'q', long = "quiet")]
quiet: bool,
#[arg(short = 'c', long = "cache")]
cache: bool,
#[arg(long = "cache-age", default_value_t = 24)]
cache_age: u64,
#[arg(long = "clear-cache")]
clear_cache: bool,
#[arg(long = "cache-stats")]
cache_stats: bool,
}
fn main() {
let cli = Cli::parse();
if cli.clear_cache {
match Cache::new() {
Ok(cache) => match cache.clear() {
Ok(()) => {
println!(
"🗑️ {} {}",
"Cache cleared successfully!".bright_green().bold(),
format!("({})", cache.cache_directory().display()).bright_blue()
);
return;
}
Err(e) => {
eprintln!("❌ {}: {}", "Failed to clear cache".bright_red().bold(), e);
process::exit(1);
}
},
Err(e) => {
eprintln!("❌ {}: {}", "Failed to access cache".bright_red().bold(), e);
process::exit(1);
}
}
}
if cli.cache_stats {
match Cache::new() {
Ok(cache) => match cache.stats() {
Ok((count, size)) => {
println!(
"📊 {} {}",
"Cache Statistics".bright_green().bold(),
format!("({})", cache.cache_directory().display()).bright_blue()
);
println!(
"📁 {}: {}",
"Cache entries".bright_cyan(),
count.to_string().bright_yellow().bold()
);
println!(
"💾 {}: {}",
"Cache size".bright_cyan(),
format_size(size, DECIMAL).bright_yellow().bold()
);
return;
}
Err(e) => {
eprintln!(
"❌ {}: {}",
"Failed to get cache stats".bright_red().bold(),
e
);
process::exit(1);
}
},
Err(e) => {
eprintln!("❌ {}: {}", "Failed to access cache".bright_red().bold(), e);
process::exit(1);
}
}
}
let base = match cli.path.canonicalize() {
Ok(p) => p,
Err(err) => {
eprintln!(
"❌ {}: failed to resolve path '{}': {}",
"Error".bright_red().bold(),
cli.path.display().to_string().bright_white(),
err.to_string().bright_red()
);
process::exit(1);
}
};
eprintln!(
"🔍 {} {}",
"Scanning directory:".bright_cyan().bold(),
base.display().to_string().bright_white()
);
let (sizes, total_files, duration) =
compute_dir_sizes_with_cache(&base, cli.quiet, cli.cache, cli.cache_age);
let mut entries: Vec<(PathBuf, u64)> = sizes.into_iter().collect();
entries.par_sort_unstable_by(|a, b| b.1.cmp(&a.1));
let base_size = entries
.iter()
.find(|(path, _)| path == &base)
.map(|(_, size)| *size)
.unwrap_or(0);
let mut display_count = 0;
for (path, bytes) in entries.into_iter() {
if path == base {
continue;
}
if display_count >= cli.top {
break;
}
let human = format_size(bytes, DECIMAL);
let display_path = if let Ok(relative) = path.strip_prefix(&base) {
let rel_str = relative.display().to_string();
if rel_str.is_empty() {
continue; } else {
rel_str
}
} else {
path.display().to_string()
};
display_count += 1;
let emoji = if bytes >= 1_000_000_000 {
"🔥"
}
else if bytes >= 100_000_000 {
"📦"
}
else if bytes >= 10_000_000 {
"📁"
}
else {
"📄"
};
let rank_color = match display_count {
1 => format!("{display_count:2}.").bright_yellow().bold(),
2 => format!("{display_count:2}.").bright_magenta().bold(),
3 => format!("{display_count:2}.").bright_cyan().bold(),
_ => format!("{display_count:2}.").bright_white(),
};
let size_color = if bytes >= 1_000_000_000 {
human.bright_red().bold()
}
else if bytes >= 100_000_000 {
human.bright_yellow().bold()
}
else if bytes >= 10_000_000 {
human.bright_green().bold()
}
else {
human.bright_blue()
};
println!(
"{} {} {:>10} {}",
emoji,
rank_color,
size_color,
display_path.bright_white()
);
}
println!(
"📊 {} {}",
"Summary of".bright_green().bold(),
base.display().to_string().bright_white().bold()
);
println!(
"💾 {}: {}",
"Total file size".bright_cyan(),
format_size(base_size, DECIMAL).bright_yellow().bold()
);
println!(
"📋 {}: {}",
"Total files".bright_cyan(),
total_files.to_string().bright_yellow().bold()
);
println!(
"⏱️ {}: {}",
"Time taken".bright_cyan(),
format!("{duration:.2?}").bright_yellow().bold()
);
}