use std::{
ffi::OsStr,
io::{IsTerminal, Read},
path::PathBuf,
};
use anyhow::anyhow;
use ignore::{WalkBuilder, WalkState};
use oxvg_ast::{node::Ref, serialize::Node as _, xmlwriter::Options};
type FnVisitor = Box<dyn FnMut(&str, Option<&PathBuf>, Option<&PathBuf>) + Send>;
#[derive(clap::Args, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct Walk {
#[clap(value_parser)]
pub paths: Vec<PathBuf>,
#[clap(long, short, num_args(0..=1))]
pub output: Option<Vec<PathBuf>>,
#[clap(long, short, default_value = "false")]
pub recursive: bool,
#[clap(long, short = '.', default_value = "false")]
pub hidden: bool,
#[clap(long, default_value = "false")]
pub no_ignore: bool,
#[clap(long, short, default_value = "0")]
pub threads: usize,
#[clap(long, default_value = "false")]
pub quiet: bool,
}
pub(crate) struct Output<'a, 'input, 'arena> {
pub options: Options,
pub dom: Ref<'input, 'arena>,
pub input: Option<&'a PathBuf>,
pub destination: Option<&'a PathBuf>,
pub input_bytes: f64,
pub quiet: bool,
}
impl Output<'_, '_, '_> {
#[allow(clippy::unnecessary_debug_formatting)]
#[allow(clippy::cast_precision_loss)]
pub fn output(self) -> anyhow::Result<()> {
let is_stdin = self.input.is_none();
if let Some(output) = self.destination {
if is_stdin && output.metadata().is_ok_and(|f| f.is_dir()) {
eprintln!("Cannot use dir as output with stdin. Printing result to stdout instead");
self.dom.serialize_into(std::io::stdout(), self.options)?;
} else {
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(output)?;
let writer = std::io::BufWriter::new(file);
self.dom.serialize_into(writer, self.options)?;
let input_kb = self.input_bytes / 1000.0;
let output_kb = output.metadata()?.len() as f64 / 1000.0;
let change = 100.0 * (input_kb - output_kb) / input_kb;
let increased = if change < 0.0 { "\x1b[31m" } else { "" };
let path = self.input.and_then(|p| p.to_str()).unwrap_or("");
if !self.quiet {
println!(
"\n\n\x1b[32m{path:?} ({input_kb:.1}KB) -> {output:?} ({output_kb:.1}KB) {increased}({change:.2}%)\x1b[0m"
);
}
}
} else {
self.dom.serialize_into(std::io::stdout(), self.options)?;
}
Ok(())
}
}
impl Walk {
pub fn run<F: Fn() -> FnVisitor>(&self, f: F) -> anyhow::Result<()> {
if !std::io::stdin().is_terminal()
&& self.paths.len() <= 1
&& self
.paths
.first()
.is_none_or(|path| path.as_os_str() == OsStr::new("."))
{
return self.handle_stdin(f());
}
if self.paths.is_empty() {
return Err(anyhow!(
"This command requires at least one path to optimise"
));
}
for path in &self.paths {
self.handle_path(path, &f);
}
Ok(())
}
fn handle_stdin(&self, mut f: FnVisitor) -> anyhow::Result<()> {
let mut source = String::new();
std::io::stdin().read_to_string(&mut source)?;
f(
&source,
None,
self.output.as_ref().and_then(|output| output.first()),
);
Ok(())
}
fn handle_path<F: Fn() -> FnVisitor>(&self, path: &PathBuf, f: F) {
let output_path = |input: &PathBuf| {
let Some(output) = self.output.as_ref().and_then(|output| output.first()) else {
return Ok(None);
};
input.strip_prefix(path).map(|p| {
Some(if p.as_os_str().is_empty() {
output.clone()
} else {
output.join(p)
})
})
};
WalkBuilder::new(path)
.max_depth(if self.recursive { None } else { Some(1) })
.hidden(!self.hidden)
.git_ignore(!self.no_ignore)
.ignore(!self.no_ignore)
.follow_links(true)
.threads(self.threads)
.build_parallel()
.run(|| {
let mut visitor = f();
Box::new(move |path| {
let Ok(path) = path else {
return WalkState::Continue;
};
if path.file_type().is_none_or(|f| !f.is_file()) {
return WalkState::Continue;
}
let path = path.into_path();
if path.extension().and_then(OsStr::to_str) != Some("svg") {
return WalkState::Continue;
}
let Ok(output_path) = output_path(&path) else {
return WalkState::Continue;
};
let Ok(file) = std::fs::read_to_string(path.clone()) else {
return WalkState::Continue;
};
visitor(&file, Some(&path), output_path.as_ref());
WalkState::Continue
})
});
}
}