From b33f01cfe106a4d06d420fec75cdee9b0de99392 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Mon, 22 Jun 2026 15:13:36 +0700 Subject: [PATCH] feat(cli): enhance help documentation and dynamic applet dispatch --- README.md | 5 +- crates/irtt-cli/src/applet.rs | 65 ++++++++++++++++++++---- crates/irtt-cli/src/cmd/client/args.rs | 32 +++++++++--- crates/irtt-cli/src/cmd/client/output.rs | 16 +++--- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c13711e..dd9938f 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,9 @@ warning_kind, message, event_wall_ns, client_send_wall_ns, client_receive_wall_ns ``` -Aliases are accepted for readability: `receive_delay` for `rd`, `send_delay` -for `sd`, `server_processing` for `proc`, `server_received_count` for +Aliases are accepted for readability: `receive_delay` and `receive_delay_us` +for `rd` and `rd_us`, `send_delay` and `send_delay_us` for `sd` and `sd_us`, +`server_processing` for `proc`, `server_received_count` for `server_received`, and `server_received_window` for `server_window`. The default table output favors readability with compact columns: diff --git a/crates/irtt-cli/src/applet.rs b/crates/irtt-cli/src/applet.rs index 832ba4e..ccf29ef 100644 --- a/crates/irtt-cli/src/applet.rs +++ b/crates/irtt-cli/src/applet.rs @@ -41,13 +41,15 @@ pub fn dispatch_from_argv(argv: Vec) -> Result let basename = applet_basename(argv0); if basename.starts_with("irtt-") && basename != "irtt-rs" { return Err(format!( - "unknown applet name '{basename}'. Known applet names: irtt-rs, irtt-cli, irtt-tui, irtt-server" + "unknown applet name '{basename}'. Known applet names: {}", + known_applet_binary_names() )); } let Some(command) = argv.get(1).and_then(|arg| arg.to_str()) else { return Err(format!( - "choose an applet: client, tui, or server\n\n{}", + "choose an applet: {}\n\n{}", + applet_command_names(), dispatcher_help() )); }; @@ -67,7 +69,8 @@ pub fn dispatch_from_argv(argv: Vec) -> Result argv: applet_argv("irtt-server", &argv[2..]), }), _ => Err(format!( - "unknown applet '{command}'. Choose one of: client, tui, server\n\n{}", + "unknown applet '{command}'. Choose one of: {}\n\n{}", + applet_command_names(), dispatcher_help() )), } @@ -76,13 +79,47 @@ pub fn dispatch_from_argv(argv: Vec) -> Result pub fn dispatcher_help() -> String { let mut command = Command::new("irtt-rs") .about("IRTT-compatible multi-applet dispatcher") - .subcommand(Command::new("client").about("Run the stream client applet")) - .subcommand(Command::new("tui").about("Run the terminal UI applet")) - .subcommand(Command::new("server").about("Run the server applet")) - .after_help("Applet binary names: irtt-cli, irtt-tui, irtt-server"); + .subcommand(Command::new("client").about(client_applet_about())) + .subcommand(Command::new("tui").about(tui_applet_about())) + .subcommand(Command::new("server").about("Server applet is not available in this build")) + .after_help(dispatcher_after_help()); command.render_help().to_string() } +fn client_applet_about() -> &'static str { + if cfg!(feature = "client") { + "Run the stream client applet" + } else { + "Stream client applet is not available in this build" + } +} + +fn tui_applet_about() -> &'static str { + if cfg!(feature = "tui") { + "Run the terminal UI applet" + } else { + "Terminal UI applet is not available in this build" + } +} + +fn known_applet_binary_names() -> &'static str { + "irtt-rs, irtt-cli, irtt-tui, irtt-server" +} + +fn applet_command_names() -> &'static str { + "client, tui, or server" +} + +fn dispatcher_after_help() -> &'static str { + if cfg!(feature = "tui") { + "Recognized applet binary names: irtt-cli, irtt-tui, irtt-server" + } else if cfg!(feature = "client") { + "Recognized applet binary names: irtt-cli, irtt-tui, irtt-server\nOptional applet: irtt-tui requires the tui feature" + } else { + "Recognized applet binary names: irtt-cli, irtt-tui, irtt-server\nOptional applets: irtt-cli requires the client feature; irtt-tui requires the tui feature" + } +} + fn applet_basename(argv0: &str) -> &str { Path::new(argv0) .file_name() @@ -127,7 +164,11 @@ mod tests { }; assert!(help.contains("IRTT-compatible multi-applet dispatcher")); assert!(help.contains("client")); - assert!(help.contains("tui")); + if cfg!(feature = "tui") { + assert!(help.contains("Run the terminal UI applet")); + } else { + assert!(help.contains("Terminal UI applet is not available in this build")); + } assert!(help.contains("server")); } @@ -136,7 +177,9 @@ mod tests { let err = dispatch_from_argv(vec!["irtt-rs".into()]).unwrap_err(); assert!(err.contains("choose an applet")); assert!(err.contains("client")); - assert!(err.contains("tui")); + if cfg!(feature = "tui") { + assert!(err.contains("tui")); + } assert!(err.contains("server")); } @@ -171,7 +214,9 @@ mod tests { let err = dispatch_from_argv(vec!["/usr/bin/irtt-typo".into()]).unwrap_err(); assert!(err.contains("unknown applet name 'irtt-typo'")); assert!(err.contains("irtt-cli")); - assert!(err.contains("irtt-tui")); + if cfg!(feature = "tui") { + assert!(err.contains("irtt-tui")); + } assert!(err.contains("irtt-server")); } } diff --git a/crates/irtt-cli/src/cmd/client/args.rs b/crates/irtt-cli/src/cmd/client/args.rs index f215158..8b762f9 100644 --- a/crates/irtt-cli/src/cmd/client/args.rs +++ b/crates/irtt-cli/src/cmd/client/args.rs @@ -26,20 +26,38 @@ pub struct ClientArgs { #[command(flatten)] pub common: CommonClientArgs, - /// Event row renderer format. - #[arg(long, value_enum, default_value_t = OutputFormat::Table)] + /// Event row output format. + #[arg( + long, + value_enum, + 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." + )] pub format: OutputFormat, - /// Comma-separated event row columns. - #[arg(short = 'c', long, value_name = "COLUMNS")] + /// Comma-separated event row columns, or default/all. + #[arg( + short = 'c', + long, + value_name = "COLUMNS", + long_help = "Comma-separated event row columns, or default/all.\n\nThe default table columns are compact and hide echo_sent rows. Custom table columns include all event rows. Run --list-columns to see valid names." + )] pub columns: Option, - /// Print available event row columns and exit. - #[arg(long)] + /// List available event row columns and aliases, then exit. + #[arg( + long, + long_help = "List available event row columns and aliases, then exit.\n\nA server argument is not required when listing columns." + )] pub list_columns: bool, /// Header policy for table, CSV, and TSV output. - #[arg(long, value_enum, default_value_t = HeaderMode::Auto)] + #[arg( + long, + value_enum, + default_value_t = HeaderMode::Auto, + long_help = "Header policy for table, CSV, and TSV output.\n\nJSON Lines never prints a header." + )] pub header: HeaderMode, /// Include extra fields in table output and final summaries. diff --git a/crates/irtt-cli/src/cmd/client/output.rs b/crates/irtt-cli/src/cmd/client/output.rs index 536bfbc..63136a1 100644 --- a/crates/irtt-cli/src/cmd/client/output.rs +++ b/crates/irtt-cli/src/cmd/client/output.rs @@ -129,17 +129,19 @@ impl OutputConfig { writeln!(out, " {:<24} {}", column.name(), column.description()).unwrap(); } writeln!(out).unwrap(); + writeln!(out, "Aliases:").unwrap(); + writeln!(out, " receive_delay, receive_delay_us").unwrap(); + writeln!(out, " send_delay, send_delay_us").unwrap(); + writeln!(out, " server_processing").unwrap(); + writeln!(out, " server_received_count, server_received_window").unwrap(); + writeln!(out).unwrap(); + writeln!(out, "Special column sets:").unwrap(); writeln!( out, - "Aliases: rd=receive_delay, sd=send_delay, proc=server_processing, \ - server_received=server_received_count, server_window=server_received_window." - ) - .unwrap(); - writeln!( - out, - "Use --columns default for the format default, or --columns all for every column." + " default format default; compact for table, all columns for csv/tsv/jsonl" ) .unwrap(); + writeln!(out, " all every column").unwrap(); out } }