feat(cli): enhance help documentation and dynamic applet dispatch

This commit is contained in:
Nils Andreas Svee committed 2026-06-22 15:13:36 +07:00
1 parent f1d442d798
commit b33f01cfe1
4 files changed
+90 -24

No files matched your search

+3 -2
View File
@@ -134,8 +134,9 @@ warning_kind, message, event_wall_ns, client_send_wall_ns,
client_receive_wall_ns client_receive_wall_ns
``` ```
Aliases are accepted for readability: `receive_delay` for `rd`, `send_delay` Aliases are accepted for readability: `receive_delay` and `receive_delay_us`
for `sd`, `server_processing` for `proc`, `server_received_count` for 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`. `server_received`, and `server_received_window` for `server_window`.
The default table output favors readability with compact columns: The default table output favors readability with compact columns:
+53 -8
View File
@@ -41,13 +41,15 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
let basename = applet_basename(argv0); let basename = applet_basename(argv0);
if basename.starts_with("irtt-") && basename != "irtt-rs" { if basename.starts_with("irtt-") && basename != "irtt-rs" {
return Err(format!( 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 { let Some(command) = argv.get(1).and_then(|arg| arg.to_str()) else {
return Err(format!( return Err(format!(
"choose an applet: client, tui, or server\n\n{}", "choose an applet: {}\n\n{}",
applet_command_names(),
dispatcher_help() dispatcher_help()
)); ));
}; };
@@ -67,7 +69,8 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
argv: applet_argv("irtt-server", &argv[2..]), argv: applet_argv("irtt-server", &argv[2..]),
}), }),
_ => Err(format!( _ => 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() dispatcher_help()
)), )),
} }
@@ -76,13 +79,47 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
pub fn dispatcher_help() -> String { pub fn dispatcher_help() -> String {
let mut command = Command::new("irtt-rs") let mut command = Command::new("irtt-rs")
.about("IRTT-compatible multi-applet dispatcher") .about("IRTT-compatible multi-applet dispatcher")
.subcommand(Command::new("client").about("Run the stream client applet")) .subcommand(Command::new("client").about(client_applet_about()))
.subcommand(Command::new("tui").about("Run the terminal UI applet")) .subcommand(Command::new("tui").about(tui_applet_about()))
.subcommand(Command::new("server").about("Run the server applet")) .subcommand(Command::new("server").about("Server applet is not available in this build"))
.after_help("Applet binary names: irtt-cli, irtt-tui, irtt-server"); .after_help(dispatcher_after_help());
command.render_help().to_string() 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 { fn applet_basename(argv0: &str) -> &str {
Path::new(argv0) Path::new(argv0)
.file_name() .file_name()
@@ -127,7 +164,11 @@ mod tests {
}; };
assert!(help.contains("IRTT-compatible multi-applet dispatcher")); assert!(help.contains("IRTT-compatible multi-applet dispatcher"));
assert!(help.contains("client")); 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")); assert!(help.contains("server"));
} }
@@ -136,7 +177,9 @@ mod tests {
let err = dispatch_from_argv(vec!["irtt-rs".into()]).unwrap_err(); let err = dispatch_from_argv(vec!["irtt-rs".into()]).unwrap_err();
assert!(err.contains("choose an applet")); assert!(err.contains("choose an applet"));
assert!(err.contains("client")); assert!(err.contains("client"));
if cfg!(feature = "tui") {
assert!(err.contains("tui")); assert!(err.contains("tui"));
}
assert!(err.contains("server")); assert!(err.contains("server"));
} }
@@ -171,7 +214,9 @@ mod tests {
let err = dispatch_from_argv(vec!["/usr/bin/irtt-typo".into()]).unwrap_err(); let err = dispatch_from_argv(vec!["/usr/bin/irtt-typo".into()]).unwrap_err();
assert!(err.contains("unknown applet name 'irtt-typo'")); assert!(err.contains("unknown applet name 'irtt-typo'"));
assert!(err.contains("irtt-cli")); assert!(err.contains("irtt-cli"));
if cfg!(feature = "tui") {
assert!(err.contains("irtt-tui")); assert!(err.contains("irtt-tui"));
}
assert!(err.contains("irtt-server")); assert!(err.contains("irtt-server"));
} }
} }
+25 -7
View File
@@ -26,20 +26,38 @@ pub struct ClientArgs {
#[command(flatten)] #[command(flatten)]
pub common: CommonClientArgs, pub common: CommonClientArgs,
/// Event row renderer format. /// Event row output format.
#[arg(long, value_enum, default_value_t = OutputFormat::Table)] #[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, pub format: OutputFormat,
/// Comma-separated event row columns. /// Comma-separated event row columns, or default/all.
#[arg(short = 'c', long, value_name = "COLUMNS")] #[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<String>, pub columns: Option<String>,
/// Print available event row columns and exit. /// List available event row columns and aliases, then exit.
#[arg(long)] #[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, pub list_columns: bool,
/// Header policy for table, CSV, and TSV output. /// 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, pub header: HeaderMode,
/// Include extra fields in table output and final summaries. /// Include extra fields in table output and final summaries.
+9 -7
View File
@@ -129,17 +129,19 @@ impl OutputConfig {
writeln!(out, " {:<24} {}", column.name(), column.description()).unwrap(); writeln!(out, " {:<24} {}", column.name(), column.description()).unwrap();
} }
writeln!(out).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!( writeln!(
out, out,
"Aliases: rd=receive_delay, sd=send_delay, proc=server_processing, \ " default format default; compact for table, all columns for csv/tsv/jsonl"
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."
) )
.unwrap(); .unwrap();
writeln!(out, " all every column").unwrap();
out out
} }
} }