1use std::path::PathBuf;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct Args {
11 pub command: Command,
13}
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum Command {
18 Run {
20 experiment_path: PathBuf,
22 seed_override: Option<u64>,
24 verbose: bool,
26 },
27 Render {
29 domain: String,
31 format: RenderFormat,
33 output: PathBuf,
35 fps: u32,
37 duration: f64,
39 seed: u64,
41 },
42 Validate {
44 experiment_path: PathBuf,
46 },
47 Verify {
49 experiment_path: PathBuf,
51 runs: usize,
53 },
54 EmcCheck {
56 experiment_path: PathBuf,
58 },
59 EmcValidate {
61 emc_path: PathBuf,
63 },
64 ListEmc,
66 Help,
68 Version,
70 Error(String),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum RenderFormat {
77 SvgFrames,
79 SvgKeyframes,
81}
82
83impl Args {
84 #[must_use]
89 pub fn parse_from<I, S>(args: I) -> Self
90 where
91 I: IntoIterator<Item = S>,
92 S: AsRef<str>,
93 {
94 let args: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
95 Self::parse_from_vec(&args)
96 }
97
98 #[must_use]
100 pub fn parse() -> Self {
101 Self::parse_from(std::env::args())
102 }
103
104 fn parse_from_vec(args: &[String]) -> Self {
106 if args.len() < 2 {
107 return Self {
108 command: Command::Help,
109 };
110 }
111
112 let command = match args[1].as_str() {
113 "run" => Self::parse_run_command(args),
114 "render" => Self::parse_render_command(args),
115 "validate" => Self::parse_validate_command(args),
116 "verify" => Self::parse_verify_command(args),
117 "emc-check" => Self::parse_emc_check_command(args),
118 "emc-validate" => Self::parse_emc_validate_command(args),
119 "list-emc" => Command::ListEmc,
120 "-h" | "--help" | "help" => Command::Help,
121 "-V" | "--version" | "version" => Command::Version,
122 unknown => Command::Error(format!("Unknown command: {unknown}")),
123 };
124
125 Self { command }
126 }
127
128 fn is_help_flag(arg: &str) -> bool {
130 arg == "--help" || arg == "-h"
131 }
132
133 fn parse_run_command(args: &[String]) -> Command {
135 if args.len() < 3 || Self::is_help_flag(&args[2]) {
136 return Command::Help;
137 }
138
139 let mut seed_override = None;
140 let mut verbose = false;
141
142 let mut i = 3;
143 while i < args.len() {
144 match args[i].as_str() {
145 "--seed" => {
146 if i + 1 < args.len() {
147 if let Ok(seed) = args[i + 1].parse() {
148 seed_override = Some(seed);
149 }
150 i += 2;
151 } else {
152 i += 1;
153 }
154 }
155 "-v" | "--verbose" => {
156 verbose = true;
157 i += 1;
158 }
159 _ => i += 1,
160 }
161 }
162
163 Command::Run {
164 experiment_path: PathBuf::from(&args[2]),
165 seed_override,
166 verbose,
167 }
168 }
169
170 fn parse_validate_command(args: &[String]) -> Command {
172 if args.len() < 3 || Self::is_help_flag(&args[2]) {
173 return Command::Help;
174 }
175
176 Command::Validate {
177 experiment_path: PathBuf::from(&args[2]),
178 }
179 }
180
181 fn parse_verify_command(args: &[String]) -> Command {
183 if args.len() < 3 || Self::is_help_flag(&args[2]) {
184 return Command::Help;
185 }
186
187 let mut runs = 3;
188 if args.len() > 3 && args[3] == "--runs" && args.len() > 4 {
189 if let Ok(n) = args[4].parse() {
190 runs = n;
191 }
192 }
193
194 Command::Verify {
195 experiment_path: PathBuf::from(&args[2]),
196 runs,
197 }
198 }
199
200 fn parse_emc_check_command(args: &[String]) -> Command {
202 if args.len() < 3 || Self::is_help_flag(&args[2]) {
203 return Command::Help;
204 }
205
206 Command::EmcCheck {
207 experiment_path: PathBuf::from(&args[2]),
208 }
209 }
210
211 fn parse_emc_validate_command(args: &[String]) -> Command {
213 if args.len() < 3 || Self::is_help_flag(&args[2]) {
214 return Command::Help;
215 }
216
217 Command::EmcValidate {
218 emc_path: PathBuf::from(&args[2]),
219 }
220 }
221
222 fn collect_flags(args: &[String], start: usize) -> std::collections::HashMap<String, String> {
224 let mut flags = std::collections::HashMap::new();
225 let mut i = start;
226 while i < args.len() {
227 if args[i].starts_with("--") && i + 1 < args.len() {
228 flags.insert(args[i].clone(), args[i + 1].clone());
229 i += 2;
230 } else {
231 i += 1;
232 }
233 }
234 flags
235 }
236
237 fn parse_render_command(args: &[String]) -> Command {
239 if args.len() >= 3 && Self::is_help_flag(&args[2]) {
240 return Command::Help;
241 }
242 let flags = Self::collect_flags(args, 2);
243
244 let domain = flags
245 .get("--domain")
246 .cloned()
247 .unwrap_or_else(|| "orbit".to_string());
248 let format = match flags.get("--format").map(String::as_str) {
249 Some("svg-frames") => RenderFormat::SvgFrames,
250 _ => RenderFormat::SvgKeyframes,
251 };
252 let output = flags
253 .get("--output")
254 .map_or_else(|| PathBuf::from("."), PathBuf::from);
255 let fps = flags
256 .get("--fps")
257 .and_then(|v| v.parse().ok())
258 .unwrap_or(60);
259 let duration = flags
260 .get("--duration")
261 .and_then(|v| v.parse().ok())
262 .unwrap_or(10.0);
263 let seed = flags
264 .get("--seed")
265 .and_then(|v| v.parse().ok())
266 .unwrap_or(42);
267
268 Command::Render {
269 domain,
270 format,
271 output,
272 fps,
273 duration,
274 seed,
275 }
276 }
277}