use crate::commands::Refine;
use crate::entries::{Entry, Fetcher, InputInfo, Recurse, TraversalMode};
use crate::utils::{display_abort, natural_cmp};
use anyhow::Result;
use clap::{Args, ValueEnum};
use human_repr::HumanCount;
use std::cmp::Ordering;
use std::sync::OnceLock;
use yansi::{Color, Paint};
#[derive(Debug, Args)]
pub struct List {
#[arg(short = 'b', long, default_value_t = By::Size, value_name = "STR", value_enum)]
by: By,
#[arg(short = 'r', long)]
rev: bool,
#[arg(short = 'p', long)]
paths: bool,
#[arg(short = 'c', long)]
no_calc_dirs: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, ValueEnum)]
pub enum By {
#[value(alias = "s")]
Size,
#[value(alias = "c")]
Count,
#[value(alias = "n")]
Name,
#[value(alias = "p")]
Path,
}
#[derive(Debug)]
pub struct Media {
entry: Entry,
size_count: Option<(u64, u32)>,
}
const ORDERING: &[(By, bool)] = &[
(By::Size, true),
(By::Count, true),
(By::Name, false),
(By::Path, false),
];
static CALC_DIR_SIZES: OnceLock<bool> = OnceLock::new();
impl Refine for List {
type Media = Media;
const OPENING_LINE: &'static str = "List files";
const T_MODE: TraversalMode = TraversalMode::ContentOverDirs;
fn tweak(&mut self, info: &InputInfo) {
self.rev ^= ORDERING.iter().find(|(b, _)| *b == self.by).unwrap().1;
if self.by == By::Path && !self.paths {
self.paths = true;
eprintln!("Enabling file paths due to sorting by paths.\n");
}
if info.num_valid > 1 && !self.paths {
self.paths = true;
eprintln!("Enabling file paths due to multiple input paths.\n");
}
CALC_DIR_SIZES.set(!self.no_calc_dirs).unwrap();
}
fn refine(&self, mut medias: Vec<Self::Media>) -> Result<()> {
let compare: fn(&Media, &Media) -> _ = match self.by {
By::Size => |m, n| {
m.size_count
.map(|(s, _)| s)
.cmp(&n.size_count.map(|(s, _)| s))
},
By::Count => |m, n| {
m.size_count
.map(|(_, c)| c)
.cmp(&n.size_count.map(|(_, c)| c))
},
By::Name => |m, n| natural_cmp(m.entry.file_name(), n.entry.file_name()),
By::Path => |_, _| Ordering::Equal, };
let compare: &dyn Fn(&_, &_) -> _ = match self.rev {
false => &compare,
true => &|m, n| compare(m, n).reverse(),
};
medias.sort_unstable_by(|m, n| {
compare(m, n).then_with(|| natural_cmp(m.entry.to_str(), n.entry.to_str())) });
medias.iter().for_each(|m| {
let (size, count) = match m.size_count {
Some((s, c)) => (&*format!("{}", s.human_count_bytes()), &*format!("{c}")),
None => ("?", "?"),
};
match self.paths {
true => print!("{size:>8} {}", m.entry.display_path()),
false => print!("{size:>8} {}", m.entry.display_filename()),
};
if m.entry.is_dir() && m.size_count.is_some() {
print!(" {} files", count.paint(Color::Blue).linger());
}
println!("{}", "".resetting());
});
if !medias.is_empty() {
println!();
}
let (mut size, mut count) = (0, 0);
medias
.iter()
.filter_map(|m| m.size_count)
.for_each(|(s, c)| {
size += s;
count += c;
});
println!("listed entries: {}{}", medias.len(), display_abort(true),);
println!(" total: {} in {count} files", size.human_count("B"),);
Ok(())
}
}
impl TryFrom<Entry> for Media {
type Error = (Entry, anyhow::Error);
fn try_from(entry: Entry) -> Result<Self, Self::Error> {
let size_count = match (entry.is_dir(), CALC_DIR_SIZES.get().unwrap()) {
(true, false) => None,
(true, true) => {
let fetcher = Fetcher::single(&entry, Recurse::Full);
let mut count = 0;
let sum = fetcher
.fetch(TraversalMode::Files)
.map(|e| {
count += 1;
e.metadata().map_or(0, |md| md.len())
})
.sum::<u64>();
Some((sum, count))
}
(false, _) => {
let size = entry.metadata().map_or(0, |md| md.len());
Some((size, 1))
}
};
Ok(Self { entry, size_count })
}
}