diff --git a/src/cli.rs b/src/cli.rs index 8d69c9f..00ef73e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -71,15 +71,15 @@ pub struct EditArgs { pub name: String, /// Enable or disable the self-monitor loopback. - #[arg(short, long)] + #[arg(short, long, value_name = "BOOL")] pub loopback: Option, /// Loopback volume: fraction (0.8) or percent (80). - #[arg(short, long)] + #[arg(short, long, value_name = "PCT")] pub volume: Option, /// Mixed-in source volume: fraction (0.8) or percent (80). - #[arg(long = "source-volume", visible_alias = "sv")] + #[arg(long = "source-volume", visible_alias = "sv", value_name = "PCT")] pub source_volume: Option, } diff --git a/src/main.rs b/src/main.rs index 7017510..b823087 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,9 +70,11 @@ fn wants_top_level_help(args: &[String]) -> bool { } } -/// Prints one screen of help: every subcommand's positional args and flags -/// summarized inline (e.g. ` [-l/--loopback]`), so nothing requires -/// drilling into a subcommand's own `--help` just to see what it takes. +/// Prints one screen of help: one short summary line per subcommand (name, +/// positional args, about), followed by an indented line per flag with its +/// own help text - so nothing requires drilling into a subcommand's own +/// `--help` just to see what it takes, without cramming everything about a +/// subcommand onto a single (often terminal-wrapping) line. fn print_full_help() { let mut cmd = Cli::command(); cmd.build(); // resolve default value names etc. before introspecting @@ -85,16 +87,37 @@ fn print_full_help() { println!(); println!("{}", ui::blue("Commands:")); - let rows: Vec<(String, String, String, String)> = cmd - .get_subcommands() - .filter(|s| s.get_name() != "help") - .map(|s| { - let aliases = s.get_visible_aliases().collect::>().join("/"); - let about = s.get_about().map(|a| a.to_string()).unwrap_or_default(); - (s.get_name().to_string(), compact_args(s), about, aliases) - }) - .collect(); - print_table(&rows); + let subcommands: Vec<&clap::Command> = cmd.get_subcommands().filter(|s| s.get_name() != "help").collect(); + let flag_rows_by_cmd: Vec> = subcommands.iter().map(|s| flag_rows(s)).collect(); + + let name_w = subcommands.iter().map(|s| s.get_name().len()).max().unwrap_or(0); + let pos_w = subcommands.iter().map(|s| visual_width(&positional_args(s))).max().unwrap_or(0); + // One width across every subcommand's flags, not just its own, so the + // help-text column lines up no matter which command it's under. + let flag_w = flag_rows_by_cmd.iter().flatten().map(|(f, _)| visual_width(f)).max().unwrap_or(0); + + for (i, s) in subcommands.iter().enumerate() { + let name = s.get_name(); + let positionals = positional_args(s); + let about = s.get_about().map(|a| a.to_string()).unwrap_or_default(); + let aliases = s.get_visible_aliases().collect::>().join("/"); + + print!(" {name:name_w$} {} {about}", pad_visual(&positionals, pos_w)); + if !aliases.is_empty() { + print!(" | {aliases}"); + } + println!(); + + let rows = &flag_rows_by_cmd[i]; + for (flag, help) in rows { + println!(" {} {help}", pad_visual(flag, flag_w)); + } + // Flagless commands stay a tight single line; only the multi-line + // (flag-bearing) ones get a blank line to separate them visually. + if !rows.is_empty() && i + 1 < subcommands.len() { + println!(); + } + } println!(); println!("{}", ui::blue("Options:")); @@ -109,40 +132,41 @@ fn print_full_help() { } } -/// `` for each positional, `[-x/--long ]` or `[-x/--long]` for -/// each flag, in one space-joined line - a compact stand-in for the -/// `[OPTIONS] ` a normal usage line would collapse this to. -/// -/// Colors match the original fish script's `vmic_usage_line`: positional -/// `<...>` args in cyan, optional `[...]` flag groups (whole bracket, -/// including any value placeholder inside) in yellow. -fn compact_args(cmd: &clap::Command) -> String { - let mut parts: Vec = cmd - .get_positionals() +/// `` for each positional arg of `cmd`, space-joined and colored cyan +/// (matching the original fish script's `vmic_usage_line` convention). +fn positional_args(cmd: &clap::Command) -> String { + cmd.get_positionals() .map(|a| ui::cyan(&format!("<{}>", a.get_id().as_str()))) - .collect(); - - for arg in cmd.get_arguments() { - if arg.is_positional() || arg.get_id().as_str() == "help" { - continue; - } - let Some(flag) = flag_names(arg) else { continue }; - let group = if matches!(arg.get_action(), clap::ArgAction::Set | clap::ArgAction::Append) { - let value = arg - .get_value_names() - .and_then(|v| v.first()) - .map(|v| v.to_string()) - .unwrap_or_else(|| arg.get_id().as_str().to_uppercase()); - format!("[{flag} <{value}>]") - } else { - format!("[{flag}]") - }; - parts.push(ui::yellow(&group)); - } - parts.join(" ") + .collect::>() + .join(" ") } -/// `-x, --long` / `-x` / `--long` for a non-positional arg, or `None` for +/// `(flag display, help text)` for each of `cmd`'s non-positional, non-help +/// args, e.g. `("-i, --input ", "Move matching sink-inputs +/// into this vmic's sink")`. The flag display is colored yellow, matching +/// the original fish script's convention for optional flags. +fn flag_rows(cmd: &clap::Command) -> Vec<(String, String)> { + cmd.get_arguments() + .filter(|a| !a.is_positional() && a.get_id().as_str() != "help") + .filter_map(|a| { + let flag = flag_names(a)?; + let display = if matches!(a.get_action(), clap::ArgAction::Set | clap::ArgAction::Append) { + let value = a + .get_value_names() + .and_then(|v| v.first()) + .map(|v| v.to_string()) + .unwrap_or_else(|| a.get_id().as_str().to_uppercase()); + format!("{flag} <{value}>") + } else { + flag + }; + let help = a.get_help().map(|h| h.to_string()).unwrap_or_default(); + Some((ui::yellow(&display), help)) + }) + .collect() +} + +/// `-x/--long` / `-x` / `--long` for a non-positional arg, or `None` for /// one with no visible flag at all. fn flag_names(arg: &clap::Arg) -> Option { match (arg.get_short(), arg.get_long()) { @@ -153,24 +177,6 @@ fn flag_names(arg: &clap::Arg) -> Option { } } -/// Prints `(name, args, about, aliases)` rows with the name and args -/// columns padded to their widest entry, so the `about` column lines up. -/// `args` carries ANSI color codes (see `compact_args`), so padding by -/// `.len()`/`{:width$}` would count invisible escape bytes as columns and -/// misalign everything - `pad_visual` pads by visible width instead. -/// `aliases`, if non-empty, trails after `| `. -fn print_table(rows: &[(String, String, String, String)]) { - let name_w = rows.iter().map(|(n, ..)| n.len()).max().unwrap_or(0); - let args_w = rows.iter().map(|(_, a, ..)| visual_width(a)).max().unwrap_or(0); - for (name, args, about, aliases) in rows { - print!(" {name:name_w$} {} {about}", pad_visual(args, args_w)); - if !aliases.is_empty() { - print!(" | {aliases}"); - } - println!(); - } -} - /// Number of visible columns in `s`, skipping any `\x1b[...m` ANSI SGR /// escape sequences it contains. fn visual_width(s: &str) -> usize {