From 40e1e7c40df993e7a1c1c5202351744d118453e8 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:07:22 +0000 Subject: [PATCH] feat(help): wrap the short help page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `-h` never wrapped. A description longer than the terminal left room for simply ran off the end, and mise's `-j, --jobs` — 63 characters of help plus `[default: 8] [env: MISE_JOBS]` — came out as one 110-column line that a reader had to scroll or reflow to finish. The wide page has wrapped since it existed; `-h` is the form most people type. The narrow page now wraps into the same column, with continuations under the description, and falls back to the block layout on the same terms the wide page does: a description with breaks of its own, or a column so deep there is no room left to say anything. Annotations join the description *before* the wrap rather than being appended after it. Appending would put `[env: …]` past the column the wrapping had just brought the text back into, which is the shape the old line had. One consequence worth stating: an entry with no description but some annotation now carries it in the column rather than flush against the usage, since that is where the text goes. Arguments are laid out for the page being rendered rather than reusing the model's pass, which computed the wide page's column and preferred the long description — wrong text in the wrong width for `-h`. The three renderers agree byte for byte over mise's 211 pages, the fleet, and the Go conformance suite. This also settles two places where they had quietly disagreed for want of a spec exercising them: a deprecation label now trails the annotations rather than sitting between the description and them, and an argument's label stays off the narrow page, which is what usage-lib always did and what the other two now do. --- argv/src/help.rs | 100 +++++++-- conformance/tests/arg_group.rs | 13 +- conformance/tests/combinations_env.rs | 32 ++- conformance/tests/metadata.rs | 6 +- go/argv/page.go | 81 +++++-- lib/src/docs/cli/mod.rs | 197 ++++++++++++++++-- .../cli/templates/spec_template_short.tera | 56 ++--- lib/src/docs/models.rs | 8 + 8 files changed, 404 insertions(+), 89 deletions(-) diff --git a/argv/src/help.rs b/argv/src/help.rs index d673d3c02..6e7617a8f 100644 --- a/argv/src/help.rs +++ b/argv/src/help.rs @@ -1106,6 +1106,9 @@ fn short_sections( .filter(|(flag, _)| !flag.hide_short_help) .collect(); let mut sections = Sections::default(); + // The narrow page wraps too. Its descriptions used to run off the end of the terminal, + // which the wide page has never done — and `-h` is the form most people type. + let width = terminal_width(meta); let out = &mut sections.about; // Text the command puts above everything else, and below it. The short form has only the @@ -1144,7 +1147,7 @@ fn short_sections( &mut sections.commands, &path[1.min(path.len())..], meta, - terminal_width(meta), + width, false, ); } @@ -1196,18 +1199,9 @@ fn short_sections( ); return; } - match a.help.filter(|h| !h.trim().is_empty()) { - Some(help) => { - let _ = write!(out, " {usage: { - let _ = write!(out, " {usage}"); - } - } let environment = inline_environment_notes(a.hide_env, a.env_fallback, a.deprecated_env); - annotations( - out, + let notes = inline_annotations( if a.hide_possible_values { &[] } else { @@ -1218,6 +1212,14 @@ fn short_sections( if a.hide_default_value { &[] } else { a.default }, None, ); + entry( + out, + &usage, + with_annotations(a.help, notes).as_deref(), + arg_col, + width, + false, + ); }, ); // One column over *both* lists, so the two sections read as one table with a rule through @@ -1249,19 +1251,10 @@ fn short_sections( flag_notes(out, f, 4); return; } - match f.help.filter(|h| !h.trim().is_empty()) { - Some(help) => { - let _ = write!(out, " {usage: { - let _ = write!(out, " {usage}"); - } - } let deprecation = deprecation_label(f.deprecated, f.deprecated_warn_at, f.deprecated_remove_at); let environment = inline_environment_notes(f.hide_env, f.env_fallback, f.deprecated_env); - annotations( - out, + let notes = inline_annotations( if f.hide_possible_values { &[] } else { @@ -1272,6 +1265,14 @@ fn short_sections( if f.hide_default_value { &[] } else { f.default }, deprecation.as_deref(), ); + entry( + out, + &usage, + with_annotations(f.help, notes).as_deref(), + flag_col, + width, + false, + ); }; split_groups_section( SectionSink { @@ -1791,6 +1792,63 @@ fn annotations( out.push('\n'); } +/// The same annotations as one string, for an entry that carries them in its text. +/// +/// [`annotations`] writes them straight out, which the flattened sections still want. The +/// narrow layout cannot: its text has to be complete before it is wrapped. +fn inline_annotations( + choices: &[&str], + env: Option<&str>, + environment: Option<&str>, + default: &[&str], + suffix: Option<&str>, +) -> Option { + let mut out = String::new(); + let mut push = |part: &str| { + if !out.is_empty() { + out.push(' '); + } + out.push_str(part); + }; + if !choices.is_empty() { + push(&format!("[{}]", choices.join(", "))); + } + if let Some(env) = env { + push(&format!("[env: {env}]")); + } + if let Some(environment) = environment { + push(environment); + } + if !default.is_empty() { + push(&format!("(default: {})", default.join(", "))); + } + if let Some(suffix) = suffix { + push(suffix); + } + (!out.is_empty()).then_some(out) +} + +/// A narrow entry's description with its annotations joined on. +/// +/// The wide layout gives each annotation a line of its own; the narrow one has no room for +/// that, so they ride along with the description — and they have to be joined *before* it is +/// wrapped, or an entry with a long description keeps its `[env: …]` out past the column the +/// wrapping was supposed to bring the text back into. +/// +/// A description with nothing to add to it is borrowed rather than copied, which is most of +/// them. +fn with_annotations<'a>( + help: Option<&'a str>, + annotations: Option, +) -> Option> { + match (summarize(help), annotations) { + (Some(help), None) => Some(Cow::Borrowed(help)), + (None, Some(annotations)) => Some(Cow::Owned(annotations)), + (Some(help), Some(annotations)) => Some(Cow::Owned(format!("{help} {annotations}"))), + (None, None) => None, + } +} + /// How a usage line writes a flag: its first long form, or its short if that is all it has. /// /// Shared with the diagnostics for the same reason as [`arg_usage`], and gated with them: under diff --git a/conformance/tests/arg_group.rs b/conformance/tests/arg_group.rs index a9ef84260..e961acf65 100644 --- a/conformance/tests/arg_group.rs +++ b/conformance/tests/arg_group.rs @@ -174,9 +174,16 @@ fn case_insensitive_group_values_match_relationships_the_same_way_they_parse() { fn a_value_carrying_group_member_reaches_help_and_the_spec() { let help = help::render(ValuedGroup::spec(), ValuedGroup::spec().root.cmd, false).expect("a page"); - assert!(help.contains("--migrate "), "{help}"); - assert!(help.contains("[prettier, biome]"), "{help}"); - assert!(help.contains("--stdin-filepath "), "{help}"); + // The narrow page wraps, so an annotation can be split across two lines. What this test is + // about is that the choices reach the page at all, so it reads the page with the layout + // collapsed rather than pinning where the break happens to fall. + let flattened = help.split_whitespace().collect::>().join(" "); + assert!(flattened.contains("--migrate "), "{help}"); + assert!(flattened.contains("[prettier, biome]"), "{help}"); + assert!( + flattened.contains("--stdin-filepath "), + "{help}" + ); let kdl = ValuedGroup::to_kdl(); assert!( diff --git a/conformance/tests/combinations_env.rs b/conformance/tests/combinations_env.rs index 113225236..96e0478c4 100644 --- a/conformance/tests/combinations_env.rs +++ b/conformance/tests/combinations_env.rs @@ -143,9 +143,18 @@ fn ordered_environment_names_use_first_set_value() { .expect("root page"); let fallback_notes = " [env fallback: USAGE_ENV_FALLBACK_A] [env fallback: USAGE_ENV_FALLBACK_B] [deprecated env: USAGE_ENV_DEPRECATED]"; let ordered_notes = format!("{fallback_notes} (default: fallback)"); - assert!(typed_help.contains(&ordered_notes), "{typed_help}"); + // Read with the layout collapsed: the narrow page wraps, so a run of notes this long is + // split across lines. What is under test is that they arrive in this order, not where the + // break falls. + assert!( + flattened(&typed_help).contains(&ordered_notes), + "{typed_help}" + ); let reference_help = usage::docs::cli::render_help(&spec, &spec.cmd, false); - assert!(reference_help.contains(&ordered_notes), "{reference_help}"); + assert!( + flattened(&reference_help).contains(&ordered_notes), + "{reference_help}" + ); unsafe { std::env::remove_var("USAGE_ENV_DEPRECATED") }; } @@ -200,9 +209,18 @@ fn positional_environment_names_round_trip_and_use_first_set_value() { .expect("root page"); let fallback_notes = " [env fallback: USAGE_ARG_ENV_FALLBACK_A] [env fallback: USAGE_ARG_ENV_FALLBACK_B] [deprecated env: USAGE_ARG_ENV_DEPRECATED]"; let ordered_notes = format!("{fallback_notes} (default: fallback)"); - assert!(typed_help.contains(&ordered_notes), "{typed_help}"); + // Read with the layout collapsed: the narrow page wraps, so a run of notes this long is + // split across lines. What is under test is that they arrive in this order, not where the + // break falls. + assert!( + flattened(&typed_help).contains(&ordered_notes), + "{typed_help}" + ); let reference_help = usage::docs::cli::render_help(&spec, &spec.cmd, false); - assert!(reference_help.contains(&ordered_notes), "{reference_help}"); + assert!( + flattened(&reference_help).contains(&ordered_notes), + "{reference_help}" + ); for name in [ "USAGE_ARG_ENV_FALLBACK_A", @@ -228,3 +246,9 @@ fn flattened_next_line_help_indents_environment_notes() { assert!(page.contains(note), "{page}"); } } + +/// A page with its wrapping taken back out, for a test about what it says rather than how it +/// is laid out. +fn flattened(page: &str) -> String { + page.split_whitespace().collect::>().join(" ") +} diff --git a/conformance/tests/metadata.rs b/conformance/tests/metadata.rs index c67c467b0..e4eeef6bf 100644 --- a/conformance/tests/metadata.rs +++ b/conformance/tests/metadata.rs @@ -60,8 +60,12 @@ fn deprecation_metadata_survives_the_typed_spec() { fn deprecation_renders_inline_and_in_flattened_command_sections() { let spec = DeprecatedCli::spec(); let page = usage_argv::help::render(spec, spec.root.cmd, false).unwrap(); + // A flag with no description carries its label in the description column, so what + // separates the two is the column rather than a single space. Read with the layout + // collapsed: the label reaching the flag's row is the point, not the width of the gap. + let flattened = page.split_whitespace().collect::>().join(" "); assert!( - page.contains("--old [deprecated: use --new; warns at 6.2; removed at 7.0]"), + flattened.contains("--old [deprecated: use --new; warns at 6.2; removed at 7.0]"), "{page}" ); diff --git a/go/argv/page.go b/go/argv/page.go index a7c7fda94..f4cbf2ca2 100644 --- a/go/argv/page.go +++ b/go/argv/page.go @@ -149,12 +149,7 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s longAnnotations(w, h, true) return } - if help := helpText(h); help != "" { - w.WriteString(" " + pad(usage, argCol) + " " + help) - } else { - w.WriteString(" " + usage) - } - annotations(w, h, true) + entry(w, usage, withAnnotations(shortSummary(h), inlineAnnotations(h, true, false)), argCol, false) }) own, inherited := ownAndGlobal(chain, help) @@ -174,7 +169,7 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s flagCol = n } } - entry := func(w *strings.Builder, f shownFlag) { + flagEntry := func(w *strings.Builder, f shownFlag) { h := help.Lookup(f.key) if f.supplied != "" { // A flag the parser supplies has no table entry; its help is fixed. @@ -185,12 +180,7 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s } return } - if text := f.suppliedHelp; text != "" { - w.WriteString(" " + pad(f.usage, flagCol) + " " + text) - } else { - w.WriteString(" " + f.usage) - } - w.WriteString("\n") + entry(w, f.usage, f.suppliedHelp, flagCol, false) return } if nextLineHelp { @@ -201,12 +191,7 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s longAnnotations(w, h, true) return } - if text := helpText(h); text != "" { - w.WriteString(" " + pad(f.usage, flagCol) + " " + text) - } else { - w.WriteString(" " + f.usage) - } - annotations(w, h, true) + entry(w, f.usage, withAnnotations(shortSummary(h), inlineAnnotations(h, true, true)), flagCol, false) } groupsSection(§ions.flags, §ions.ungroupedFlags, §ions.groupedFlags, "Flags", len(own), func(i int) string { @@ -215,13 +200,13 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s } return headingOf(help, own[i].key) }, - func(w *strings.Builder, i int) { entry(w, own[i]) }) + func(w *strings.Builder, i int) { flagEntry(w, own[i]) }) // After the command's own, and under a heading that says where they came // from: a global belongs to the program, not to this command, and a reader // should be able to see that. groupsSection(§ions.flags, §ions.ungroupedFlags, §ions.groupedFlags, "Global flags", len(inherited), func(int) string { return "" }, - func(w *strings.Builder, i int) { entry(w, inherited[i]) }) + func(w *strings.Builder, i int) { flagEntry(w, inherited[i]) }) if meta != nil && meta.FlattenHelp { flatCommandsShort(§ions.flattened, path[min(1, len(path)):], cmd, help, nextLineHelp) } @@ -539,6 +524,60 @@ func annotations(out *strings.Builder, h *Help, withDefault bool) { out.WriteString("\n") } +// inlineAnnotations is the same annotations as one string, for an entry that carries +// them in its text rather than writing them out. The narrow layout needs its text +// complete before it is wrapped. +func inlineAnnotations(h *Help, withDefault, withDeprecation bool) string { + if h == nil { + return "" + } + parts := []string{} + if !h.HidePossibleValues && len(h.Choices) > 0 { + parts = append(parts, "["+strings.Join(h.Choices, ", ")+"]") + } + if !h.HideEnv && h.Env != "" { + parts = append(parts, "[env: "+h.Env+"]") + } + if !h.HideEnv { + for _, env := range h.EnvFallback { + parts = append(parts, "[env fallback: "+env+"]") + } + for _, env := range h.DeprecatedEnv { + parts = append(parts, "[deprecated env: "+env+"]") + } + } + if withDefault && !h.HideDefaultValue && len(h.Default) > 0 { + parts = append(parts, "(default: "+strings.Join(h.Default, ", ")+")") + } + // Last, as it is everywhere else a row carries one. + if withDeprecation { + if label := deprecationLabel(h); label != "" { + parts = append(parts, label) + } + } + return strings.Join(parts, " ") +} + +// shortSummary is the description a narrow entry shows, with nothing appended. +func shortSummary(h *Help) string { + if h == nil { + return "" + } + return trimEnd(h.Short) +} + +// withAnnotations joins a description to its annotations. Either may be empty. +func withAnnotations(help, annotations string) string { + switch { + case help == "": + return annotations + case annotations == "": + return help + default: + return help + " " + annotations + } +} + func helpText(h *Help) string { if h == nil { return "" diff --git a/lib/src/docs/cli/mod.rs b/lib/src/docs/cli/mod.rs index 1f2f3628c..88bce2c18 100644 --- a/lib/src/docs/cli/mod.rs +++ b/lib/src/docs/cli/mod.rs @@ -68,9 +68,18 @@ pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String { .map(|f| f.display_usage.as_str()), ); for group in &mut docs_cmd.flag_groups { - lay_out(&mut group.items, width, col); + lay_out(&mut group.items, width, col, long); + } + lay_out(&mut inherited, width, col, long); + + // The arguments get their own column, laid out here for the page being rendered rather than + // taken from the model's — which is the long page's, and would put a long description in the + // short page's column unwrapped. + let arg_col = + crate::docs::layout::max_usage_width(docs_cmd.args.iter().map(|a| a.usage.as_str())); + for group in &mut docs_cmd.arg_groups { + lay_out_args(&mut group.items, width, arg_col, long); } - lay_out(&mut inherited, width, col); // The command list, laid out the way the flag list is: one column for the whole page, the // name in it, and everything else — summary, aliases, deprecation — trailing as text that @@ -359,22 +368,182 @@ fn supplied_flags( /// command's own flags and the ones it inherits. The width is not only padding — a wrapped /// description is indented to sit under itself — so it cannot be decided per section and then /// shared. -fn lay_out(flags: &mut [crate::docs::models::SpecFlag], terminal_width: usize, col: usize) { +fn lay_out( + flags: &mut [crate::docs::models::SpecFlag], + terminal_width: usize, + col: usize, + long: bool, +) { for flag in flags { flag.usage_col_width = col; - flag.help_rendered = None; - flag.help_is_multiline = false; - let help = flag.help_long.as_deref().or(flag.help.as_deref()); - if let Some(help) = help { - let (rendered, is_multiline) = - crate::docs::layout::render_help_text(help, terminal_width, col); - // An empty rendering is how this says "use the block layout instead". - if !rendered.is_empty() { - flag.help_rendered = Some(rendered); - flag.help_is_multiline = is_multiline; - } + let text = if long { + flag.help_long + .as_deref() + .or(flag.help.as_deref()) + .map(str::to_string) + } else { + with_annotations(flag.help.as_deref(), flag_annotations(flag)) + }; + wrap_into( + text, + terminal_width, + col, + &mut flag.row, + &mut flag.help_rendered, + &mut flag.help_is_multiline, + ); + } +} + +/// The same pass over a command's arguments. +/// +/// `SpecCommand::from` already made one, but it made the long page's — the short page prefers +/// the short description and carries the annotations in the text — so the page it is actually +/// rendering gets the last word. +fn lay_out_args( + args: &mut [crate::docs::models::SpecArg], + terminal_width: usize, + col: usize, + long: bool, +) { + for arg in args { + arg.usage_col_width = col; + let text = if long { + arg.help_long + .as_deref() + .or(arg.help.as_deref()) + .map(str::to_string) + } else { + with_annotations(arg.help.as_deref(), arg_annotations(arg)) + }; + wrap_into( + text, + terminal_width, + col, + &mut arg.row, + &mut arg.help_rendered, + &mut arg.help_is_multiline, + ); + } +} + +/// Fit one entry's text to the column, and say which layout it wants. +/// +/// `row` is the text as composed and `help_rendered` the same text wrapped; an empty wrapping +/// is how [`crate::docs::layout::render_help_text`] says "no room, put it underneath instead", +/// which is the case the template reads `row` for. +fn wrap_into( + text: Option, + terminal_width: usize, + col: usize, + row: &mut Option, + help_rendered: &mut Option, + help_is_multiline: &mut bool, +) { + *row = None; + *help_rendered = None; + *help_is_multiline = false; + let Some(text) = text else { return }; + let (rendered, is_multiline) = + crate::docs::layout::render_help_text(&text, terminal_width, col); + if !rendered.is_empty() { + *help_rendered = Some(rendered); + *help_is_multiline = is_multiline; + } + *row = Some(text); +} + +/// A short entry's description with its annotations joined on. +/// +/// The wide layout gives each annotation a line of its own; the narrow one has no room for +/// that, so they ride along with the description — and they have to be joined *before* it is +/// wrapped, or an entry with a long description keeps its `[env: …]` out past the column where +/// the wrapping was supposed to bring the text back. +fn with_annotations(help: Option<&str>, annotations: Vec) -> Option { + let mut parts = Vec::new(); + if let Some(help) = summarize(help) { + parts.push(help.to_string()); + } + parts.extend(annotations); + (!parts.is_empty()).then(|| parts.join(" ")) +} + +/// What a flag's short entry says about it beyond its description. +fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec { + let mut parts = value_annotations( + flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()), + flag.hide_possible_values, + flag.env.as_deref(), + flag.hide_env, + &flag.env_fallback, + &flag.deprecated_env, + &flag.default, + flag.hide_default_value, + ); + if let Some(label) = deprecation_label( + flag.deprecated.as_deref(), + flag.deprecated_warn_at.as_deref(), + flag.deprecated_remove_at.as_deref(), + ) { + parts.push(label); + } + parts +} + +/// The same for an argument, which carries no deprecation on the narrow page. +fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec { + value_annotations( + arg.choices.as_ref(), + arg.hide_possible_values, + arg.env.as_deref(), + arg.hide_env, + &arg.env_fallback, + &arg.deprecated_env, + &arg.default, + arg.hide_default_value, + ) +} + +/// What can be said about a value, in the order the narrow page says it. +#[allow(clippy::too_many_arguments)] +fn value_annotations( + choices: Option<&crate::SpecChoices>, + hide_possible_values: bool, + env: Option<&str>, + hide_env: bool, + env_fallback: &[String], + deprecated_env: &[String], + default: &[String], + hide_default_value: bool, +) -> Vec { + let mut parts = Vec::new(); + if let Some(choices) = choices.filter(|_| !hide_possible_values) { + if !choices.choices.is_empty() { + parts.push(format!("[{}]", choices.choices.join(", "))); + } + if let Some(env) = choices.env() { + parts.push(format!("[choices env: {env}]")); } } + if !hide_env { + if let Some(env) = env { + parts.push(format!("[env: {env}]")); + } + parts.extend( + env_fallback + .iter() + .map(|env| format!("[env fallback: {env}]")), + ); + parts.extend( + deprecated_env + .iter() + .map(|env| format!("[deprecated env: {env}]")), + ); + } + if !hide_default_value && !default.is_empty() { + parts.push(format!("(default: {})", default.join(", "))); + } + parts } /// The entry every command list ends with, unless the CLI turned it off. diff --git a/lib/src/docs/cli/templates/spec_template_short.tera b/lib/src/docs/cli/templates/spec_template_short.tera index dd96f7cf1..f21758b54 100644 --- a/lib/src/docs/cli/templates/spec_template_short.tera +++ b/lib/src/docs/cli/templates/spec_template_short.tera @@ -53,9 +53,10 @@ Usage: {{ (spec.bin ~ " " ~ cmd.usage) | trim }} {{ group.heading | default(value="Arguments") }}: {%- for arg in group.items %} - {% if arg.help %}{% if cmd.next_line_help %}{{ arg.usage | trim }} - {{ arg.help | indent(width=4) }}{% else %}{{ arg.usage | trim | ljust(width=arg.usage_col_width) }} {{ arg.help }}{% endif %}{% else %}{{ arg.usage | trim }}{% endif %} {%- if cmd.next_line_help %} + {{ arg.usage | trim }} +{%- if arg.help %} + {{ arg.help | indent(width=4) }}{% endif %} {%- if not arg.hide_possible_values and arg.choices and arg.choices.choices %} [possible values: {{ arg.choices.choices | join(sep=", ") }}]{%- endif %} {%- if not arg.hide_possible_values and arg.choices and arg.choices.env %} @@ -67,12 +68,13 @@ Usage: {{ (spec.bin ~ " " ~ cmd.usage) | trim }} [deprecated env: {{ env }}]{%- endfor %}{%- endif %} {%- if not arg.hide_default_value and arg.default %} (default: {{ arg.default | join(sep=", ") }}){%- endif %} +{%- elif arg.help_rendered %} + {{ arg.usage | trim | ljust(width=arg.usage_col_width) }} {{ arg.help_rendered }} +{%- elif arg.row %} + {{ arg.usage | trim }} + {{ arg.row | indent(width=4) }} {%- else %} -{%- if not arg.hide_possible_values and arg.choices and arg.choices.choices %} [{{ arg.choices.choices | join(sep=", ") }}]{%- endif %} -{%- if not arg.hide_possible_values and arg.choices and arg.choices.env %} [choices env: {{ arg.choices.env }}]{%- endif %} -{%- if not arg.hide_env and arg.env %} [env: {{ arg.env }}]{%- endif %} -{%- if not arg.hide_env %}{% for env in arg.env_fallback %} [env fallback: {{ env }}]{%- endfor %}{% for env in arg.deprecated_env %} [deprecated env: {{ env }}]{%- endfor %}{%- endif %} -{%- if not arg.hide_default_value and arg.default %} (default: {{ arg.default | join(sep=", ") }}){%- endif %} + {{ arg.usage | trim }} {%- endif %} {%- endfor %} {%- endfor %}{% if arg_has_ungrouped and not arg_has_grouped %}{{ mark_grouped_args }}{% endif %}{{ mark_flags }}{% if not flag_has_ungrouped %}{{ mark_grouped_flags }}{% endif %} @@ -82,9 +84,10 @@ Usage: {{ (spec.bin ~ " " ~ cmd.usage) | trim }} {{ group.heading | default(value="Flags") }}: {%- for flag in group.items %} - {% if flag.help %}{% if cmd.next_line_help %}{{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} - {{ flag.help | indent(width=4) }}{% else %}{{ flag.display_usage | ljust(width=flag.usage_col_width) }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} {{ flag.help }}{% endif %}{% else %}{{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %}{% endif %} {%- if cmd.next_line_help %} + {{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} +{%- if flag.help %} + {{ flag.help | indent(width=4) }}{% endif %} {%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.choices %} [possible values: {{ flag.arg.choices.choices | join(sep=", ") }}]{%- endif %} {%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.env %} @@ -96,15 +99,16 @@ Usage: {{ (spec.bin ~ " " ~ cmd.usage) | trim }} [deprecated env: {{ env }}]{%- endfor %}{%- endif %} {%- if not flag.hide_default_value and flag.default %} (default: {{ flag.default | join(sep=", ") }}){%- endif %} +{%- if flag.deprecated or flag.deprecated_warn_at or flag.deprecated_remove_at %} + [deprecated:{% if flag.deprecated %} {{ flag.deprecated }}{% endif %}{% if flag.deprecated_warn_at %}{% if flag.deprecated %};{% endif %} warns at {{ flag.deprecated_warn_at }}{% endif %}{% if flag.deprecated_remove_at %}{% if flag.deprecated or flag.deprecated_warn_at %};{% endif %} removed at {{ flag.deprecated_remove_at }}{% endif %}]{%- endif %} +{%- elif flag.help_rendered %} + {{ flag.display_usage | ljust(width=flag.usage_col_width) }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} {{ flag.help_rendered }} +{%- elif flag.row %} + {{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} + {{ flag.row | indent(width=4) }} {%- else %} -{%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.choices %} [{{ flag.arg.choices.choices | join(sep=", ") }}]{%- endif %} -{%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.env %} [choices env: {{ flag.arg.choices.env }}]{%- endif %} -{%- if not flag.hide_env and flag.env %} [env: {{ flag.env }}]{%- endif %} -{%- if not flag.hide_env %}{% for env in flag.env_fallback %} [env fallback: {{ env }}]{%- endfor %}{% for env in flag.deprecated_env %} [deprecated env: {{ env }}]{%- endfor %}{%- endif %} -{%- if not flag.hide_default_value and flag.default %} (default: {{ flag.default | join(sep=", ") }}){%- endif %} + {{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} {%- endif %} -{%- if flag.deprecated or flag.deprecated_warn_at or flag.deprecated_remove_at %}{% if cmd.next_line_help %} - {% else %} {% endif %}[deprecated:{% if flag.deprecated %} {{ flag.deprecated }}{% endif %}{% if flag.deprecated_warn_at %}{% if flag.deprecated %};{% endif %} warns at {{ flag.deprecated_warn_at }}{% endif %}{% if flag.deprecated_remove_at %}{% if flag.deprecated or flag.deprecated_warn_at %};{% endif %} removed at {{ flag.deprecated_remove_at }}{% endif %}]{%- endif %} {%- endfor %} {%- endfor %}{% if flag_has_ungrouped and not flag_has_grouped %}{{ mark_grouped_flags }}{% endif %}{{ mark_global_flags }} @@ -112,9 +116,10 @@ Usage: {{ (spec.bin ~ " " ~ cmd.usage) | trim }} Global flags: {%- for flag in global_flags %} - {% if flag.help %}{% if cmd.next_line_help %}{{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} - {{ flag.help | indent(width=4) }}{% else %}{{ flag.display_usage | ljust(width=flag.usage_col_width) }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} {{ flag.help }}{% endif %}{% else %}{{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %}{% endif %} {%- if cmd.next_line_help %} + {{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} +{%- if flag.help %} + {{ flag.help | indent(width=4) }}{% endif %} {%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.choices %} [possible values: {{ flag.arg.choices.choices | join(sep=", ") }}]{%- endif %} {%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.env %} @@ -126,15 +131,16 @@ Global flags: [deprecated env: {{ env }}]{%- endfor %}{%- endif %} {%- if not flag.hide_default_value and flag.default %} (default: {{ flag.default | join(sep=", ") }}){%- endif %} +{%- if flag.deprecated or flag.deprecated_warn_at or flag.deprecated_remove_at %} + [deprecated:{% if flag.deprecated %} {{ flag.deprecated }}{% endif %}{% if flag.deprecated_warn_at %}{% if flag.deprecated %};{% endif %} warns at {{ flag.deprecated_warn_at }}{% endif %}{% if flag.deprecated_remove_at %}{% if flag.deprecated or flag.deprecated_warn_at %};{% endif %} removed at {{ flag.deprecated_remove_at }}{% endif %}]{%- endif %} +{%- elif flag.help_rendered %} + {{ flag.display_usage | ljust(width=flag.usage_col_width) }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} {{ flag.help_rendered }} +{%- elif flag.row %} + {{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} + {{ flag.row | indent(width=4) }} {%- else %} -{%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.choices %} [{{ flag.arg.choices.choices | join(sep=", ") }}]{%- endif %} -{%- if not flag.hide_possible_values and flag.arg.choices and flag.arg.choices.env %} [choices env: {{ flag.arg.choices.env }}]{%- endif %} -{%- if not flag.hide_env and flag.env %} [env: {{ flag.env }}]{%- endif %} -{%- if not flag.hide_env %}{% for env in flag.env_fallback %} [env fallback: {{ env }}]{%- endfor %}{% for env in flag.deprecated_env %} [deprecated env: {{ env }}]{%- endfor %}{%- endif %} -{%- if not flag.hide_default_value and flag.default %} (default: {{ flag.default | join(sep=", ") }}){%- endif %} + {{ flag.display_usage }}{% if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %} {%- endif %} -{%- if flag.deprecated or flag.deprecated_warn_at or flag.deprecated_remove_at %}{% if cmd.next_line_help %} - {% else %} {% endif %}[deprecated:{% if flag.deprecated %} {{ flag.deprecated }}{% endif %}{% if flag.deprecated_warn_at %}{% if flag.deprecated %};{% endif %} warns at {{ flag.deprecated_warn_at }}{% endif %}{% if flag.deprecated_remove_at %}{% if flag.deprecated or flag.deprecated_warn_at %};{% endif %} removed at {{ flag.deprecated_remove_at }}{% endif %}]{%- endif %} {%- endfor %} {%- endif %}{{ mark_flattened }} diff --git a/lib/src/docs/models.rs b/lib/src/docs/models.rs index fc5c6fe3f..e113a3c00 100644 --- a/lib/src/docs/models.rs +++ b/lib/src/docs/models.rs @@ -183,6 +183,9 @@ pub struct SpecFlag { pub rendered: bool, #[serde(skip_serializing_if = "Option::is_none")] pub help_rendered: Option, + /// The same text unwrapped, for the layout that indents it under the usage instead. + #[serde(skip_serializing_if = "Option::is_none")] + pub row: Option, pub help_is_multiline: bool, pub usage_col_width: usize, } @@ -549,6 +552,9 @@ pub struct SpecArg { pub rendered: bool, #[serde(skip_serializing_if = "Option::is_none")] pub help_rendered: Option, + /// The same text unwrapped, for the layout that indents it under the usage instead. + #[serde(skip_serializing_if = "Option::is_none")] + pub row: Option, pub help_is_multiline: bool, pub usage_col_width: usize, } @@ -1080,6 +1086,7 @@ impl From<&crate::SpecFlag> for SpecFlag { display_order: flag.display_order, rendered: false, help_rendered: None, + row: None, help_is_multiline: false, usage_col_width: 0, } @@ -1132,6 +1139,7 @@ impl From<&crate::SpecArg> for SpecArg { display_order: arg.display_order, rendered: false, help_rendered: None, + row: None, help_is_multiline: false, usage_col_width: 0, }