#![allow(dead_code)]
use std::collections::HashSet;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
use clap::{ArgMatches, Command};
use console::Style;
use indicatif::{ProgressBar, ProgressStyle};
pub const AFTER_HELP: &str =
"Part of oxyROMon, the Rusty ROM OrgaNizer: https://github.com/alucryd/oxyromon";
pub fn run(
command: Command,
mut convert: impl FnMut(&Path, &ArgMatches, &mut Outputs) -> Result<String, String>,
) -> ExitCode {
let matches = command.get_matches();
let inputs: Vec<&PathBuf> = matches.get_many::<PathBuf>("INPUTS").unwrap().collect();
let mut outputs = Outputs::new(&inputs);
let mut failed = false;
for input in inputs {
match convert(input, &matches, &mut outputs) {
Ok(message) => success(message),
Err(message) => {
error(format!("{}: {message}", input.display()));
failed = true;
}
}
}
if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
pub struct Outputs {
inputs: HashSet<PathBuf>,
written: HashSet<PathBuf>,
}
impl Outputs {
fn new(inputs: &[&PathBuf]) -> Outputs {
Outputs {
inputs: inputs
.iter()
.filter_map(|input| std::path::absolute(input).ok())
.collect(),
written: HashSet::new(),
}
}
pub fn claim(&mut self, path: PathBuf) -> Result<PathBuf, String> {
let absolute = std::path::absolute(&path).map_err(|e| e.to_string())?;
if self.inputs.contains(&absolute) {
return Err(format!(
"would overwrite \"{}\", one of the inputs",
path.display()
));
}
if !self.written.insert(absolute) {
return Err(format!(
"would overwrite \"{}\", written for an earlier input",
path.display()
));
}
Ok(path)
}
}
pub fn output_dir(input: &Path, matches: &ArgMatches) -> Result<PathBuf, String> {
let dir = match matches.get_one::<PathBuf>("output") {
Some(output) => output.clone(),
None => input.parent().unwrap_or(Path::new("")).to_path_buf(),
};
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
Ok(dir)
}
pub fn output_path(dir: &Path, input: &Path, extension: &str) -> PathBuf {
let stem = input.file_stem().unwrap_or_default().to_string_lossy();
dir.join(format!("{stem}.{extension}"))
}
pub fn extension(input: &Path) -> String {
input
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase()
}
pub fn progress_bar(length: u64, verb: &str, input: &Path) -> ProgressBar {
let bar = ProgressBar::new(length).with_style(
ProgressStyle::with_template(
" {spinner:.cyan} {wide_msg}\n {bytes}/{total_bytes} [{bar:40.cyan/dim}] {bytes_per_sec} {elapsed_precise} (ETA {eta_precise})",
)
.expect("a valid template")
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
.progress_chars("━╸━"),
);
let name = input.file_name().unwrap_or_default().to_string_lossy();
bar.set_message(format!("{verb} \"{name}\""));
bar.enable_steady_tick(Duration::from_millis(100));
bar
}
pub fn wrote(path: &Path) -> String {
format!("Wrote \"{}\"", path.display())
}
fn success(message: impl Display) {
println!(" {} {message}", Style::new().green().apply_to("✔"));
}
fn error(message: impl Display) {
eprintln!(" {} {message}", Style::new().red().bold().apply_to("✖"));
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Arg;
fn matches(argv: &[&str]) -> ArgMatches {
Command::new("tool")
.arg(
Arg::new("output")
.short('o')
.value_parser(clap::value_parser!(PathBuf)),
)
.try_get_matches_from(argv)
.unwrap()
}
#[test]
fn output_names_keep_their_dots() {
let path = output_path(Path::new("out"), Path::new("in/Game (v1.02).iso"), "cso");
assert_eq!(path, Path::new("out/Game (v1.02).cso"));
}
#[test]
fn output_goes_next_to_the_input_by_default() {
let dir = tempfile::tempdir().unwrap();
let input = dir.path().join("game.iso");
assert_eq!(output_dir(&input, &matches(&["tool"])).unwrap(), dir.path());
assert_eq!(
output_dir(Path::new("game.iso"), &matches(&["tool"])).unwrap(),
Path::new("")
);
}
#[test]
fn no_output_is_written_twice_nor_over_an_input() {
let (a, b) = (PathBuf::from("a/game.iso"), PathBuf::from("game.cso"));
let mut outputs = Outputs::new(&[&a, &b]);
outputs.claim(PathBuf::from("out/game.cso")).unwrap();
assert!(outputs.claim(PathBuf::from("./out/game.cso")).is_err());
assert!(outputs.claim(PathBuf::from("./game.cso")).is_err());
assert!(outputs.claim(PathBuf::from("a/game.cso")).is_ok());
}
}