use cli_ui::progress::format_bytes;
use cli_ui::styles::{paint, BOLD, CYAN, DIM, ERR, OK, YELLOW};
use cli_ui::{bail, header, ok, phase, step, substep, summary, CliOptions, Progress};
use std::path::PathBuf;
#[derive(CliOptions)]
#[cli(
name = "mytool",
about = "batch file processor",
tagline = "fast and parallel",
theme = "cyan",
example = "mytool input/ output/",
example = "mytool input/ output/ -j 8 --format json",
example = "mytool -i input/ -o output/ --include '*.csv' --dry-run",
hint = "all local files are copied automatically",
url = "https://github.com/you/mytool"
)]
struct Opt {
#[arg(positional)]
input: PathBuf,
#[arg(positional)]
output: PathBuf,
#[arg(
section = "Input / Output",
short = 'i',
long = "input",
conflicts_with = "input"
)]
alt_input: Option<PathBuf>,
#[arg(
section = "Input / Output",
short = 'o',
long = "output",
conflicts_with = "output"
)]
alt_output: Option<PathBuf>,
#[arg(section = "Processing", short = 'j', long = "jobs", default = 4)]
jobs: usize,
#[arg(section = "Processing", long = "format", default = "html")]
format: String,
#[arg(section = "Filters", long = "include", multi)]
include: Vec<String>,
#[arg(section = "Filters", long = "exclude", multi)]
exclude: Vec<String>,
#[arg(section = "Filters", long = "dry-run", negatable)]
dry_run: bool,
#[arg(section = "Filters", long = "keep-tmp", negatable)]
_keep_tmp: bool,
}
fn main() {
let opt = Opt::parse();
let input = opt.resolved_alt_input();
let output = opt.resolved_alt_output();
let start = std::time::Instant::now();
if !input.exists() {
bail!("input path does not exist: {}", input.display());
}
header!(
"mytool",
env!("CARGO_PKG_VERSION"),
"batch file processor",
"fast and parallel"
);
phase!("init", "scanning {}", input.display());
phase!(
"scan",
"found {} remote · {} local jobs: {}",
8,
3,
opt.jobs
);
if opt.dry_run {
phase!("mode", "dry run — nothing will be written");
}
if !opt.include.is_empty() {
phase!("filter", "include: {}", opt.include.join(", "));
}
if !opt.exclude.is_empty() {
phase!("filter", "exclude: {}", opt.exclude.join(", "));
}
let remote: &[(&str, &str, usize)] = &[
("https://example.com/data.csv", "./data/data.csv", 43_100),
("https://example.com/ref.json", "./data/ref.json", 8_500),
(
"https://example.com/schema.json",
"./data/schema.json",
2_300,
),
("https://cdn.example.com/lib.js", "./js/lib.js", 128_000),
(
"https://cdn.example.com/style.css",
"./css/style.css",
12_400,
),
];
step!("downloading remote files");
let pb = Progress::new(remote.len());
let mut errors = 0usize;
for (url, local, bytes) in remote {
if url.contains("lib.js") {
pb.fail("file", url, "connection timeout");
errors += 1;
} else {
pb.ok("file", "remote", url, local, *bytes);
}
}
pb.finish();
let local: &[(&str, &str, usize)] = &[
("config.toml", "./cfg/config.toml", 1_200),
("rules.yaml", "./cfg/rules.yaml", 3_400),
("README.md", "./README.md", 8_800),
];
step!("copying local files");
let pb2 = Progress::new(local.len());
for (src, dst, bytes) in local {
pb2.ok("file", "local", src, dst, *bytes);
}
pb2.finish();
step!("resolving references in style.css");
substep!(
"https://fonts.example.com/Inter.woff2",
"./fonts/Inter.woff2"
);
substep!(
"https://fonts.example.com/JetBrains.woff2",
"./fonts/JetBrains.woff2"
);
if !opt.dry_run {
step!("writing output");
ok!(output.join(format!("result.{}", opt.format)).display());
}
let remote_ok = remote.len() - errors;
let total_files = remote_ok + local.len();
let total_bytes: usize = remote
.iter()
.filter(|(u, _, _)| !u.contains("lib.js"))
.map(|(_, _, b)| b)
.chain(local.iter().map(|(_, _, b)| b))
.sum();
let input_val = paint(CYAN, &input.display().to_string());
let output_val = paint(
BOLD,
&output
.join(format!("result.{}", opt.format))
.display()
.to_string(),
);
let files_val = format!(
"{} total · {} errors",
paint(OK, &total_files.to_string()),
paint(if errors > 0 { ERR } else { DIM }, &errors.to_string())
);
let size_val = paint(YELLOW, &format_bytes(total_bytes));
let time_val = paint(DIM, &format!("{}ms", start.elapsed().as_millis()));
if errors == 0 {
summary! {
done: "All files processed",
"input" => input_val,
"output" => output_val,
section,
"files" => files_val,
"size" => size_val,
"time" => time_val,
}
} else {
summary! {
warn: "Completed with errors",
"input" => input_val,
"output" => output_val,
section,
"files" => files_val,
"size" => size_val,
"time" => time_val,
}
}
}