1use cli_ui::progress::format_bytes;
11use cli_ui::styles::{paint, BOLD, CYAN, DIM, ERR, OK, YELLOW};
12use cli_ui::{bail, header, ok, phase, step, substep, summary, CliOptions, Progress};
13use std::path::PathBuf;
14
15#[derive(CliOptions)]
16#[cli(
17 name = "mytool",
18 about = "batch file processor",
19 tagline = "fast and parallel",
20 theme = "cyan",
21 example = "mytool input/ output/",
22 example = "mytool input/ output/ -j 8 --format json",
23 example = "mytool -i input/ -o output/ --include '*.csv' --dry-run",
24 hint = "all local files are copied automatically",
25 url = "https://github.com/you/mytool"
26)]
27struct Opt {
28 #[arg(positional)]
30 input: PathBuf,
31
32 #[arg(positional)]
34 output: PathBuf,
35
36 #[arg(
39 section = "Input / Output",
40 short = 'i',
41 long = "input",
42 conflicts_with = "input"
43 )]
44 alt_input: Option<PathBuf>,
45
46 #[arg(
48 section = "Input / Output",
49 short = 'o',
50 long = "output",
51 conflicts_with = "output"
52 )]
53 alt_output: Option<PathBuf>,
54
55 #[arg(section = "Processing", short = 'j', long = "jobs", default = 4)]
58 jobs: usize,
59
60 #[arg(section = "Processing", long = "format", default = "html")]
62 format: String,
63
64 #[arg(section = "Filters", long = "include", multi)]
67 include: Vec<String>,
68
69 #[arg(section = "Filters", long = "exclude", multi)]
71 exclude: Vec<String>,
72
73 #[arg(section = "Filters", long = "dry-run", negatable)]
75 dry_run: bool,
76
77 #[arg(section = "Filters", long = "keep-tmp", negatable)]
79 _keep_tmp: bool,
80}
81
82fn main() {
83 let opt = Opt::parse();
86
87 let input = opt.resolved_alt_input();
89 let output = opt.resolved_alt_output();
90 let start = std::time::Instant::now();
91
92 if !input.exists() {
94 bail!("input path does not exist: {}", input.display());
95 }
96
97 header!(
98 "mytool",
99 env!("CARGO_PKG_VERSION"),
100 "batch file processor",
101 "fast and parallel"
102 );
103
104 phase!("init", "scanning {}", input.display());
105 phase!(
106 "scan",
107 "found {} remote · {} local jobs: {}",
108 8,
109 3,
110 opt.jobs
111 );
112
113 if opt.dry_run {
114 phase!("mode", "dry run — nothing will be written");
115 }
116 if !opt.include.is_empty() {
117 phase!("filter", "include: {}", opt.include.join(", "));
118 }
119 if !opt.exclude.is_empty() {
120 phase!("filter", "exclude: {}", opt.exclude.join(", "));
121 }
122
123 let remote: &[(&str, &str, usize)] = &[
125 ("https://example.com/data.csv", "./data/data.csv", 43_100),
126 ("https://example.com/ref.json", "./data/ref.json", 8_500),
127 (
128 "https://example.com/schema.json",
129 "./data/schema.json",
130 2_300,
131 ),
132 ("https://cdn.example.com/lib.js", "./js/lib.js", 128_000),
133 (
134 "https://cdn.example.com/style.css",
135 "./css/style.css",
136 12_400,
137 ),
138 ];
139
140 step!("downloading remote files");
141 let pb = Progress::new(remote.len());
142 let mut errors = 0usize;
143 for (url, local, bytes) in remote {
144 if url.contains("lib.js") {
146 pb.fail("file", url, "connection timeout");
147 errors += 1;
148 } else {
149 pb.ok("file", "remote", url, local, *bytes);
150 }
151 }
152 pb.finish();
153
154 let local: &[(&str, &str, usize)] = &[
156 ("config.toml", "./cfg/config.toml", 1_200),
157 ("rules.yaml", "./cfg/rules.yaml", 3_400),
158 ("README.md", "./README.md", 8_800),
159 ];
160
161 step!("copying local files");
162 let pb2 = Progress::new(local.len());
163 for (src, dst, bytes) in local {
164 pb2.ok("file", "local", src, dst, *bytes);
165 }
166 pb2.finish();
167
168 step!("resolving references in style.css");
170 substep!(
171 "https://fonts.example.com/Inter.woff2",
172 "./fonts/Inter.woff2"
173 );
174 substep!(
175 "https://fonts.example.com/JetBrains.woff2",
176 "./fonts/JetBrains.woff2"
177 );
178
179 if !opt.dry_run {
181 step!("writing output");
182 ok!(output.join(format!("result.{}", opt.format)).display());
183 }
184
185 let remote_ok = remote.len() - errors;
187 let total_files = remote_ok + local.len();
188 let total_bytes: usize = remote
189 .iter()
190 .filter(|(u, _, _)| !u.contains("lib.js"))
191 .map(|(_, _, b)| b)
192 .chain(local.iter().map(|(_, _, b)| b))
193 .sum();
194
195 let input_val = paint(CYAN, &input.display().to_string());
196 let output_val = paint(
197 BOLD,
198 &output
199 .join(format!("result.{}", opt.format))
200 .display()
201 .to_string(),
202 );
203 let files_val = format!(
204 "{} total · {} errors",
205 paint(OK, &total_files.to_string()),
206 paint(if errors > 0 { ERR } else { DIM }, &errors.to_string())
207 );
208 let size_val = paint(YELLOW, &format_bytes(total_bytes));
209 let time_val = paint(DIM, &format!("{}ms", start.elapsed().as_millis()));
210
211 if errors == 0 {
212 summary! {
213 done: "All files processed",
214 "input" => input_val,
215 "output" => output_val,
216 section,
217 "files" => files_val,
218 "size" => size_val,
219 "time" => time_val,
220 }
221 } else {
222 summary! {
223 warn: "Completed with errors",
224 "input" => input_val,
225 "output" => output_val,
226 section,
227 "files" => files_val,
228 "size" => size_val,
229 "time" => time_val,
230 }
231 }
232}