From 0a1a7f81743ee6d67de3b10cf632c05d557858dc Mon Sep 17 00:00:00 2001 From: drei Date: Tue, 26 May 2026 10:54:18 -0500 Subject: [PATCH] CLI grouped help: catch `rift help`; style headers + names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups on the grouped-help renderer: 1. `rift help` was still hitting clap's built-in `help` subcommand, which prints the flat clap-default output. Extend the argv-prefix intercept to catch `help` alongside `-h` / `--help`. `rift help ` still falls through to clap (correctly — nested help is what clap handles well). 2. Styling pass via the `console` crate (already a dep): - Group headers (Account: / Resources: / Billing: / Meta:): yellow, bold — pop-out so the grouping is the visual anchor. - Command names (init, links, …): cyan, bold — matches the convention from gh, kubectl, fly. - "Usage:" / "Options:": bold (no color) — clap's default look. - "Aliases: …" hint and footer "Run …": dim — they're context, not action. `console::colors_enabled()` gates the entire style block: when stdout isn't a TTY or `NO_COLOR` is set, every style collapses to plain text so piped / file / CI output stays clean. Padding bug avoided: the styled-string `.len()` includes ANSI escape bytes, so the alignment uses the raw name length and prepends spaces manually. Pulled the per-row rendering into a `print_row` helper since both the group loop and the "Other:" fallback do the same thing. Co-Authored-By: Claude Opus 4.7 (1M context) --- client/cli/src/lib.rs | 86 ++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/client/cli/src/lib.rs b/client/cli/src/lib.rs index 680d9b07..7c2e9780 100644 --- a/client/cli/src/lib.rs +++ b/client/cli/src/lib.rs @@ -286,14 +286,15 @@ enum AnalyticsCommand { } pub async fn run() -> Result<(), CliError> { - // Intercept the root-help cases (`rift`, `rift -h`, `rift --help`) so we - // can render groups instead of clap's flat `Commands:` block. Anything - // deeper — `rift links --help`, `rift team rm -h` — falls through to - // clap's auto-help, which is fine since nested help is already scoped. + // Intercept the root-help cases (`rift`, `rift -h`, `rift --help`, + // `rift help`) so we can render groups instead of clap's flat + // `Commands:` block. Anything deeper — `rift links --help`, + // `rift help links`, `rift team rm -h` — falls through to clap's + // auto-help, which is fine since nested help is already scoped. let argv: Vec = std::env::args().collect(); let bare_or_root_help = match argv.as_slice() { [_] => true, - [_, flag] if flag == "-h" || flag == "--help" => true, + [_, flag] if flag == "-h" || flag == "--help" || flag == "help" => true, _ => false, }; if bare_or_root_help { @@ -467,18 +468,41 @@ const GROUPS: &[(&str, std::ops::RangeInclusive)] = &[ fn print_grouped_help() { use clap::CommandFactory; + use console::Style; + let cmd = Cli::command(); let bin = cmd.get_name(); + // `console::colors_enabled()` honors `NO_COLOR`, `CLICOLOR_FORCE`, + // and the TTY check — styling collapses to plain text when piped + // to a file or run under CI. Pre-compute the styles once so we don't + // re-check inside hot loops. + let on = console::colors_enabled(); + let bold = if on { + Style::new().bold() + } else { + Style::new() + }; + let name_style = if on { + Style::new().cyan().bold() + } else { + Style::new() + }; + let group_style = if on { + Style::new().yellow().bold() + } else { + Style::new() + }; + let dim = if on { Style::new().dim() } else { Style::new() }; + if let Some(about) = cmd.get_about() { println!("{about}"); println!(); } - println!("Usage: {bin} "); + println!("{} {bin} ", bold.apply_to("Usage:")); - // Pad the command-name column to the widest name across all groups so - // descriptions line up vertically through the entire help output, not - // just within each group. + // Pad on the *raw* name length, not the styled string's byte length — + // ANSI escape codes inflate `.len()` and would skew alignment. let name_col = cmd .get_subcommands() .map(|s| s.get_name().len()) @@ -496,15 +520,14 @@ fn print_grouped_help() { continue; } println!(); - println!("{label}:"); + println!("{}", group_style.apply_to(format!("{label}:"))); for sub in subs { - let name = sub.get_name(); - let about = sub.get_about().map(|s| s.to_string()).unwrap_or_default(); - println!(" {name: --help` for details on a specific command."); + println!( + "{}", + dim.apply_to(format!( + "Run `{bin} --help` for details on a specific command." + )), + ); +} + +/// Render one ` ` row with the name padded to a fixed +/// column. Pulled out because each group's loop does the same thing and +/// the ANSI-aware padding is the kind of detail worth in one place. +fn print_row(sub: &clap::Command, name_style: &console::Style, name_col: usize) { + let raw_name = sub.get_name(); + let styled = name_style.apply_to(raw_name); + let pad = " ".repeat(name_col.saturating_sub(raw_name.len())); + let about = sub.get_about().map(|s| s.to_string()).unwrap_or_default(); + println!(" {styled}{pad} {about}"); }