Refactor help output formatting for improved clarity and readability
- Updated subcommand help to display one summary line per subcommand, followed by detailed flag descriptions. - Added individual alignment for flag columns across subcommands for consistent presentation. - Replaced compact inline summaries with a more structured format to prevent line wrapping and improve visual layout. - Enhanced flag argument handling by introducing `value
This commit is contained in:
@@ -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<bool>,
|
||||
|
||||
/// Loopback volume: fraction (0.8) or percent (80).
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, value_name = "PCT")]
|
||||
pub volume: Option<f32>,
|
||||
|
||||
/// 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<f32>,
|
||||
}
|
||||
|
||||
|
||||
116
src/main.rs
116
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. `<name> [-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::<Vec<_>>().join("/");
|
||||
let subcommands: Vec<&clap::Command> = cmd.get_subcommands().filter(|s| s.get_name() != "help").collect();
|
||||
let flag_rows_by_cmd: Vec<Vec<(String, String)>> = 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();
|
||||
(s.get_name().to_string(), compact_args(s), about, aliases)
|
||||
})
|
||||
.collect();
|
||||
print_table(&rows);
|
||||
let aliases = s.get_visible_aliases().collect::<Vec<_>>().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() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `<name>` for each positional, `[-x/--long <VALUE>]` or `[-x/--long]` for
|
||||
/// each flag, in one space-joined line - a compact stand-in for the
|
||||
/// `[OPTIONS] <NAME>` 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<String> = cmd
|
||||
.get_positionals()
|
||||
/// `<name>` 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();
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
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
|
||||
/// `(flag display, help text)` for each of `cmd`'s non-positional, non-help
|
||||
/// args, e.g. `("-i, --input <APP[:MEDIA]>", "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(|| arg.get_id().as_str().to_uppercase());
|
||||
format!("[{flag} <{value}>]")
|
||||
.unwrap_or_else(|| a.get_id().as_str().to_uppercase());
|
||||
format!("{flag} <{value}>")
|
||||
} else {
|
||||
format!("[{flag}]")
|
||||
flag
|
||||
};
|
||||
parts.push(ui::yellow(&group));
|
||||
}
|
||||
parts.join(" ")
|
||||
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
|
||||
/// `-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<String> {
|
||||
match (arg.get_short(), arg.get_long()) {
|
||||
@@ -153,24 +177,6 @@ fn flag_names(arg: &clap::Arg) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
Reference in New Issue
Block a user