diff --git a/crates/irtt-app/src/applet.rs b/crates/irtt-app/src/applet.rs index 3e8f509..c797019 100644 --- a/crates/irtt-app/src/applet.rs +++ b/crates/irtt-app/src/applet.rs @@ -55,6 +55,10 @@ pub fn dispatch_from_argv(argv: Vec) -> Result }; match command { + "-V" | "--version" => Ok(AppletDispatch::Help(format!( + "irtt-rs {}", + env!("CARGO_PKG_VERSION") + ))), "-h" | "--help" => Ok(AppletDispatch::Help(dispatcher_help())), "client" => Ok(AppletDispatch::Run { applet: RequestedApplet::Client, @@ -78,6 +82,7 @@ pub fn dispatch_from_argv(argv: Vec) -> Result pub fn dispatcher_help() -> String { let mut command = Command::new("irtt-rs") + .version(env!("CARGO_PKG_VERSION")) .about("IRTT-compatible multi-applet dispatcher") .subcommand(Command::new("client").about(client_applet_about())) .subcommand(Command::new("tui").about(tui_applet_about())) diff --git a/crates/irtt-app/src/cmd/client/args.rs b/crates/irtt-app/src/cmd/client/args.rs index f936f0a..5813580 100644 --- a/crates/irtt-app/src/cmd/client/args.rs +++ b/crates/irtt-app/src/cmd/client/args.rs @@ -9,7 +9,11 @@ use clap::{Parser, ValueEnum}; pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10); #[derive(Debug, Clone, Parser)] -#[command(name = "irtt-client", about = "Minimal IRTT-compatible stream client")] +#[command( + name = "irtt-client", + version, + about = "Minimal IRTT-compatible stream client" +)] pub struct ClientArgs { /// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode. #[arg( @@ -38,13 +42,18 @@ pub struct ClientArgs { #[arg( long, + short = 'd', default_value = "10s", value_parser = parse_test_duration, - help = "Test duration (e.g. 2.5s, 1m30s, 1h); use 0 for continuous mode", - long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use 0 for continuous mode.\n\nFinite runs retain exact statistics for final summaries. Continuous mode uses bounded-memory running statistics and prints a final summary when interrupted; --targets-stdin also prints the final retained summary on EOF." + help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)", + long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use --continuous for continuous mode (0 is also accepted).\n\nFinite runs retain exact statistics for final summaries. Continuous mode uses bounded-memory running statistics and prints a final summary when interrupted; --targets-stdin also prints the final retained summary on EOF." )] pub duration: Duration, + /// Run continuously until interrupted (equivalent to --duration 0). + #[arg(long, conflicts_with = "duration")] + pub continuous: bool, + #[command(flatten)] pub common: CommonClientArgs, @@ -52,6 +61,7 @@ pub struct ClientArgs { #[arg( long, value_enum, + short = 'f', default_value_t = OutputFormat::Table, long_help = "Event row output format.\n\nTable is the default interactive format. CSV, TSV, and JSON Lines default to all columns for structured export." )] @@ -83,7 +93,7 @@ pub struct ClientArgs { pub header: HeaderMode, /// Include extra fields in table output and final summaries. - #[arg(long)] + #[arg(short = 'v', long)] pub verbose: bool, } @@ -94,11 +104,15 @@ impl ClientArgs { /// called, so listing columns still needs no target. pub async fn prepare(&self) -> Result { if self.targets_stdin && !self.is_continuous() { - return Err("--targets-stdin requires --duration 0".to_owned()); + return Err("--targets-stdin requires --continuous or --duration 0".to_owned()); } prepare_managed_run( &self.common, - self.duration, + if self.continuous { + Duration::ZERO + } else { + self.duration + }, TargetSelection { targets: &self.targets, pacing: self.pacing, @@ -109,7 +123,7 @@ impl ClientArgs { } pub fn is_continuous(&self) -> bool { - self.duration == Duration::ZERO + self.continuous || self.duration == Duration::ZERO } pub fn timestamp_mode(&self) -> TimestampArg { diff --git a/crates/irtt-app/src/cmd/server/args.rs b/crates/irtt-app/src/cmd/server/args.rs index 55fc52d..ec34d9a 100644 --- a/crates/irtt-app/src/cmd/server/args.rs +++ b/crates/irtt-app/src/cmd/server/args.rs @@ -15,6 +15,7 @@ const DEFAULT_PORT: u16 = 2112; #[derive(Clone, Parser)] #[command( name = "irtt-server", + version, about = "Minimal IRTT-compatible UDP server", after_help = "With no --bind, the server listens on the wildcard IRTT port for both address families ([::]:2112 and 0.0.0.0:2112) where the platform supports wildcard reply-source selection.\nRepeat --bind to serve multiple addresses in one process; any explicit --bind replaces the default pair entirely. All listeners use the same policy but keep independent session namespaces." )] diff --git a/crates/irtt-app/src/cmd/tui/args.rs b/crates/irtt-app/src/cmd/tui/args.rs index 8a7560d..0971d68 100644 --- a/crates/irtt-app/src/cmd/tui/args.rs +++ b/crates/irtt-app/src/cmd/tui/args.rs @@ -10,7 +10,11 @@ use crate::shared::client::{ pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO; #[derive(Debug, Clone, Parser)] -#[command(name = "irtt-tui", about = "Minimal IRTT-compatible TUI client")] +#[command( + name = "irtt-tui", + version, + about = "Minimal IRTT-compatible TUI client" +)] pub struct TuiArgs { /// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode. #[arg( @@ -33,13 +37,18 @@ pub struct TuiArgs { #[arg( long, + short = 'd', default_value = "0", value_parser = parse_test_duration, - help = "Test duration (e.g. 2.5s, 1m30s, 1h); use 0 for continuous mode", - long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use 0 for continuous mode. The TUI defaults to continuous mode." + help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)", + long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use --continuous for continuous mode (0 is also accepted). The TUI defaults to continuous mode." )] pub duration: Duration, + /// Run continuously until interrupted (equivalent to --duration 0). + #[arg(long, conflicts_with = "duration")] + pub continuous: bool, + #[command(flatten)] pub common: CommonClientArgs, } @@ -52,7 +61,11 @@ impl TuiArgs { pub async fn prepare(&self) -> Result { prepare_managed_run( &self.common, - self.duration, + if self.continuous { + Duration::ZERO + } else { + self.duration + }, TargetSelection { targets: &self.targets, pacing: self.pacing, @@ -63,7 +76,7 @@ impl TuiArgs { } pub fn is_continuous(&self) -> bool { - self.duration == Duration::ZERO + self.continuous || self.duration == Duration::ZERO } } diff --git a/crates/irtt-app/src/shared/client/args.rs b/crates/irtt-app/src/shared/client/args.rs index 1bcbf05..def4f9c 100644 --- a/crates/irtt-app/src/shared/client/args.rs +++ b/crates/irtt-app/src/shared/client/args.rs @@ -19,11 +19,11 @@ pub struct CommonClientArgs { pub dual_stack: bool, /// Positive probe interval (e.g. 2.5s, 1m30s, 500us, 100ns, 1h). - #[arg(long, default_value = "1s", value_parser = parse_duration)] + #[arg(short = 'i', long, default_value = "1s", value_parser = parse_duration)] pub interval: Duration, /// UDP payload length. - #[arg(long, default_value_t = 0, value_parser = parse_length)] + #[arg(short = 'l', long, default_value_t = 0, value_parser = parse_length)] pub length: u32, // Decode during configuration preparation so Clap cannot echo rejected keys.