use crate::error::Error;
use crate::mode::ExplicitChoice;
use clap::{Parser, Subcommand};
#[derive(Parser, Debug, Clone)]
#[command(
name = "rusty-ts",
version,
about = "Prefix each line of stdin with a timestamp (Rust port of moreutils ts)",
long_about = None,
)]
pub struct Cli {
#[arg(
short = 'i',
long = "incremental",
conflicts_with = "since_start",
action = clap::ArgAction::SetTrue
)]
pub incremental: bool,
#[arg(
short = 's',
long = "since-start",
conflicts_with = "incremental",
action = clap::ArgAction::SetTrue
)]
pub since_start: bool,
#[arg(short = 'm', long = "monotonic", action = clap::ArgAction::SetTrue)]
pub monotonic: bool,
#[arg(short = 'r', long = "relative", action = clap::ArgAction::SetTrue)]
pub relative: bool,
#[arg(
short = 'u',
long = "utc",
conflicts_with = "tz",
action = clap::ArgAction::SetTrue
)]
pub utc: bool,
#[arg(long = "tz", value_name = "IANA-NAME", conflicts_with = "utc")]
pub tz: Option<String>,
#[arg(
long = "strict",
alias = "moreutils-compat",
conflicts_with = "no_strict",
action = clap::ArgAction::SetTrue
)]
pub strict: bool,
#[arg(
long = "no-strict",
alias = "no-moreutils-compat",
conflicts_with = "strict",
action = clap::ArgAction::SetTrue
)]
pub no_strict: bool,
#[arg(value_name = "FORMAT")]
pub format: Option<String>,
#[command(subcommand)]
pub subcommand: Option<CliCommand>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum CliCommand {
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
impl Cli {
pub fn explicit_compat_choice(&self) -> Option<ExplicitChoice> {
match (self.strict, self.no_strict) {
(true, false) => Some(ExplicitChoice::Strict),
(false, true) => Some(ExplicitChoice::Default),
(false, false) => None,
(true, true) => None,
}
}
pub fn validate(&self) -> Result<(), Error> {
if self.utc {
if let Some(tz) = &self.tz {
return Err(Error::InvalidUtcWithNamedTz { tz: tz.clone() });
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_command_factory_builds_without_panic() {
let _cmd = Cli::command();
}
#[test]
fn explicit_choice_signals_strict_when_strict_flag_set() {
let cli = Cli::parse_from(["rusty-ts", "--strict"]);
assert_eq!(cli.explicit_compat_choice(), Some(ExplicitChoice::Strict));
}
#[test]
fn explicit_choice_signals_default_when_no_strict_flag_set() {
let cli = Cli::parse_from(["rusty-ts", "--no-strict"]);
assert_eq!(cli.explicit_compat_choice(), Some(ExplicitChoice::Default));
}
#[test]
fn explicit_choice_none_when_neither_flag() {
let cli = Cli::parse_from(["rusty-ts"]);
assert_eq!(cli.explicit_compat_choice(), None);
}
#[test]
fn positional_format_captured() {
let cli = Cli::parse_from(["rusty-ts", "%Y-%m-%d %H:%M:%S"]);
assert_eq!(cli.format.as_deref(), Some("%Y-%m-%d %H:%M:%S"));
}
#[test]
fn utc_flag_parsed() {
let cli = Cli::parse_from(["rusty-ts", "-u"]);
assert!(cli.utc);
assert!(cli.tz.is_none());
}
#[test]
fn tz_flag_parsed() {
let cli = Cli::parse_from(["rusty-ts", "--tz=Asia/Tokyo"]);
assert_eq!(cli.tz.as_deref(), Some("Asia/Tokyo"));
assert!(!cli.utc);
}
#[test]
fn utc_and_tz_rejected_by_clap_conflict() {
let err = Cli::try_parse_from(["rusty-ts", "-u", "--tz=Asia/Tokyo"]);
assert!(
err.is_err(),
"clap should reject -u + --tz=... via conflicts_with"
);
}
#[test]
fn incremental_and_since_start_rejected_by_clap_conflict() {
let err = Cli::try_parse_from(["rusty-ts", "-i", "-s"]);
assert!(
err.is_err(),
"clap should reject -i + -s via conflicts_with"
);
}
#[test]
fn validate_passes_when_only_utc() {
let cli = Cli::parse_from(["rusty-ts", "-u"]);
assert!(cli.validate().is_ok());
}
#[test]
fn completions_subcommand_parsed() {
let cli = Cli::parse_from(["rusty-ts", "completions", "bash"]);
assert!(matches!(
cli.subcommand,
Some(CliCommand::Completions { shell: _ })
));
}
}