1use clap::{Parser, Subcommand};
4
5#[derive(Parser, Debug)]
12#[command(
13 name = "",
14 no_binary_name = true,
15 disable_help_flag = true,
16 disable_help_subcommand = true,
17 disable_version_flag = true
18)]
19pub struct ReplCommand {
20 #[command(subcommand)]
21 pub action: Option<Action>,
22}
23
24#[derive(Subcommand, Debug)]
25pub enum Action {
26 Find {
28 #[arg(long)]
30 biome: Option<String>,
31
32 #[arg(long)]
34 infested: bool,
35
36 #[arg(long)]
38 within: Option<f64>,
39
40 #[arg(long)]
42 nearest: Option<usize>,
43
44 #[arg(long)]
46 named: bool,
47
48 #[arg(long)]
50 discoverer: Option<String>,
51
52 #[arg(long)]
54 from: Option<String>,
55 },
56
57 Show {
59 #[command(subcommand)]
60 target: ShowTarget,
61 },
62
63 Stats {
65 #[arg(long)]
67 biomes: bool,
68
69 #[arg(long)]
71 discoveries: bool,
72 },
73
74 Convert {
76 #[arg(long, group = "input")]
78 glyphs: Option<String>,
79
80 #[arg(long, group = "input")]
82 coords: Option<String>,
83
84 #[arg(long, group = "input")]
86 ga: Option<String>,
87
88 #[arg(long, group = "input")]
90 voxel: Option<String>,
91
92 #[arg(long)]
94 ssi: Option<u16>,
95
96 #[arg(long, default_value = "0")]
98 planet: u8,
99
100 #[arg(long, default_value = "0")]
102 galaxy: String,
103 },
104
105 Route {
107 #[arg(long)]
109 biome: Option<String>,
110
111 #[arg(long = "target", num_args = 1)]
113 targets: Vec<String>,
114
115 #[arg(long)]
117 from: Option<String>,
118
119 #[arg(long)]
121 warp_range: Option<f64>,
122
123 #[arg(long)]
125 within: Option<f64>,
126
127 #[arg(long)]
129 max_targets: Option<usize>,
130
131 #[arg(long)]
133 algo: Option<String>,
134
135 #[arg(long)]
137 round_trip: bool,
138 },
139
140 Set {
142 #[command(subcommand)]
143 target: SetTarget,
144 },
145
146 Reset {
148 #[arg(default_value = "all")]
150 target: String,
151 },
152
153 Status,
155
156 Info,
158
159 Help,
161
162 Exit,
164
165 Quit,
167}
168
169#[derive(Subcommand, Debug)]
170pub enum SetTarget {
171 Position {
173 name: String,
175 },
176 Biome {
178 name: String,
180 },
181 #[command(name = "warp-range")]
183 WarpRange {
184 ly: f64,
186 },
187}
188
189#[derive(Subcommand, Debug)]
190pub enum ShowTarget {
191 System {
193 name: String,
195 },
196 Base {
198 name: String,
200 },
201}
202
203pub fn parse_line(line: &str) -> Result<Option<Action>, String> {
208 let line = line.trim();
209 if line.is_empty() {
210 return Ok(None);
211 }
212
213 let args = shell_words(line);
214
215 match ReplCommand::try_parse_from(args) {
216 Ok(cmd) => Ok(cmd.action),
217 Err(e) => {
218 let rendered = e.render().to_string();
219 if e.use_stderr() {
220 Err(rendered)
221 } else {
222 print!("{rendered}");
224 Ok(None)
225 }
226 }
227 }
228}
229
230fn shell_words(input: &str) -> Vec<String> {
232 let mut words = Vec::new();
233 let mut current = String::new();
234 let mut in_quotes = false;
235
236 for ch in input.chars() {
237 match ch {
238 '"' => in_quotes = !in_quotes,
239 ' ' if !in_quotes => {
240 if !current.is_empty() {
241 words.push(std::mem::take(&mut current));
242 }
243 }
244 _ => current.push(ch),
245 }
246 }
247
248 if !current.is_empty() {
249 words.push(current);
250 }
251
252 words
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_parse_empty_line() {
261 assert!(parse_line("").unwrap().is_none());
262 assert!(parse_line(" ").unwrap().is_none());
263 }
264
265 #[test]
266 fn test_parse_exit() {
267 let action = parse_line("exit").unwrap().unwrap();
268 assert!(matches!(action, Action::Exit));
269 }
270
271 #[test]
272 fn test_parse_quit() {
273 let action = parse_line("quit").unwrap().unwrap();
274 assert!(matches!(action, Action::Quit));
275 }
276
277 #[test]
278 fn test_parse_help() {
279 let action = parse_line("help").unwrap().unwrap();
280 assert!(matches!(action, Action::Help));
281 }
282
283 #[test]
284 fn test_parse_find_with_biome() {
285 let action = parse_line("find --biome Lush --nearest 5")
286 .unwrap()
287 .unwrap();
288 match action {
289 Action::Find { biome, nearest, .. } => {
290 assert_eq!(biome.as_deref(), Some("Lush"));
291 assert_eq!(nearest, Some(5));
292 }
293 _ => panic!("Expected Find"),
294 }
295 }
296
297 #[test]
298 fn test_parse_show_base_quoted() {
299 let action = parse_line("show base \"Acadia National Park\"")
300 .unwrap()
301 .unwrap();
302 match action {
303 Action::Show {
304 target: ShowTarget::Base { name },
305 } => {
306 assert_eq!(name, "Acadia National Park");
307 }
308 _ => panic!("Expected Show Base"),
309 }
310 }
311
312 #[test]
313 fn test_parse_unknown_command() {
314 assert!(parse_line("foobar").is_err());
315 }
316
317 #[test]
318 fn test_shell_words_basic() {
319 let words = shell_words("find --biome Lush");
320 assert_eq!(words, vec!["find", "--biome", "Lush"]);
321 }
322
323 #[test]
324 fn test_shell_words_quoted() {
325 let words = shell_words("show base \"My Base Name\"");
326 assert_eq!(words, vec!["show", "base", "My Base Name"]);
327 }
328
329 #[test]
330 fn test_parse_stats_flags() {
331 let action = parse_line("stats --biomes").unwrap().unwrap();
332 match action {
333 Action::Stats {
334 biomes,
335 discoveries,
336 } => {
337 assert!(biomes);
338 assert!(!discoveries);
339 }
340 _ => panic!("Expected Stats"),
341 }
342 }
343
344 #[test]
345 fn test_parse_info() {
346 let action = parse_line("info").unwrap().unwrap();
347 assert!(matches!(action, Action::Info));
348 }
349
350 #[test]
351 fn test_parse_status() {
352 let action = parse_line("status").unwrap().unwrap();
353 assert!(matches!(action, Action::Status));
354 }
355
356 #[test]
357 fn test_parse_set_biome() {
358 let action = parse_line("set biome Lush").unwrap().unwrap();
359 match action {
360 Action::Set {
361 target: SetTarget::Biome { name },
362 } => assert_eq!(name, "Lush"),
363 _ => panic!("Expected Set Biome"),
364 }
365 }
366
367 #[test]
368 fn test_parse_set_position() {
369 let action = parse_line("set position \"Home Base\"").unwrap().unwrap();
370 match action {
371 Action::Set {
372 target: SetTarget::Position { name },
373 } => assert_eq!(name, "Home Base"),
374 _ => panic!("Expected Set Position"),
375 }
376 }
377
378 #[test]
379 fn test_parse_set_warp_range() {
380 let action = parse_line("set warp-range 2500").unwrap().unwrap();
381 match action {
382 Action::Set {
383 target: SetTarget::WarpRange { ly },
384 } => assert_eq!(ly, 2500.0),
385 _ => panic!("Expected Set WarpRange"),
386 }
387 }
388
389 #[test]
390 fn test_parse_reset_default() {
391 let action = parse_line("reset").unwrap().unwrap();
392 match action {
393 Action::Reset { target } => assert_eq!(target, "all"),
394 _ => panic!("Expected Reset"),
395 }
396 }
397
398 #[test]
399 fn test_parse_route_with_biome_and_warp_range() {
400 let action = parse_line("route --biome Lush --warp-range 2500")
401 .unwrap()
402 .unwrap();
403 match action {
404 Action::Route {
405 biome, warp_range, ..
406 } => {
407 assert_eq!(biome.as_deref(), Some("Lush"));
408 assert_eq!(warp_range, Some(2500.0));
409 }
410 _ => panic!("Expected Route"),
411 }
412 }
413
414 #[test]
415 fn test_parse_route_with_targets() {
416 let action = parse_line("route --target \"Alpha Base\" --target \"Beta Base\"")
417 .unwrap()
418 .unwrap();
419 match action {
420 Action::Route { targets, .. } => {
421 assert_eq!(targets.len(), 2);
422 assert_eq!(targets[0], "Alpha Base");
423 assert_eq!(targets[1], "Beta Base");
424 }
425 _ => panic!("Expected Route"),
426 }
427 }
428
429 #[test]
430 fn test_parse_route_round_trip() {
431 let action = parse_line("route --biome Lush --round-trip")
432 .unwrap()
433 .unwrap();
434 match action {
435 Action::Route { round_trip, .. } => {
436 assert!(round_trip);
437 }
438 _ => panic!("Expected Route"),
439 }
440 }
441
442 #[test]
443 fn test_parse_reset_biome() {
444 let action = parse_line("reset biome").unwrap().unwrap();
445 match action {
446 Action::Reset { target } => assert_eq!(target, "biome"),
447 _ => panic!("Expected Reset"),
448 }
449 }
450}