use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(
name = "",
no_binary_name = true,
disable_help_flag = true,
disable_help_subcommand = true,
disable_version_flag = true
)]
pub struct ReplCommand {
#[command(subcommand)]
pub action: Option<Action>,
}
#[derive(Subcommand, Debug)]
pub enum Action {
Find {
#[arg(long)]
biome: Option<String>,
#[arg(long)]
infested: bool,
#[arg(long)]
within: Option<f64>,
#[arg(long)]
nearest: Option<usize>,
#[arg(long)]
named: bool,
#[arg(long)]
discoverer: Option<String>,
#[arg(long)]
from: Option<String>,
},
Show {
#[command(subcommand)]
target: ShowTarget,
},
Stats {
#[arg(long)]
biomes: bool,
#[arg(long)]
discoveries: bool,
},
Convert {
#[arg(long, group = "input")]
glyphs: Option<String>,
#[arg(long, group = "input")]
coords: Option<String>,
#[arg(long, group = "input")]
ga: Option<String>,
#[arg(long, group = "input")]
voxel: Option<String>,
#[arg(long)]
ssi: Option<u16>,
#[arg(long, default_value = "0")]
planet: u8,
#[arg(long, default_value = "0")]
galaxy: String,
},
Route {
#[arg(long)]
biome: Option<String>,
#[arg(long = "target", num_args = 1)]
targets: Vec<String>,
#[arg(long)]
from: Option<String>,
#[arg(long)]
warp_range: Option<f64>,
#[arg(long)]
within: Option<f64>,
#[arg(long)]
max_targets: Option<usize>,
#[arg(long)]
algo: Option<String>,
#[arg(long)]
round_trip: bool,
},
Set {
#[command(subcommand)]
target: SetTarget,
},
Reset {
#[arg(default_value = "all")]
target: String,
},
Status,
Info,
Help,
Exit,
Quit,
}
#[derive(Subcommand, Debug)]
pub enum SetTarget {
Position {
name: String,
},
Biome {
name: String,
},
#[command(name = "warp-range")]
WarpRange {
ly: f64,
},
}
#[derive(Subcommand, Debug)]
pub enum ShowTarget {
System {
name: String,
},
Base {
name: String,
},
}
pub fn parse_line(line: &str) -> Result<Option<Action>, String> {
let line = line.trim();
if line.is_empty() {
return Ok(None);
}
let args = shell_words(line);
match ReplCommand::try_parse_from(args) {
Ok(cmd) => Ok(cmd.action),
Err(e) => {
let rendered = e.render().to_string();
if e.use_stderr() {
Err(rendered)
} else {
print!("{rendered}");
Ok(None)
}
}
}
}
fn shell_words(input: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
for ch in input.chars() {
match ch {
'"' => in_quotes = !in_quotes,
' ' if !in_quotes => {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
words.push(current);
}
words
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_empty_line() {
assert!(parse_line("").unwrap().is_none());
assert!(parse_line(" ").unwrap().is_none());
}
#[test]
fn test_parse_exit() {
let action = parse_line("exit").unwrap().unwrap();
assert!(matches!(action, Action::Exit));
}
#[test]
fn test_parse_quit() {
let action = parse_line("quit").unwrap().unwrap();
assert!(matches!(action, Action::Quit));
}
#[test]
fn test_parse_help() {
let action = parse_line("help").unwrap().unwrap();
assert!(matches!(action, Action::Help));
}
#[test]
fn test_parse_find_with_biome() {
let action = parse_line("find --biome Lush --nearest 5")
.unwrap()
.unwrap();
match action {
Action::Find { biome, nearest, .. } => {
assert_eq!(biome.as_deref(), Some("Lush"));
assert_eq!(nearest, Some(5));
}
_ => panic!("Expected Find"),
}
}
#[test]
fn test_parse_show_base_quoted() {
let action = parse_line("show base \"Acadia National Park\"")
.unwrap()
.unwrap();
match action {
Action::Show {
target: ShowTarget::Base { name },
} => {
assert_eq!(name, "Acadia National Park");
}
_ => panic!("Expected Show Base"),
}
}
#[test]
fn test_parse_unknown_command() {
assert!(parse_line("foobar").is_err());
}
#[test]
fn test_shell_words_basic() {
let words = shell_words("find --biome Lush");
assert_eq!(words, vec!["find", "--biome", "Lush"]);
}
#[test]
fn test_shell_words_quoted() {
let words = shell_words("show base \"My Base Name\"");
assert_eq!(words, vec!["show", "base", "My Base Name"]);
}
#[test]
fn test_parse_stats_flags() {
let action = parse_line("stats --biomes").unwrap().unwrap();
match action {
Action::Stats {
biomes,
discoveries,
} => {
assert!(biomes);
assert!(!discoveries);
}
_ => panic!("Expected Stats"),
}
}
#[test]
fn test_parse_info() {
let action = parse_line("info").unwrap().unwrap();
assert!(matches!(action, Action::Info));
}
#[test]
fn test_parse_status() {
let action = parse_line("status").unwrap().unwrap();
assert!(matches!(action, Action::Status));
}
#[test]
fn test_parse_set_biome() {
let action = parse_line("set biome Lush").unwrap().unwrap();
match action {
Action::Set {
target: SetTarget::Biome { name },
} => assert_eq!(name, "Lush"),
_ => panic!("Expected Set Biome"),
}
}
#[test]
fn test_parse_set_position() {
let action = parse_line("set position \"Home Base\"").unwrap().unwrap();
match action {
Action::Set {
target: SetTarget::Position { name },
} => assert_eq!(name, "Home Base"),
_ => panic!("Expected Set Position"),
}
}
#[test]
fn test_parse_set_warp_range() {
let action = parse_line("set warp-range 2500").unwrap().unwrap();
match action {
Action::Set {
target: SetTarget::WarpRange { ly },
} => assert_eq!(ly, 2500.0),
_ => panic!("Expected Set WarpRange"),
}
}
#[test]
fn test_parse_reset_default() {
let action = parse_line("reset").unwrap().unwrap();
match action {
Action::Reset { target } => assert_eq!(target, "all"),
_ => panic!("Expected Reset"),
}
}
#[test]
fn test_parse_route_with_biome_and_warp_range() {
let action = parse_line("route --biome Lush --warp-range 2500")
.unwrap()
.unwrap();
match action {
Action::Route {
biome, warp_range, ..
} => {
assert_eq!(biome.as_deref(), Some("Lush"));
assert_eq!(warp_range, Some(2500.0));
}
_ => panic!("Expected Route"),
}
}
#[test]
fn test_parse_route_with_targets() {
let action = parse_line("route --target \"Alpha Base\" --target \"Beta Base\"")
.unwrap()
.unwrap();
match action {
Action::Route { targets, .. } => {
assert_eq!(targets.len(), 2);
assert_eq!(targets[0], "Alpha Base");
assert_eq!(targets[1], "Beta Base");
}
_ => panic!("Expected Route"),
}
}
#[test]
fn test_parse_route_round_trip() {
let action = parse_line("route --biome Lush --round-trip")
.unwrap()
.unwrap();
match action {
Action::Route { round_trip, .. } => {
assert!(round_trip);
}
_ => panic!("Expected Route"),
}
}
#[test]
fn test_parse_reset_biome() {
let action = parse_line("reset biome").unwrap().unwrap();
match action {
Action::Reset { target } => assert_eq!(target, "biome"),
_ => panic!("Expected Reset"),
}
}
}