use std::path::Path;
use clap::builder::PossibleValue;
use clap::{Parser, ValueEnum};
use crate::cache;
use crate::color::Choice;
use crate::i18n::Language;
use crate::theme::Theme;
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[arg(value_name = "NAME")]
name: Option<String>,
#[arg(long, value_enum, value_name = "LANG")]
lang: Option<Language>,
#[arg(long, value_enum, value_name = "WHEN", default_value = "auto")]
color: Choice,
#[arg(long, value_enum, value_name = "PALETTE")]
theme: Option<Theme>,
#[arg(long, conflicts_with_all = ["name", "lang", "color", "theme", "cache_dir"])]
clear_cache: bool,
#[arg(long, conflicts_with_all = ["name", "lang", "color", "theme"])]
cache_dir: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Startup {
pub language: Option<Language>,
pub species: Option<String>,
pub theme: Option<Theme>,
pub color: Choice,
}
#[derive(Debug)]
pub enum Outcome {
Handled,
Launch(Startup),
}
pub async fn run() -> anyhow::Result<Outcome> {
dispatch(Cli::parse()).await
}
async fn dispatch(cli: Cli) -> anyhow::Result<Outcome> {
if cli.cache_dir {
println!("{}", cache_dir()?.display());
return Ok(Outcome::Handled);
}
if cli.clear_cache {
let dir = cache_dir()?;
if cache::clear(dir).await? {
println!("Removed {}", dir.display());
} else {
println!("Nothing to remove: {} does not exist", dir.display());
}
return Ok(Outcome::Handled);
}
Ok(Outcome::Launch(Startup {
language: cli.lang,
species: cli.name,
theme: cli.theme,
color: cli.color,
}))
}
fn cache_dir() -> anyhow::Result<&'static Path> {
cache::dir().ok_or_else(|| {
anyhow::anyhow!(
"could not work out a cache directory: set XDG_CACHE_HOME or HOME \
(LOCALAPPDATA on Windows)"
)
})
}
impl ValueEnum for Choice {
fn value_variants<'a>() -> &'a [Self] {
&[
Choice::Auto,
Choice::Truecolor,
Choice::Ansi256,
Choice::Never,
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Choice::Auto => PossibleValue::new("auto").help("Detect from COLORTERM and TERM"),
Choice::Truecolor => PossibleValue::new("truecolor").help("Force 24-bit colour"),
Choice::Ansi256 => PossibleValue::new("256").help("Force the 256-colour palette"),
Choice::Never => PossibleValue::new("never").help("No colour, and no sprites"),
})
}
}
impl ValueEnum for Language {
fn value_variants<'a>() -> &'a [Self] {
&Language::ALL
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(PossibleValue::new(self.flavor_code()).help(self.label()))
}
}
impl ValueEnum for Theme {
fn value_variants<'a>() -> &'a [Self] {
&Theme::ALL
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(PossibleValue::new(self.code()).help(self.label()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
Cli::try_parse_from(std::iter::once("pokeductor").chain(args.iter().copied()))
}
#[test]
fn the_command_definition_is_internally_consistent() {
Cli::command().debug_assert();
}
#[test]
fn a_bare_invocation_overrides_nothing() {
let cli = parse(&[]).expect("no arguments is valid");
assert_eq!(cli.name, None);
assert_eq!(cli.lang, None);
}
#[test]
fn a_palette_can_be_named_on_the_command_line() {
assert_eq!(parse(&["--theme", "dmg"]).unwrap().theme, Some(Theme::Dmg));
assert_eq!(
parse(&["--theme", "pico8"]).unwrap().theme,
Some(Theme::Pico8)
);
assert!(parse(&["--theme", "cga"]).is_err());
assert_eq!(parse(&[]).unwrap().theme, None);
}
#[test]
fn a_positional_argument_is_the_species_to_open() {
assert_eq!(parse(&["gengar"]).unwrap().name.as_deref(), Some("gengar"));
assert_eq!(
parse(&["type:ghost"]).unwrap().name.as_deref(),
Some("type:ghost")
);
}
#[test]
fn every_ui_language_is_an_accepted_lang_value() {
for language in Language::ALL {
let cli = parse(&["--lang", language.flavor_code()])
.unwrap_or_else(|_| panic!("--lang {} should parse", language.flavor_code()));
assert_eq!(cli.lang, Some(language));
}
}
#[test]
fn a_language_we_do_not_ship_is_rejected_rather_than_guessed_at() {
assert!(parse(&["--lang", "ja"]).is_err());
assert!(parse(&["--lang", "English"]).is_err());
}
#[test]
fn colour_defaults_to_working_it_out_from_the_environment() {
assert_eq!(parse(&[]).unwrap().color, Choice::Auto);
}
#[test]
fn every_colour_depth_is_reachable_by_the_name_it_is_offered_under() {
let expected = [
("auto", Choice::Auto),
("truecolor", Choice::Truecolor),
("256", Choice::Ansi256),
("never", Choice::Never),
];
for (value, choice) in expected {
assert_eq!(parse(&["--color", value]).unwrap().color, choice);
}
assert_eq!(
expected.len(),
Choice::value_variants().len(),
"every variant should have a spelling the tests cover"
);
}
#[test]
fn a_colour_depth_we_cannot_render_is_rejected() {
assert!(parse(&["--color", "16"]).is_err());
assert!(parse(&["--color", "always"]).is_err());
}
#[test]
fn the_cache_commands_refuse_arguments_they_would_ignore() {
assert!(parse(&["--clear-cache", "--cache-dir"]).is_err());
assert!(parse(&["--clear-cache", "gengar"]).is_err());
assert!(parse(&["--cache-dir", "gengar"]).is_err());
assert!(parse(&["--cache-dir", "--lang", "tr"]).is_err());
assert!(parse(&["--clear-cache", "--color", "never"]).is_err());
}
#[test]
fn an_unknown_flag_is_an_error_rather_than_a_species_name() {
assert!(parse(&["--shiny"]).is_err());
}
#[tokio::test]
async fn arguments_that_ask_for_nothing_special_launch_the_tui() {
let outcome = dispatch(parse(&["gengar", "--lang", "tr"]).unwrap())
.await
.expect("launching needs no filesystem");
match outcome {
Outcome::Launch(startup) => {
assert_eq!(startup.species.as_deref(), Some("gengar"));
assert_eq!(startup.language, Some(Language::Turkish));
assert_eq!(startup.color, Choice::Auto);
}
Outcome::Handled => panic!("nothing here is handled on the command line"),
}
}
}