use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(
name = "",
no_binary_name = 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,
},
List {
#[command(subcommand)]
target: ListTarget,
},
Map,
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 ListTarget {
Galaxies {
#[arg(long = "type")]
galaxy_type: Option<String>,
},
Biomes,
Glyphs,
Bases {
#[arg(long, default_value = "0")]
limit: usize,
#[arg(long)]
all: bool,
},
Systems {
#[arg(long, default_value = "50")]
limit: usize,
#[arg(long)]
all: bool,
},
#[command(name = "terrain-types")]
TerrainTypes,
}
#[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 mut args = shell_words(line);
if args.len() >= 2 && args.last().map(|s| s.as_str()) == Some("help") {
let last = args.len() - 1;
args[last] = "--help".to_string();
}
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"),
}
}
#[test]
fn test_parse_list_galaxies() {
let action = parse_line("list galaxies").unwrap().unwrap();
match action {
Action::List {
target: ListTarget::Galaxies { galaxy_type },
} => assert!(galaxy_type.is_none()),
_ => panic!("Expected List Galaxies"),
}
}
#[test]
fn test_parse_list_galaxies_with_type() {
let action = parse_line("list galaxies --type Lush").unwrap().unwrap();
match action {
Action::List {
target: ListTarget::Galaxies { galaxy_type },
} => assert_eq!(galaxy_type.as_deref(), Some("Lush")),
_ => panic!("Expected List Galaxies"),
}
}
#[test]
fn test_parse_list_biomes() {
let action = parse_line("list biomes").unwrap().unwrap();
assert!(matches!(
action,
Action::List {
target: ListTarget::Biomes
}
));
}
#[test]
fn test_parse_list_glyphs() {
let action = parse_line("list glyphs").unwrap().unwrap();
assert!(matches!(
action,
Action::List {
target: ListTarget::Glyphs
}
));
}
#[test]
fn test_parse_list_bases() {
let action = parse_line("list bases").unwrap().unwrap();
assert!(matches!(
action,
Action::List {
target: ListTarget::Bases { .. }
}
));
}
#[test]
fn test_parse_list_bases_with_all() {
let action = parse_line("list bases --all").unwrap().unwrap();
match action {
Action::List {
target: ListTarget::Bases { all, .. },
} => assert!(all),
_ => panic!("Expected List Bases"),
}
}
#[test]
fn test_parse_list_systems() {
let action = parse_line("list systems").unwrap().unwrap();
match action {
Action::List {
target: ListTarget::Systems { limit, all },
} => {
assert_eq!(limit, 50);
assert!(!all);
}
_ => panic!("Expected List Systems"),
}
}
#[test]
fn test_parse_list_systems_with_all() {
let action = parse_line("list systems --all").unwrap().unwrap();
match action {
Action::List {
target: ListTarget::Systems { all, .. },
} => assert!(all),
_ => panic!("Expected List Systems"),
}
}
#[test]
fn test_parse_list_systems_with_limit() {
let action = parse_line("list systems --limit 10").unwrap().unwrap();
match action {
Action::List {
target: ListTarget::Systems { limit, .. },
} => assert_eq!(limit, 10),
_ => panic!("Expected List Systems"),
}
}
#[test]
fn test_parse_list_terrain_types() {
let action = parse_line("list terrain-types").unwrap().unwrap();
assert!(matches!(
action,
Action::List {
target: ListTarget::TerrainTypes
}
));
}
#[test]
fn test_parse_map() {
let action = parse_line("map").unwrap().unwrap();
assert!(matches!(action, Action::Map));
}
#[test]
fn test_parse_command_help_shows_subcommand_help() {
let result = parse_line("find help");
assert!(result.unwrap().is_none());
}
#[test]
fn test_parse_command_dash_help_shows_subcommand_help() {
let result = parse_line("find --help");
assert!(result.unwrap().is_none());
}
}