use std::path::PathBuf;
use chrono::DateTime;
use chrono::Utc;
use clap::Args;
use clap::ColorChoice;
use clap::Parser;
use clap::ValueEnum;
use color_eyre::eyre;
use tytanic_core::config::Direction;
use tytanic_core::doc::compile::Warnings;
use tytanic_core::test::Id;
use tytanic_core::test::unit::Kind;
use super::Context;
pub mod delete;
pub mod list;
pub mod new;
pub mod run;
pub mod status;
pub mod update;
pub mod util;
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
pub trait OptionDelegate: Sized {
type Native;
fn into_native(self) -> Self::Native;
}
#[derive(clap::ValueEnum, Debug, Clone, Copy)]
pub enum KindOption {
Persistent,
Ephemeral,
CompileOnly,
}
impl OptionDelegate for KindOption {
type Native = Kind;
fn into_native(self) -> Self::Native {
match self {
Self::Persistent => Kind::Persistent,
Self::Ephemeral => Kind::Ephemeral,
Self::CompileOnly => Kind::CompileOnly,
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum WarningsOption {
Ignore,
Emit,
Promote,
}
impl OptionDelegate for WarningsOption {
type Native = Warnings;
fn into_native(self) -> Self::Native {
match self {
Self::Ignore => Warnings::Ignore,
Self::Emit => Warnings::Emit,
Self::Promote => Warnings::Promote,
}
}
}
pub trait Switch: Sized {
const DEFAULT: bool;
fn get(self) -> Option<bool>;
fn get_or_default(self) -> bool {
self.get().unwrap_or(Self::DEFAULT)
}
}
macro_rules! impl_switch {
(
$(#[$switch_meta:meta])*
$switch:ident($default:literal) {
$(#[$field_meta:meta])*
$field:ident $(= $field_short:literal)?,
$(#[$no_field_meta:meta])*
$no_field:ident $(= $no_field_short:literal)?,
}
) => {
$(#[$switch_meta])*
#[derive(Args, Clone, Copy)]
pub struct $switch {
$(#[$field_meta])*
#[arg(long, global = true)]
$(#[arg(short = $field_short)])?
$field: bool,
$(#[$no_field_meta])*
#[arg(long, overrides_with = stringify!($field), global = true)]
$(#[arg(short = $no_field_short)])?
$no_field: bool,
}
impl std::fmt::Debug for $switch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(stringify!($switch)).field(&self.get_or_default()).finish()
}
}
impl Switch for $switch {
const DEFAULT: bool = $default;
fn get(self) -> Option<bool> {
if self.$field {
Some(true)
} else if self.$no_field {
Some(false)
} else {
None
}
}
}
};
}
impl_switch! {
UseEmbeddedFontsSwitch(true) {
#[cfg_attr(not(feature = "embed-fonts"), clap(hide = true))]
use_embedded_fonts,
#[cfg_attr(not(feature = "embed-fonts"), clap(hide = true))]
no_use_embedded_fonts,
}
}
impl_switch! {
UseSystemFontsSwitch(false) {
use_system_fonts,
no_use_system_fonts,
}
}
impl_switch! {
TemplateSwitch(true) {
template,
no_template,
}
}
impl_switch! {
CompareSwitch(true) {
compare,
no_compare,
}
}
impl_switch! {
ExportEphemeralSwitch(true) {
export_ephemeral,
no_export_ephemeral,
}
}
impl_switch! {
FailFastSwitch(true) {
fail_fast = 'f',
no_fail_fast = 'F',
}
}
impl_switch! {
SkipSwitch(true) {
skip = 's',
no_skip = 'S',
}
}
impl_switch! {
OptimizeRefsSwitch(true) {
optimize_refs,
no_optimize_refs,
}
}
macro_rules! ansi {
($s:expr; b) => {
concat!("\x1B[1m", $s, "\x1B[0m")
};
($s:expr; u) => {
concat!("\x1B[4m", $s, "\x1B[0m")
};
($s:expr;) => {
$s
};
($s:expr; $first:ident $( + $rest:tt)*) => {
ansi!(ansi!($s; $($rest)*); $first)
};
}
#[rustfmt::skip]
static AFTER_LONG_ABOUT: &str = concat!(
ansi!("Exit Codes:\n"; u + b),
" ", ansi!("0"; b), " Success\n",
" ", ansi!("1"; b), " At least one test failed\n",
" ", ansi!("2"; b), " The requested operation failed\n",
" ", ansi!("3"; b), " An unexpected error occurred",
);
#[derive(Parser, Debug, Clone)]
#[command(version, after_long_help = AFTER_LONG_ABOUT)]
pub struct CliArguments {
#[command(subcommand)]
pub cmd: Command,
#[arg(long, short, env = "TYPST_ROOT", global = true)]
pub root: Option<PathBuf>,
#[arg(long, short, global = true)]
pub jobs: Option<usize>,
#[arg(long, value_enum, default_value = "auto", global = true)]
pub vcs: Vcs,
#[command(flatten, next_help_heading = "Font Options")]
pub font: FontOptions,
#[command(flatten, next_help_heading = "Package Options")]
pub package: PackageOptions,
#[command(flatten, next_help_heading = "Output Options")]
pub output: OutputArgs,
}
#[derive(Debug, Default, Clone, Copy, ValueEnum)]
pub enum Vcs {
#[default]
Auto,
Git,
Mercurial,
Hg,
}
#[derive(Args, Debug, Clone)]
pub struct FontOptions {
#[command(flatten)]
pub use_embedded_fonts: UseEmbeddedFontsSwitch,
#[command(flatten)]
pub use_system_fonts: UseSystemFontsSwitch,
#[arg(
long = "font-path",
env = "TYPST_FONT_PATHS",
value_name = "DIR",
value_delimiter = ENV_PATH_SEP,
global = true,
)]
pub font_paths: Vec<PathBuf>,
}
#[derive(Args, Debug, Clone)]
pub struct PackageOptions {
#[clap(long, env = "TYPST_PACKAGE_PATH", value_name = "DIR", global = true)]
pub package_path: Option<PathBuf>,
#[clap(
long,
env = "TYPST_PACKAGE_CACHE_PATH",
value_name = "DIR",
global = true
)]
pub package_cache_path: Option<PathBuf>,
#[clap(long, visible_alias = "cert", env = "TYPST_CERT", global = true)]
pub certificate: Option<PathBuf>,
}
#[derive(Args, Debug, Clone)]
pub struct FilterOptions {
#[allow(rustdoc::bare_urls)]
#[arg(short, long, default_value = "all()", value_name = "EXPR")]
pub expression: String,
#[command(flatten)]
pub skip: SkipSwitch,
#[arg(required = false, conflicts_with = "expression", value_name = "TEST")]
pub tests: Vec<Id>,
}
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
if raw.eq_ignore_ascii_case("now") {
return Ok(Utc::now());
}
let timestamp: i64 = raw.parse().map_err(|err| {
format!("timestamp must be decimal integer or the literal string `now` ({err})")
})?;
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| "timestamp out of range".to_string())
}
#[derive(Args, Debug, Clone)]
pub struct CompileOptions {
#[arg(
long,
env = "SOURCE_DATE_EPOCH",
value_name = "now|<UNIX_TIMESTAMP>",
default_value = "0",
value_parser = parse_source_date_epoch,
global = true,
)]
pub timestamp: DateTime<Utc>,
#[arg(long, default_value = "emit", value_name = "WHAT")]
pub warnings: WarningsOption,
}
#[derive(Args, Debug, Clone)]
pub struct ExportOptions {
#[arg(long)]
pub dir: Option<DirectionOption>,
#[arg(long)]
pub ppi: Option<f32>,
#[command(flatten)]
pub export_ephemeral: ExportEphemeralSwitch,
#[command(flatten)]
pub optimize_refs: OptimizeRefsSwitch,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DirectionOption {
Ltr,
Rtl,
}
impl OptionDelegate for DirectionOption {
type Native = Direction;
fn into_native(self) -> Self::Native {
match self {
DirectionOption::Ltr => Direction::Ltr,
DirectionOption::Rtl => Direction::Rtl,
}
}
}
#[derive(Args, Debug, Clone)]
pub struct CompareOptions {
#[command(flatten)]
pub compare: CompareSwitch,
#[arg(long)]
pub max_delta: Option<u8>,
#[arg(long)]
pub max_deviations: Option<usize>,
}
#[derive(Args, Debug, Clone)]
pub struct RunnerOptions {
#[command(flatten)]
pub fail_fast: FailFastSwitch,
}
#[derive(Args, Debug, Clone)]
pub struct OutputArgs {
#[clap(
long,
value_name = "WHEN",
require_equals = true,
num_args = 0..=1,
default_value = "auto",
default_missing_value = "always",
global = true,
)]
pub color: ColorChoice,
#[arg(long, short, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
}
#[derive(clap::Subcommand, Debug, Clone)]
pub enum Command {
#[command(visible_alias = "st")]
Status(status::Args),
#[command(visible_alias = "ls")]
List(list::Args),
#[command(visible_alias = "r")]
Run(run::Args),
#[command()]
Update(update::Args),
#[command(alias = "add")]
New(new::Args),
#[command(alias = "remove", alias = "rm")]
Delete(delete::Args),
#[command()]
Util(util::Args),
}
impl Command {
pub fn run(&self, ctx: &mut Context<'_>) -> eyre::Result<()> {
match self {
Command::New(args) => new::run(ctx, args),
Command::Delete(args) => delete::run(ctx, args),
Command::Status(args) => status::run(ctx, args),
Command::List(args) => list::run(ctx, args),
Command::Update(args) => update::run(ctx, args),
Command::Run(args) => run::run(ctx, args),
Command::Util(args) => args.cmd.run(ctx),
}
}
}