use crate::filter::{Bit, Block, Dye, Emoji, Filter, Hanzi};
use crate::genesis::Density;
use crate::screen::FpsMax;
use clap::{Arg, ArgAction, ArgMatches, ValueEnum, command, value_parser};
use crossterm::style::Color;
use std::sync::LazyLock;
static MATCHES: LazyLock<ArgMatches> = LazyLock::new(|| {
command!()
.args([
Arg::new("nrows")
.short('r')
.long("nrows")
.value_name("INTEGER")
.value_parser(value_parser!(usize))
.default_value("40")
.help("Number of rows"),
Arg::new("ncols")
.short('c')
.long("ncols")
.value_name("INTEGER")
.value_parser(value_parser!(usize))
.default_value("40")
.help("Number of columns"),
Arg::new("seed")
.long("seed")
.value_name("STRING")
.help("Seed for world initialization"),
Arg::new("density")
.short('p')
.long("density")
.value_name("DECIMAL")
.value_parser(value_parser!(f64))
.default_value("0.5")
.help("Initial population density (use default if out of range [0.0, 1.0])"),
Arg::new("filter")
.long("filter")
.value_name("FILTER")
.value_parser(value_parser!(FilterKind))
.default_value("dye")
.hide_default_value(true)
.hide_possible_values(true)
.help(FilterKind::help(
"Filter to observe the world",
Some(FilterKind::Dye),
)),
Arg::new("color-dead")
.short('D')
.long("color-dead")
.value_name("COLOR")
.value_parser(value_parser!(ColorKind))
.default_value("green")
.hide_default_value(true)
.hide_possible_values(true)
.help(ColorKind::help(
"Color for dead cells (omit if filter is not dye)",
Some(ColorKind::Green),
)),
Arg::new("color-alive")
.short('A')
.long("color-alive")
.value_name("COLOR")
.value_parser(value_parser!(ColorKind))
.default_value("white")
.hide_default_value(true)
.hide_possible_values(true)
.help(ColorKind::help(
"Color for alive cells (omit if filter is not dye)",
Some(ColorKind::White),
)),
Arg::new("fps-max")
.long("fps-max")
.value_name("DECIMAL")
.value_parser(value_parser!(f64))
.default_value("60.0")
.help("Maximum fps (use default if out of range [0.0, inf))"),
Arg::new("show-stats")
.long("show-stats")
.action(ArgAction::SetTrue)
.help("Show statistics"),
])
.get_matches()
});
pub struct Args {
pub nrows: usize,
pub ncols: usize,
pub seed: Option<&'static str>,
pub density: Density,
pub filter: Box<dyn Filter>,
pub fps_max: FpsMax,
pub show_stats: bool,
}
impl Args {
pub fn parse() -> Self {
let nrows = MATCHES
.get_one("nrows")
.copied()
.unwrap_or_else(|| unreachable!());
let ncols = MATCHES
.get_one("ncols")
.copied()
.unwrap_or_else(|| unreachable!());
let seed = MATCHES.get_one("seed").map(String::as_ref);
let density = MATCHES
.get_one("density")
.copied()
.unwrap_or_else(|| unreachable!());
let filter = MATCHES
.get_one("filter")
.copied()
.unwrap_or_else(|| unreachable!());
let fps_max = MATCHES
.get_one("fps-max")
.copied()
.unwrap_or_else(|| unreachable!());
let show_stats = MATCHES.get_flag("show-stats");
let density = Density::new(density)
.or(Density::new(0.5))
.unwrap_or_else(|| unreachable!());
let filter: Box<dyn Filter> = match filter {
FilterKind::Bit => Box::new(Bit),
FilterKind::Block => Box::new(Block),
FilterKind::Dye => {
let color_dead = MATCHES
.get_one::<ColorKind>("color-dead")
.copied()
.unwrap_or_else(|| unreachable!())
.into();
let color_alive = MATCHES
.get_one::<ColorKind>("color-alive")
.copied()
.unwrap_or_else(|| unreachable!())
.into();
Box::new(Dye::new(color_dead, color_alive))
}
FilterKind::Emoji => Box::new(Emoji::random()),
FilterKind::Hanzi => Box::new(Hanzi),
};
let fps_max = FpsMax::new(fps_max)
.or(FpsMax::new(60.0))
.unwrap_or_else(|| unreachable!());
Self {
nrows,
ncols,
seed,
density,
filter,
fps_max,
show_stats,
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum FilterKind {
Bit,
Block,
Dye,
Emoji,
Hanzi,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum ColorKind {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl From<ColorKind> for Color {
fn from(value: ColorKind) -> Self {
match value {
ColorKind::Black => Color::Black,
ColorKind::Red => Color::Red,
ColorKind::Green => Color::Green,
ColorKind::Yellow => Color::Yellow,
ColorKind::Blue => Color::Blue,
ColorKind::Magenta => Color::Magenta,
ColorKind::Cyan => Color::Cyan,
ColorKind::White => Color::White,
}
}
}
trait ValueEnumExt: ValueEnum {
fn help(description: &str, default: Option<Self>) -> String {
let mut message = description.to_string();
if let Some(default) = default.and_then(|variant| variant.to_possible_value()) {
message.push_str(" [default: ");
message.push_str(default.get_name());
message.push(']');
}
Self::value_variants()
.iter()
.filter_map(ValueEnum::to_possible_value)
.for_each(|entry| {
message.push_str("\n- ");
message.push_str(entry.get_name());
});
message
}
}
impl<T> ValueEnumExt for T where T: ValueEnum {}