Skip to main content

usage/docs/cli/
mod.rs

1use crate::{Spec, SpecCommand};
2use std::collections::HashMap;
3use std::sync::LazyLock;
4use tera::Tera;
5
6mod style;
7pub use style::Style;
8
9pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String {
10    render_help_styled(spec, cmd, long, Style::PLAIN)
11}
12
13/// Render a terminal help page with an explicit colour policy.
14///
15/// Runs nothing: `choices run=` is described as where the values come from. A program showing
16/// its own help while it runs wants [`render_runtime_help`], which lists them.
17pub fn render_help_styled(spec: &Spec, cmd: &SpecCommand, long: bool, style: Style) -> String {
18    render_page(spec, cmd, long, style, ChoiceRuns::Describe)
19}
20
21/// Render the help page of a program that is running, listing what `choices run=` prints.
22///
23/// The page [`render_help_styled`] draws, except that each `choices run=` command on it is run
24/// and its output listed among the possible values, as `choices "a" "b"` would be. A command
25/// that fails is described instead, the way the static page describes it, and what it wrote to
26/// stderr reaches the terminal. `env` is added to each command's environment, for a caller
27/// that parses with [`crate::Parser::with_env`]. Each distinct command runs once per page.
28///
29/// For a script's or task's `--help`; generated documentation should use
30/// [`render_help_styled`], which never runs anything.
31pub fn render_runtime_help(
32    spec: &Spec,
33    cmd: &SpecCommand,
34    long: bool,
35    style: Style,
36    env: Option<&HashMap<String, String>>,
37) -> String {
38    let _scope = crate::spec::choices::RunScope::enter();
39    render_page(spec, cmd, long, style, ChoiceRuns::Run(env))
40}
41
42/// Whether a page runs `choices run=` commands or describes them.
43#[derive(Clone, Copy)]
44enum ChoiceRuns<'a> {
45    Describe,
46    Run(Option<&'a HashMap<String, String>>),
47}
48
49fn render_page(
50    spec: &Spec,
51    cmd: &SpecCommand,
52    long: bool,
53    style: Style,
54    runs: ChoiceRuns<'_>,
55) -> String {
56    // Convert to docs models to get layout calculations
57    let docs_spec = crate::docs::models::Spec::from(spec);
58    let mut docs_cmd = crate::docs::models::SpecCommand::from(&without_hidden(cmd, long));
59
60    let mut ctx = tera::Context::new();
61    ctx.insert("spec", &docs_spec);
62    ctx.insert("long", &long);
63    // Which page this is. The banner and the program's own description belong to the
64    // program's page; a subcommand's page describes the subcommand, which is the question
65    // that was asked. `full_cmd` is the path a user would type, so the root's is empty.
66    ctx.insert("root", &docs_cmd.full_cmd.is_empty());
67    // Keep this out of the recursively serialized docs command. It controls this page only;
68    // carrying it on every descendant makes rendering a whole command tree pay for the same
69    // boolean at every level.
70    ctx.insert("show_help_subcommand", &!cmd.disable_help_subcommand);
71    // Everything this command inherits: from each ancestor, only what it declared `global` —
72    // the rule the parser follows on the way down. `full_cmd` is the typed path, so walking it
73    // from the root gives the exact ancestry with none of the ambiguity a search would have.
74    //
75    // Listed nowhere before this: `communique generate` accepts `--config` from its root and
76    // its page mentioned none of it — a flag a user can type and cannot discover.
77    let (mut inherited, ancestors_taken) = inherited_flags(spec, cmd, &docs_cmd.full_cmd, long);
78    // Before anything is laid out, since the values listed decide how an entry wraps.
79    if let ChoiceRuns::Run(env) = runs {
80        resolve_command_choices(&mut docs_cmd, env);
81        for flag in &mut inherited {
82            resolve_flag_choices(flag, env);
83        }
84    }
85
86    // One column over both lists, so the two sections read as one table with a rule through it
87    // rather than two that happen to be adjacent. The width feeds the wrapping as well as the
88    // padding — a continuation line is indented to sit under the description — so both lists
89    // are laid out again once the width is known.
90    // Last in the command's own section, which is where clap has them: they carry no
91    // `help_heading`, so a CLI that groups its flags gets them at the end of the ungrouped
92    // list rather than inside somebody's section.
93    {
94        let supplied = supplied_flags(spec, cmd, &ancestors_taken, docs_cmd.full_cmd.is_empty());
95        if !supplied.is_empty() {
96            match docs_cmd
97                .flag_groups
98                .iter_mut()
99                .find(|g| g.heading.is_none())
100            {
101                Some(group) => group.items.extend(supplied),
102                // Inserted first, not pushed: `group_by_heading` sorts the unheaded group to
103                // the front and argv's `groups_section` emits it there, so a CLI that heads
104                // every one of its flags would otherwise get `Flags:` *after* the headed
105                // sections here and before them there.
106                None => docs_cmd.flag_groups.insert(
107                    0,
108                    crate::docs::models::Group {
109                        heading: None,
110                        help: None,
111                        help_rendered: None,
112                        items: supplied,
113                    },
114                ),
115            }
116        }
117    }
118
119    // The terminal's width, less the margin the root page reserves for a logo: help wraps to
120    // whatever width it is given, so a page laid out at the full width leaves no margin for
121    // the art to sit in. See `crate::docs::logo::margin`.
122    let terminal = crate::docs::layout::help_width(cmd.term_width, cmd.max_term_width);
123    let logo_margin = docs_cmd
124        .full_cmd
125        .is_empty()
126        .then_some(spec.logo.as_deref())
127        .flatten()
128        .and_then(|logo| crate::docs::logo::margin(logo, terminal));
129    let width = logo_margin.map_or(terminal, |(page, _)| page);
130    ctx.insert("terminal_width", &width);
131    lay_out_group_help(&mut docs_cmd.subcommand_groups, width);
132    lay_out_group_help(&mut docs_cmd.arg_groups, width);
133    lay_out_group_help(&mut docs_cmd.flag_groups, width);
134    let col = crate::docs::layout::usage_column_width(
135        docs_cmd
136            .flag_groups
137            .iter()
138            .flat_map(|g| g.items.iter())
139            .chain(inherited.iter())
140            .map(|f| f.display_usage.as_str()),
141        width,
142    );
143    let flag_column = Column {
144        width,
145        col,
146        long,
147        next_line: cmd.next_line_help,
148    };
149    for group in &mut docs_cmd.flag_groups {
150        lay_out(&mut group.items, flag_column);
151    }
152    lay_out(&mut inherited, flag_column);
153
154    // The arguments get their own column, laid out here for the page being rendered rather than
155    // taken from the model's — which is the long page's, and would put a long description in the
156    // short page's column unwrapped.
157    let arg_col = crate::docs::layout::usage_column_width(
158        docs_cmd.args.iter().map(|a| a.usage.as_str()),
159        width,
160    );
161    for group in &mut docs_cmd.arg_groups {
162        lay_out_args(
163            &mut group.items,
164            Column {
165                width,
166                col: arg_col,
167                long,
168                next_line: cmd.next_line_help,
169            },
170        );
171    }
172
173    // Flattened descendants live on this page, so this page's width governs them. Their own
174    // `term_width` applies when they get a page of their own, not when an ancestor expands them.
175    lay_out_flattened(&mut docs_cmd.flattened_subcommands, width, long);
176
177    // The command list, laid out the way the flag list is: one column for the whole page, the
178    // name in it, and everything else — summary, aliases, deprecation — trailing as text that
179    // wraps under itself. Both pages get the same rows, so `--help` no longer reprints every
180    // child's long help on its parent's page.
181    let help_row = {
182        let show_help_row = !docs_cmd.subcommands.is_empty()
183            && !docs_cmd.flatten_help
184            && !cmd.disable_help_subcommand;
185        let cmd_col = crate::docs::layout::usage_column_width(
186            docs_cmd
187                .subcommand_groups
188                .iter()
189                .flat_map(|g| g.items.iter())
190                .map(|c| c.name.as_str())
191                .chain(show_help_row.then_some(HELP_SUBCOMMAND)),
192            width,
193        );
194        let default_name = docs_cmd
195            .full_cmd
196            .is_empty()
197            .then(|| {
198                spec.default_subcommand.as_deref().and_then(|name| {
199                    spec.cmd
200                        .find_subcommand(name)
201                        .filter(|child| !child.hide)
202                        .map(|child| child.name.as_str())
203                })
204            })
205            .flatten();
206        for group in &mut docs_cmd.subcommand_groups {
207            lay_out_commands(
208                &mut group.items,
209                width,
210                cmd_col,
211                cmd.next_line_help,
212                default_name,
213            );
214        }
215        // Rendered here rather than in the template because it is a row like any other: it
216        // sits in the same column and wraps by the same rule, and neither is something a
217        // template should be working out.
218        show_help_row.then(|| {
219            render_row(
220                HELP_SUBCOMMAND,
221                HELP_SUBCOMMAND_SUMMARY,
222                width,
223                cmd_col,
224                cmd.next_line_help,
225            )
226        })
227    };
228    ctx.insert("help_row", &help_row);
229
230    let arg_has_ungrouped = docs_cmd
231        .arg_groups
232        .iter()
233        .any(|group| group.heading.is_none());
234    let arg_has_grouped = docs_cmd
235        .arg_groups
236        .iter()
237        .any(|group| group.heading.is_some());
238    let flag_has_ungrouped = docs_cmd
239        .flag_groups
240        .iter()
241        .any(|group| group.heading.is_none());
242    let flag_has_grouped = docs_cmd
243        .flag_groups
244        .iter()
245        .any(|group| group.heading.is_some());
246    ctx.insert("arg_has_ungrouped", &arg_has_ungrouped);
247    ctx.insert("arg_has_grouped", &arg_has_grouped);
248    ctx.insert("flag_has_ungrouped", &flag_has_ungrouped);
249    ctx.insert("flag_has_grouped", &flag_has_grouped);
250
251    // Inserted after the layout, not before: the template reads the widths, and a `cmd` put
252    // into the context first would carry the ones computed before the two lists were joined.
253    ctx.insert("cmd", &docs_cmd);
254    ctx.insert("global_flags", &inherited);
255    for (name, mark) in MARKS {
256        ctx.insert(name, &mark);
257    }
258    ctx.insert("mark_grouped_args", &MARK_GROUPED_ARGS);
259    ctx.insert("mark_grouped_flags", &MARK_GROUPED_FLAGS);
260    ctx.insert("mark_global_flags", &MARK_GLOBAL_FLAGS);
261    let template = if long {
262        "spec_template_long.tera"
263    } else {
264        "spec_template_short.tera"
265    };
266    let rendered = TERA.render(template, &ctx).unwrap();
267    let sections = Sections::split(&rendered);
268    let styling = style::Styling::new(
269        &docs_cmd,
270        &inherited,
271        sections.usage,
272        !cmd.disable_help_subcommand,
273    );
274    let page = match spec
275        .help_template
276        .as_deref()
277        .filter(|t| crate::help_template::is_set(t))
278    {
279        Some(template) => {
280            crate::help_template::substitute_with_style(template, style.coloured, |name| {
281                sections
282                    .named(name)
283                    .map(|section| styling.apply(&section, style))
284            })
285        }
286        None => styling.apply(&sections.concatenated(), style),
287    };
288    let page = if style.coloured {
289        std::borrow::Cow::Borrowed(page.as_str())
290    } else {
291        crate::docs::strip_ansi(&page)
292    };
293    let mut page = page.trim().to_string();
294    page.push('\n');
295    // Last, and only on the program's own page. The art is positioned by column, so anything
296    // that trims or re-wraps the page afterwards would move it; and a default subcommand's
297    // page is appended after this, below the logo rather than beside it.
298    if docs_cmd.full_cmd.is_empty()
299        && let Some(logo) = spec.logo.as_deref()
300    {
301        page = crate::docs::logo::place(
302            &page,
303            logo,
304            spec.logo_style.as_deref(),
305            terminal,
306            logo_margin.map(|(_, column)| column),
307            style.coloured,
308        );
309    }
310    append_default_command_help(spec, cmd, long, style, runs, page)
311}
312
313/// Fold what each `choices run=` on a page prints into its possible values.
314fn resolve_command_choices(
315    cmd: &mut crate::docs::models::SpecCommand,
316    env: Option<&HashMap<String, String>>,
317) {
318    for arg in cmd
319        .args
320        .iter_mut()
321        .chain(cmd.arg_groups.iter_mut().flat_map(|g| g.items.iter_mut()))
322    {
323        resolve_arg_choices(arg, env);
324    }
325    for flag in cmd
326        .flags
327        .iter_mut()
328        .chain(cmd.flag_groups.iter_mut().flat_map(|g| g.items.iter_mut()))
329    {
330        resolve_flag_choices(flag, env);
331    }
332    for child in &mut cmd.flattened_subcommands {
333        resolve_command_choices(child, env);
334    }
335}
336
337fn resolve_flag_choices(
338    flag: &mut crate::docs::models::SpecFlag,
339    env: Option<&HashMap<String, String>>,
340) {
341    // The page reads the flag's own `hide_possible_values`, so a hidden list is not worth
342    // running a command for.
343    if flag.hide_possible_values {
344        return;
345    }
346    if let Some(arg) = &mut flag.arg {
347        resolve_arg_choices(arg, env);
348    }
349}
350
351fn resolve_arg_choices(
352    arg: &mut crate::docs::models::SpecArg,
353    env: Option<&HashMap<String, String>>,
354) {
355    if arg.hide_possible_values {
356        return;
357    }
358    if let Some(choices) = arg.choices.as_mut().filter(|c| c.run.is_some()) {
359        choices.resolve_for_help(env);
360    }
361}
362
363fn append_default_command_help(
364    spec: &Spec,
365    cmd: &SpecCommand,
366    long: bool,
367    style: Style,
368    runs: ChoiceRuns<'_>,
369    parent: String,
370) -> String {
371    if !cmd.full_cmd.is_empty() || !spec.default_subcommand_help || cmd.flatten_help {
372        return parent;
373    }
374    let Some(child) = spec
375        .default_subcommand
376        .as_deref()
377        .and_then(|name| spec.cmd.find_subcommand(name))
378        .filter(|child| !child.hide)
379    else {
380        return parent;
381    };
382    let child_page = render_page(spec, child, long, style, runs);
383    let mut out = parent.trim_end().to_string();
384    out.push_str("\n\nDefault command: ");
385    out.push_str(&crate::help_template::semantic(
386        "command",
387        &child.name,
388        style.coloured,
389    ));
390    out.push_str("\n  Unmatched words select this command.\n\n");
391    out.push_str(child_page.trim_end());
392    out.push('\n');
393    out
394}
395
396/// Where each section of a rendered page starts, as the templates write it.
397///
398/// The layout lives in the templates, and this is how it stays there: each one emits a marker
399/// at every section boundary, so the boundaries are declared beside the sections rather than
400/// worked out again here. A page with no `help_template` is the marks taken back out, which is
401/// the same string the templates produced before any of this existed — and what the fleet gate
402/// compares byte for byte.
403///
404/// Control characters, because a marker has to be something no help text contains and no
405/// terminal shows if one ever escapes.
406const MARKS: [(&str, &str); 6] = [
407    ("mark_usage", "\u{1}usage\u{1}"),
408    ("mark_commands", "\u{1}commands\u{1}"),
409    ("mark_args", "\u{1}args\u{1}"),
410    ("mark_flags", "\u{1}flags\u{1}"),
411    ("mark_flattened", "\u{1}flattened\u{1}"),
412    ("mark_after_help", "\u{1}after_help\u{1}"),
413];
414const MARK_GROUPED_ARGS: &str = "\u{1}grouped_args\u{1}";
415const MARK_GROUPED_FLAGS: &str = "\u{1}grouped_flags\u{1}";
416const MARK_GLOBAL_FLAGS: &str = "\u{1}global_flags\u{1}";
417
418/// A rendered page cut into the sections a `help_template` may reorder.
419///
420/// The twin of `usage_argv::help`'s `Sections`, down to `flattened` not being a section an
421/// author can name: it is the other half of `commands`, since `flatten_help` replaces a
422/// command list with the subcommands' own bodies, and only one of the two is ever there.
423struct Sections<'a> {
424    about: &'a str,
425    usage: &'a str,
426    commands: &'a str,
427    args: String,
428    flags: String,
429    grouped_args: &'a str,
430    ungrouped_args: &'a str,
431    grouped_flags: &'a str,
432    ungrouped_flags: String,
433    flattened: &'a str,
434    after_help: &'a str,
435}
436
437impl<'a> Sections<'a> {
438    fn split(rendered: &'a str) -> Self {
439        let mut rest = rendered;
440        let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
441        for (_, mark) in MARKS {
442            // A missing marker leaves that section empty rather than swallowing the ones after
443            // it: every one is written at the top level of both templates, so this cannot
444            // happen, and it is not worth a panic in a help renderer if it ever does.
445            match rest.split_once(mark) {
446                Some((before, after)) => {
447                    parts.push(before);
448                    rest = after;
449                }
450                None => parts.push(""),
451            }
452        }
453        parts.push(rest);
454        let (ungrouped_args, grouped_args) = parts[3]
455            .split_once(MARK_GROUPED_ARGS)
456            .unwrap_or((parts[3], ""));
457        let (own_flags, global_flags) = parts[4]
458            .split_once(MARK_GLOBAL_FLAGS)
459            .unwrap_or((parts[4], ""));
460        let (own_ungrouped_flags, grouped_flags) = own_flags
461            .split_once(MARK_GROUPED_FLAGS)
462            .unwrap_or((own_flags, ""));
463        Self {
464            about: parts[0],
465            usage: parts[1],
466            commands: parts[2],
467            args: format!("{ungrouped_args}{grouped_args}"),
468            flags: format!("{own_ungrouped_flags}{grouped_flags}{global_flags}"),
469            grouped_args,
470            ungrouped_args,
471            grouped_flags,
472            ungrouped_flags: format!("{own_ungrouped_flags}{global_flags}"),
473            flattened: parts[5],
474            after_help: parts[6],
475        }
476    }
477
478    /// The default page: every section in the order the templates wrote them.
479    fn concatenated(&self) -> String {
480        [
481            self.about,
482            self.usage,
483            self.commands,
484            self.args.as_str(),
485            self.flags.as_str(),
486            self.flattened,
487            self.after_help,
488        ]
489        .concat()
490    }
491
492    /// One section by name, trimmed, so that a template owns the whitespace between them.
493    fn named(&self, name: &str) -> Option<String> {
494        Some(match name {
495            "about" => self.about.trim().to_string(),
496            "usage" => self.usage.trim().to_string(),
497            "commands" => {
498                let mut out = self.commands.trim().to_string();
499                let flattened = self.flattened.trim();
500                if !flattened.is_empty() {
501                    if !out.is_empty() {
502                        out.push_str("\n\n");
503                    }
504                    out.push_str(flattened);
505                }
506                out
507            }
508            "args" => self.args.trim().to_string(),
509            "flags" => self.flags.trim().to_string(),
510            "grouped_args" => self.grouped_args.trim().to_string(),
511            "ungrouped_args" => self.ungrouped_args.trim().to_string(),
512            "grouped_flags" => self.grouped_flags.trim().to_string(),
513            "ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
514            "after_help" => self.after_help.trim().to_string(),
515            _ => return None,
516        })
517    }
518}
519
520/// The entries for `--help` and `--version`, which the parser supplies and no spec declares.
521///
522/// Listed because help is written for people: a reader looking for how to ask for help should
523/// find it on the page. This reverses the rule these two used to follow — that a page lists
524/// exactly what its spec declares — and the reason is that the spec has its own readers, and
525/// they are not the ones reading this.
526///
527/// `--version` only on the program's own page and only where a version is declared, which is
528/// where a parser accepts one. Each spelling is dropped where the CLI claimed it, since a page
529/// must not describe a flag that something else binds.
530///
531/// The twin of `supplied_entries` in `usage-argv`'s `help` module; the gate over mise's spec is
532/// what says the two agree.
533fn supplied_flags(
534    spec: &Spec,
535    cmd: &SpecCommand,
536    ancestors_taken: &[String],
537    is_root: bool,
538) -> Vec<crate::docs::models::SpecFlag> {
539    // The command's own spellings plus everything in scope above it — the set the inherited
540    // walk built, which counts hidden globals and negations. Rebuilding it from the *visible*
541    // inherited list lost both: a hidden ancestor that binds `--help` would have had the page
542    // offer it anyway.
543    let mut taken: Vec<String> = ancestors_taken.to_vec();
544    for f in &cmd.flags {
545        taken.extend(f.long.iter().map(|l| format!("--{l}")));
546        taken.extend(f.short.iter().map(|s| format!("-{s}")));
547        // Stored with its dashes here, unlike in usage-argv.
548        taken.extend(f.negate.clone());
549    }
550
551    let build = |name: &str, long: &str, short: char, help: &str| {
552        let long_free = !taken.contains(&format!("--{long}"));
553        let short_free = !taken.contains(&format!("-{short}"));
554        if !long_free && !short_free {
555            return None;
556        }
557        // Named after the form it shows: a short-only entry called `help` reads as a renamed
558        // flag and printed `help: -h`.
559        let name = if long_free { name } else { &short.to_string() };
560        let mut flag = crate::SpecFlag {
561            name: name.to_string(),
562            long: if long_free {
563                vec![long.to_string()]
564            } else {
565                vec![]
566            },
567            short: if short_free { vec![short] } else { vec![] },
568            help: Some(help.to_string()),
569            ..Default::default()
570        };
571        flag.usage = flag.usage();
572        Some(crate::docs::models::SpecFlag::from(&flag))
573    };
574
575    let mut out = Vec::new();
576    // `disable_help` turns the parser's answer off — `is_help_arg` refuses the spelling
577    // outright — so a page that still listed it would describe an action nothing performs.
578    // The same rule as a claimed or hidden spelling, with the claim made by the spec itself.
579    //
580    // usage-argv has no equivalent: `disable_help` is a KDL-only word, so no spec that crate
581    // can hold ever carries one, and the two renderers cannot disagree about it.
582    if spec.disable_help != Some(true) && !cmd.disable_help_flag {
583        out.extend(build("help", "help", 'h', "Print help"));
584    }
585    if is_root
586        && (spec.version.is_some() || spec.long_version.is_some())
587        && !cmd.disable_version_flag
588    {
589        out.extend(build("version", "version", 'V', "Print version"));
590    }
591    out
592}
593
594/// Fit a list of flags to a column: how wide their names are, and where their help wraps.
595///
596/// The same pass `SpecCommand::from` makes, run again once the width is known over *both* the
597/// command's own flags and the ones it inherits. The width is not only padding — a wrapped
598/// description is indented to sit under itself — so it cannot be decided per section and then
599/// shared.
600fn lay_out(flags: &mut [crate::docs::models::SpecFlag], column: Column) {
601    for flag in flags {
602        flag.usage_col_width = column.col;
603        flag.help_is_block =
604            !column.can_inline(crate::docs::layout::visible_width(&flag.display_usage));
605        let text = if column.long {
606            flag.help_long
607                .as_deref()
608                .or(flag.help.as_deref())
609                .map(str::to_string)
610        } else if column.next_line {
611            flag.help.clone()
612        } else {
613            with_annotations(flag.help.as_deref(), flag_annotations(flag))
614        };
615        wrap_into(
616            text,
617            column,
618            crate::docs::layout::visible_width(&flag.display_usage),
619            &mut flag.row,
620            &mut flag.help_rendered,
621            &mut flag.help_is_multiline,
622            &mut flag.ann_indent,
623        );
624    }
625}
626
627/// The same pass over a command's arguments.
628///
629/// `SpecCommand::from` already made one, but it made the long page's — the short page prefers
630/// the short description and carries the annotations in the text — so the page it is actually
631/// rendering gets the last word.
632fn lay_out_args(args: &mut [crate::docs::models::SpecArg], column: Column) {
633    for arg in args {
634        arg.usage_col_width = column.col;
635        arg.help_is_block = !column.can_inline(crate::docs::layout::visible_width(&arg.usage));
636        let text = if column.long {
637            arg.help_long
638                .as_deref()
639                .or(arg.help.as_deref())
640                .map(str::to_string)
641        } else if column.next_line {
642            arg.help.clone()
643        } else {
644            with_annotations(arg.help.as_deref(), arg_annotations(arg))
645        };
646        wrap_into(
647            text,
648            column,
649            crate::docs::layout::visible_width(&arg.usage),
650            &mut arg.row,
651            &mut arg.help_rendered,
652            &mut arg.help_is_multiline,
653            &mut arg.ann_indent,
654        );
655    }
656}
657
658fn lay_out_flattened(commands: &mut [crate::docs::models::SpecCommand], width: usize, long: bool) {
659    for command in commands {
660        lay_out_group_help(&mut command.arg_groups, width);
661        lay_out_group_help(&mut command.flag_groups, width);
662        let arg_col = crate::docs::layout::usage_column_width(
663            command
664                .arg_groups
665                .iter()
666                .flat_map(|group| group.items.iter())
667                .map(|arg| arg.usage.as_str()),
668            width,
669        );
670        let arg_column = Column {
671            width,
672            col: arg_col,
673            long,
674            next_line: command.flattened_next_line_help,
675        };
676        for group in &mut command.arg_groups {
677            lay_out_args(&mut group.items, arg_column);
678        }
679
680        let flag_col = crate::docs::layout::usage_column_width(
681            command
682                .flag_groups
683                .iter()
684                .flat_map(|group| group.items.iter())
685                .map(|flag| flag.display_usage.as_str()),
686            width,
687        );
688        let flag_column = Column {
689            width,
690            col: flag_col,
691            long,
692            next_line: command.flattened_next_line_help,
693        };
694        for group in &mut command.flag_groups {
695            lay_out(&mut group.items, flag_column);
696        }
697    }
698}
699
700fn lay_out_group_help<T>(groups: &mut [crate::docs::models::Group<T>], width: usize) {
701    for group in groups {
702        group.help_rendered = group
703            .help
704            .as_deref()
705            .map(|help| crate::docs::layout::render_indented_text(help, width, 2));
706    }
707}
708
709/// Fit one entry's text to the column, and say which layout it wants.
710///
711/// `row` is the text as composed and `help_rendered` the same text wrapped; an empty wrapping
712/// is how [`crate::docs::layout::render_help_text`] says "no room, put it underneath instead",
713/// which is the case the template reads `row` for.
714fn wrap_into(
715    text: Option<String>,
716    column: Column,
717    usage_width: usize,
718    row: &mut Option<String>,
719    help_rendered: &mut Option<String>,
720    help_is_multiline: &mut bool,
721    ann_indent: &mut String,
722) {
723    *row = None;
724    *help_rendered = None;
725    *help_is_multiline = false;
726    // An entry with nothing in the column still has annotations to place, and the column is
727    // where they go: it is the entry's own row that is empty, not the table's.
728    *ann_indent = column.annotation_indent(column.can_inline(usage_width));
729    let Some(text) = text else { return };
730    if !column.can_inline(usage_width) {
731        let indent = column.block_indent();
732        *row = Some(indent_text(
733            &crate::docs::layout::render_indented_text(&text, column.width, indent),
734            indent,
735        ));
736        return;
737    }
738    let first_indent = column.inline_help_start(usage_width);
739    let continuation_indent = column.description_indent();
740    let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
741        &text,
742        column.width,
743        first_indent,
744        continuation_indent,
745    );
746    // `render_help_text` wraps whatever it is given; whether the page *uses* that is the
747    // template's decision, and on a next-line page it does not. Both have to agree, or the
748    // annotations align to a column the description never entered.
749    *ann_indent = column.annotation_indent(!rendered.is_empty());
750    if !rendered.is_empty() {
751        *help_rendered = Some(rendered);
752        *help_is_multiline = is_multiline;
753    }
754    *row = Some(text);
755}
756
757fn indent_text(text: &str, indent: usize) -> String {
758    let pad = " ".repeat(indent);
759    text.lines()
760        .map(|line| {
761            if line.is_empty() {
762                String::new()
763            } else {
764                format!("{pad}{line}")
765            }
766        })
767        .collect::<Vec<_>>()
768        .join("\n")
769}
770
771/// A short entry's description with its annotations joined on.
772///
773/// The wide layout gives each annotation a line of its own; the narrow one has no room for
774/// that, so they ride along with the description — and they have to be joined *before* it is
775/// wrapped, or an entry with a long description keeps its `[env: …]` out past the column where
776/// the wrapping was supposed to bring the text back.
777fn with_annotations(help: Option<&str>, annotations: Vec<String>) -> Option<String> {
778    let mut parts = Vec::new();
779    if let Some(help) = summarize(help) {
780        parts.push(help.to_string());
781    }
782    parts.extend(annotations);
783    (!parts.is_empty()).then(|| parts.join(" "))
784}
785
786/// What a flag's short entry says about it beyond its description.
787fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec<String> {
788    let mut parts = value_annotations(
789        flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()),
790        flag.hide_possible_values,
791        flag.env.as_deref(),
792        flag.hide_env,
793        &flag.env_fallback,
794        &flag.deprecated_env,
795        &flag.default,
796        flag.hide_default_value,
797    );
798    if let Some(label) = deprecation_label(
799        flag.deprecated.as_deref(),
800        flag.deprecated_warn_at.as_deref(),
801        flag.deprecated_remove_at.as_deref(),
802    ) {
803        parts.push(label);
804    }
805    parts
806}
807
808/// The same for an argument, which carries no deprecation on the narrow page.
809fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec<String> {
810    value_annotations(
811        arg.choices.as_ref(),
812        arg.hide_possible_values,
813        arg.env.as_deref(),
814        arg.hide_env,
815        &arg.env_fallback,
816        &arg.deprecated_env,
817        &arg.default,
818        arg.hide_default_value,
819    )
820}
821
822/// What can be said about a value, in the order the narrow page says it.
823#[allow(clippy::too_many_arguments)]
824fn value_annotations(
825    choices: Option<&crate::SpecChoices>,
826    hide_possible_values: bool,
827    env: Option<&str>,
828    hide_env: bool,
829    env_fallback: &[String],
830    deprecated_env: &[String],
831    default: &[String],
832    hide_default_value: bool,
833) -> Vec<String> {
834    let mut parts = Vec::new();
835    if let Some(choices) = choices.filter(|_| !hide_possible_values) {
836        if !choices.choices.is_empty() {
837            parts.push(format!("[{}]", choices.choices.join(", ")));
838        }
839        if let Some(env) = choices.env() {
840            parts.push(format!("[choices env: {env}]"));
841        }
842        if let Some(run) = choices.run() {
843            // Beside the bare `[a, b]` list this page gives declared values.
844            parts.push(format!("[output of `{run}`]"));
845        }
846    }
847    if !hide_env {
848        if let Some(env) = env {
849            parts.push(format!("[env: {env}]"));
850        }
851        parts.extend(
852            env_fallback
853                .iter()
854                .map(|env| format!("[env fallback: {env}]")),
855        );
856        parts.extend(
857            deprecated_env
858                .iter()
859                .map(|env| format!("[deprecated env: {env}]")),
860        );
861    }
862    if !hide_default_value && !default.is_empty() {
863        parts.push(format!("(default: {})", default.join(", ")));
864    }
865    parts
866}
867
868/// What a page is fitting its entries to.
869#[derive(Clone, Copy)]
870struct Column {
871    /// The width the page is laid out for.
872    width: usize,
873    /// How wide the usage column is.
874    col: usize,
875    /// The long page, which prefers the long description and gives each annotation a line.
876    long: bool,
877    /// A page that puts every description under its usage rather than beside it.
878    next_line: bool,
879}
880
881/// The indent a page uses when it cannot align to its column.
882const BLOCK_INDENT: usize = 4;
883
884impl Column {
885    fn usage_overflows(&self, usage_width: usize) -> bool {
886        !self.next_line && usage_width > self.col
887    }
888
889    fn description_indent(&self) -> usize {
890        2 + self.col + 2
891    }
892
893    fn inline_help_start(&self, usage_width: usize) -> usize {
894        if self.usage_overflows(usage_width) {
895            2 + usage_width + 2
896        } else {
897            self.description_indent()
898        }
899    }
900
901    /// Keep an outlier inline only when its spelling leaves a useful amount of prose.
902    fn can_inline(&self, usage_width: usize) -> bool {
903        if self.next_line {
904            return false;
905        }
906        let minimum = if self.usage_overflows(usage_width) {
907            crate::docs::layout::MIN_INLINE_HELP_WIDTH
908        } else {
909            10
910        };
911        self.width
912            .saturating_sub(self.inline_help_start(usage_width))
913            >= minimum
914    }
915
916    /// Where an entry's annotations are indented to.
917    ///
918    /// The description column, when the description reached it — an annotation is a note about
919    /// the same entry and belongs under the text it qualifies, not in the gutter beside a
920    /// column it is ignoring. Where the description is already a block underneath, there is no
921    /// column to align to and the annotations join it there.
922    fn annotation_indent(&self, reached_column: bool) -> String {
923        let indent = if reached_column {
924            self.description_indent()
925        } else {
926            self.block_indent()
927        };
928        " ".repeat(indent)
929    }
930
931    /// Prefer the normal description column for a stacked entry whenever it still leaves a
932    /// useful line. Exceptionally narrow pages retain the compact four-space fallback.
933    fn block_indent(&self) -> usize {
934        let description = self.description_indent();
935        if !self.next_line && self.width.saturating_sub(description) >= 10 {
936            description
937        } else {
938            BLOCK_INDENT
939        }
940    }
941}
942
943/// The entry every command list ends with, unless the CLI turned it off.
944const HELP_SUBCOMMAND: &str = "help";
945const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
946
947/// Fit a list of subcommand summaries to the page's command column.
948///
949/// `lay_out`'s counterpart for the command list: the same column, the same wrapping, the same
950/// "an empty rendering means use the block layout" signal to the template.
951fn lay_out_commands(
952    commands: &mut [crate::docs::models::HelpCommand],
953    terminal_width: usize,
954    col: usize,
955    next_line: bool,
956    default_name: Option<&str>,
957) {
958    let column = Column {
959        width: terminal_width,
960        col,
961        long: false,
962        next_line,
963    };
964    for command in commands {
965        command.usage_col_width = col;
966        command.row = command_row(command, default_name == Some(command.name.as_str()));
967        command.help_rendered = None;
968        command.help_is_multiline = false;
969        let usage_width = crate::docs::layout::visible_width(&command.name);
970        if let Some(row) = command.row.as_deref() {
971            if column.can_inline(usage_width) {
972                let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
973                    row,
974                    terminal_width,
975                    column.inline_help_start(usage_width),
976                    column.description_indent(),
977                );
978                if !rendered.is_empty() {
979                    command.help_rendered = Some(rendered);
980                    command.help_is_multiline = is_multiline;
981                }
982            } else if let Some(row) = command.row.as_mut() {
983                let indent = column.block_indent();
984                *row = indent_text(
985                    &crate::docs::layout::render_indented_text(row, terminal_width, indent),
986                    indent,
987                );
988            }
989        }
990    }
991}
992
993/// Everything that follows a command's name in its parent's list, as one string.
994///
995/// The name alone occupies the column, so the summaries line up down the page and the syntax a
996/// command takes belongs to that command's own page. What qualifies the command rather than
997/// describing it — the names it also answers to, that it is going away — trails the summary,
998/// where it wraps with the text instead of pushing it out of the column.
999fn command_row(cmd: &crate::docs::models::HelpCommand, is_default: bool) -> Option<String> {
1000    let mut parts = Vec::new();
1001    // A command that wrote only `help_long` still has a summary: its first line. Both pages
1002    // read the same one, so `-h` never says less about a command than `--help` does.
1003    let summary = summarize(cmd.help.as_deref()).or_else(|| {
1004        summarize(
1005            cmd.help_long
1006                .as_deref()
1007                .and_then(|help| help.lines().next()),
1008        )
1009    });
1010    if let Some(summary) = summary {
1011        parts.push(summary.to_string());
1012    }
1013    if !cmd.aliases.is_empty() {
1014        parts.push(format!("[aliases: {}]", cmd.aliases.join(", ")));
1015    }
1016    if let Some(label) = deprecation_label(
1017        cmd.deprecated.as_deref(),
1018        cmd.deprecated_warn_at.as_deref(),
1019        cmd.deprecated_remove_at.as_deref(),
1020    ) {
1021        parts.push(label);
1022    }
1023    if is_default {
1024        parts.push("(default)".to_string());
1025    }
1026    (!parts.is_empty()).then(|| parts.join(" "))
1027}
1028
1029/// A description reduced to what a list can show, or nothing if it says nothing.
1030fn summarize(text: Option<&str>) -> Option<&str> {
1031    text.map(str::trim_end).filter(|text| !text.is_empty())
1032}
1033
1034/// How a page says something is going away, in the one place both lists read it from.
1035fn deprecation_label(
1036    message: Option<&str>,
1037    warn_at: Option<&str>,
1038    remove_at: Option<&str>,
1039) -> Option<String> {
1040    if message.is_none() && warn_at.is_none() && remove_at.is_none() {
1041        return None;
1042    }
1043    let mut parts = Vec::new();
1044    if let Some(message) = message {
1045        parts.push(message.to_string());
1046    }
1047    if let Some(at) = warn_at {
1048        parts.push(format!("warns at {at}"));
1049    }
1050    if let Some(at) = remove_at {
1051        parts.push(format!("removed at {at}"));
1052    }
1053    Some(format!("[deprecated: {}]", parts.join("; ")))
1054}
1055
1056/// One command row, ready to print — the form the synthetic `help` entry takes.
1057fn render_row(
1058    name: &str,
1059    row: &str,
1060    terminal_width: usize,
1061    col: usize,
1062    next_line_help: bool,
1063) -> String {
1064    if !next_line_help {
1065        if crate::docs::layout::visible_width(name) <= col {
1066            let (rendered, _) = crate::docs::layout::render_help_text(row, terminal_width, col);
1067            if !rendered.is_empty() {
1068                return format!("  {name:<col$}  {rendered}");
1069            }
1070        } else if !row.contains('\n') {
1071            return format!(
1072                "  {name}\n    {}",
1073                crate::docs::layout::render_block_text(row, terminal_width)
1074            );
1075        }
1076    }
1077    format!("  {name}\n    {row}")
1078}
1079
1080/// The flags a command inherits, as its page should list them.
1081///
1082/// Walked down `full_cmd` from the root, which is the path a user would type — so the chain is
1083/// exact. Each ancestor contributes only what it declared `global`, and hidden ones are left
1084/// out here as they are everywhere else.
1085///
1086/// The twin of `own_and_global` in `usage-argv`'s `help` module; the two must agree, and the
1087/// gate over mise's spec is what says they do.
1088fn inherited_flags(
1089    spec: &Spec,
1090    cmd: &SpecCommand,
1091    full_cmd: &[String],
1092    long_help: bool,
1093) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
1094    // Every ancestor, root first, which is the order a reader meets them walking down.
1095    let mut ancestors: Vec<&SpecCommand> = Vec::new();
1096    let mut at = &spec.cmd;
1097    for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
1098        ancestors.push(at);
1099        let Some(next) = at.subcommands.get(name) else {
1100            return (Vec::new(), Vec::new());
1101        };
1102        at = next;
1103    }
1104    if !full_cmd.is_empty() {
1105        ancestors.push(at);
1106    }
1107
1108    // Shadowing, which the parser does and the page has to agree with: a command's own flags
1109    // are looked up before its ancestors', so `mise use --raw` is *use's* and never the root's.
1110    // Listing both would print two descriptions for one spelling, one of which can never apply.
1111    // Nearest ancestor first for the decision, then emitted root-first.
1112    // Two sets, because the parser has two passes: it resolves a word against every long and
1113    // short in scope before it looks at a negation at all, so *any* long beats *any* negation
1114    // however far away it is. Reading them as one said a nearer negation had taken a spelling
1115    // that a farther long actually wins.
1116    //
1117    // usage-lib stores a negation *with* its dashes — `negate="--no-colour"` reaches the model
1118    // as `--no-colour` — where usage-argv stores it without. Prefixing here produced
1119    // `----no-colour`, which matched nothing, so negations were counted in name only.
1120    let forms = |f: &crate::SpecFlag| -> Vec<String> {
1121        f.long
1122            .iter()
1123            .map(|l| format!("--{l}"))
1124            .chain(f.short.iter().map(|s| format!("-{s}")))
1125            .collect()
1126    };
1127    let every_form: Vec<String> = cmd
1128        .flags
1129        .iter()
1130        .chain(
1131            ancestors
1132                .iter()
1133                .flat_map(|a| a.flags.iter())
1134                .filter(|f| f.global),
1135        )
1136        .flat_map(&forms)
1137        .collect();
1138
1139    let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
1140    let mut taken_negations: Vec<String> =
1141        cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
1142    let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
1143    for ancestor in ancestors.iter().rev() {
1144        for f in ancestor.flags.iter().filter(|f| f.global) {
1145            let long = f
1146                .long
1147                .iter()
1148                .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
1149                .cloned();
1150            let short = f
1151                .short
1152                .iter()
1153                .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
1154                .copied();
1155            let mine = forms(f);
1156            let negate = f.negate.as_ref().is_some_and(|n| {
1157                !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
1158            });
1159            // Reserved whether or not it is shown: a hidden one still binds, and so does one
1160            // whose every spelling something nearer already took.
1161            taken.extend(forms(f));
1162            taken_negations.extend(f.negate.clone());
1163            if f.hide
1164                || if long_help {
1165                    f.hide_long_help
1166                } else {
1167                    f.hide_short_help
1168                }
1169                || (long.is_none() && short.is_none() && !negate)
1170            {
1171                continue;
1172            }
1173            keep.push((f, long, short, negate));
1174        }
1175    }
1176    let shown: Vec<crate::docs::models::SpecFlag> = ancestors
1177        .iter()
1178        .flat_map(|a| a.flags.iter())
1179        .filter_map(|f| {
1180            keep.iter()
1181                .find(|(k, _, _, _)| std::ptr::eq(*k, f))
1182                .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
1183        })
1184        .map(|(f, long, short, negate)| {
1185            // Only the spellings that survived, so the entry offers what the parser would
1186            // actually accept here.
1187            let mut shown = f.clone();
1188            shown.long = long.into_iter().collect();
1189            shown.short = short.into_iter().collect();
1190            if !negate {
1191                shown.negate = None;
1192            }
1193            shown.usage = shown.usage();
1194            crate::docs::models::SpecFlag::from(&shown)
1195        })
1196        .collect();
1197    // The claim set travels with the result, forms and negations together: the supplied
1198    // `--help` and `--version` entries lose to both, since `find_negation` runs before either
1199    // is offered — even though a negation loses to a long.
1200    taken.extend(taken_negations);
1201    (shown, taken)
1202}
1203
1204/// The command without anything marked `hide`.
1205///
1206/// Help showed hidden flags, hidden arguments and hidden subcommands — everything `hide`
1207/// exists to keep out of it. The usage *line* filtered them already, through
1208/// `SpecCommand::usage`, so `ex --help` listed a `--secret` that the line above it did not
1209/// mention. Markdown and manpage rendering filter too; the help templates were the one place
1210/// that did not.
1211///
1212/// Filtered here rather than in the templates, and before the docs model builds its groups, so
1213/// that a heading whose every entry is hidden produces no section — the same rule markdown
1214/// already follows.
1215fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
1216    let mut visible = cmd.clone();
1217    visible.flags.retain(|flag| {
1218        !flag.hide
1219            && if long {
1220                !flag.hide_long_help
1221            } else {
1222                !flag.hide_short_help
1223            }
1224    });
1225    visible.args.retain(|arg| {
1226        !arg.hide
1227            && if long {
1228                !arg.hide_long_help
1229            } else {
1230                !arg.hide_short_help
1231            }
1232    });
1233    visible.subcommands.retain(|_, sub| !sub.hide);
1234    // Ordinary help only lists immediate subcommands, so their fields are never
1235    // rendered on this page. Walking and cloning the whole remaining tree here
1236    // makes rendering every page quadratic on a fleet-sized CLI. Flattened help
1237    // is the one mode that renders descendant fields and therefore needs the
1238    // recursive filtering.
1239    if visible.flatten_help {
1240        for sub in visible.subcommands.values_mut() {
1241            *sub = without_hidden(sub, long);
1242        }
1243    }
1244    visible
1245}
1246
1247static TERA: LazyLock<Tera> = LazyLock::new(|| {
1248    let mut tera = Tera::default();
1249
1250    // Register ljust filter for left-justifying text with padding
1251    tera.register_filter(
1252        "ljust",
1253        |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1254            let value = value.as_str().unwrap_or("");
1255            let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
1256            Ok(format!("{:<width$}", value, width = width))
1257        },
1258    );
1259    tera.register_filter(
1260        "default",
1261        |value: &tera::Value,
1262         kwargs: tera::Kwargs,
1263         _: &tera::State|
1264         -> tera::TeraResult<tera::Value> {
1265            let default_val = kwargs.must_get::<tera::Value>("value")?;
1266            let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
1267            if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
1268                Ok(default_val)
1269            } else {
1270                Ok(value.clone())
1271            }
1272        },
1273    );
1274    tera.register_filter(
1275        "terminal_wrap",
1276        |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1277            let value = value.as_str().unwrap_or("");
1278            let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1279            let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1280            Ok(crate::docs::layout::render_indented_text(
1281                value, width, indent,
1282            ))
1283        },
1284    );
1285    tera.register_filter(
1286        "terminal_label",
1287        |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1288            let value = value.as_str().unwrap_or("");
1289            let label = args.must_get::<String>("label")?;
1290            let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1291            let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1292            Ok(crate::docs::layout::render_labelled_text(
1293                &label, value, width, indent,
1294            ))
1295        },
1296    );
1297    tera.register_filter(
1298        "terminal_annotation",
1299        |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1300            let body = if let Some(values) = value.as_array() {
1301                values
1302                    .iter()
1303                    .filter_map(tera::Value::as_str)
1304                    .collect::<Vec<_>>()
1305                    .join(", ")
1306            } else {
1307                value.as_str().unwrap_or("").to_string()
1308            };
1309            let label = args.get::<String>("label")?.unwrap_or_default();
1310            let suffix = args.get::<String>("suffix")?.unwrap_or_default();
1311            let indent = args
1312                .get::<String>("indent")?
1313                .unwrap_or_default()
1314                .chars()
1315                .count();
1316            let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1317            let rendered = crate::docs::layout::render_indented_text(
1318                &format!("{label}{body}{suffix}"),
1319                width,
1320                indent,
1321            );
1322            Ok(indent_text(&rendered, indent))
1323        },
1324    );
1325    tera.register_filter(
1326        "terminal_deprecation",
1327        |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1328            let field = |name| value.get_from_path(name).and_then(tera::Value::as_str);
1329            let Some(label) = deprecation_label(
1330                field("deprecated"),
1331                field("deprecated_warn_at"),
1332                field("deprecated_remove_at"),
1333            ) else {
1334                return Ok(String::new());
1335            };
1336            let indent = args
1337                .get::<String>("indent")?
1338                .unwrap_or_default()
1339                .chars()
1340                .count();
1341            let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1342            let rendered = crate::docs::layout::render_indented_text(&label, width, indent);
1343            Ok(indent_text(&rendered, indent))
1344        },
1345    );
1346
1347    #[rustfmt::skip]
1348    tera.add_raw_templates([
1349        ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
1350        ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
1351    ]).unwrap();
1352
1353    tera
1354});
1355
1356#[cfg(test)]
1357mod tests {
1358    use super::*;
1359    use insta::assert_snapshot;
1360
1361    #[test]
1362    fn flag_aliases_do_not_leak_into_interactive_help() {
1363        let spec = crate::spec! { r#"
1364bin "ex"
1365flag "-t -f --tail --follow" help="Follow output"
1366        "# }
1367        .unwrap();
1368
1369        for long in [false, true] {
1370            let page = super::render_help(&spec, &spec.cmd, long);
1371            assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1372            assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1373        }
1374    }
1375
1376    #[test]
1377    fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1378        // `--help` is supplied by the parser, and a hidden global that declares it still binds
1379        // first — `hide` keeps a flag off the page, not out of the parse. Deciding the supplied
1380        // entries from the *visible* inherited list lost exactly that, and the page offered a
1381        // `--help` that does something else.
1382        let spec = crate::spec! { r#"
1383bin "ex"
1384flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1385cmd inner help="a command" {
1386    flag "--plain" help="its own"
1387}
1388        "# }
1389        .unwrap();
1390
1391        let inner = spec.cmd.subcommands.get("inner").expect("inner");
1392        for long in [false, true] {
1393            let page = super::render_help(&spec, inner, long);
1394            assert!(
1395                !page.contains("--help"),
1396                "long={long}: a hidden ancestor binds this:\n{page}"
1397            );
1398            // The short form is untouched, since nothing claimed it.
1399            assert!(page.contains("-h"), "long={long}:\n{page}");
1400        }
1401    }
1402
1403    #[test]
1404    fn a_long_beats_a_negation_however_far_away_it_is() {
1405        // A negation is stored *with* its dashes here and without them in usage-argv, so the
1406        // spelling was being looked up as `----no-cache` and matched nothing — negations were
1407        // counted in name only. And which one binds is not about distance: a word is resolved
1408        // against every long in scope before any negation is considered, so the root's plain
1409        // `--no-cache` wins over the subcommand's negation and belongs on its page.
1410        let spec = crate::spec! { r#"
1411bin "ex"
1412flag "--no-cache" global=#true help="the root's plain long"
1413flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1414cmd narrow help="a command" {
1415    flag "--cache" negate="--no-cache" help="its own, with a negation"
1416    flag "--tint" negate="--no-colour" help="claims the root's negation"
1417}
1418        "# }
1419        .unwrap();
1420
1421        let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1422        for long in [false, true] {
1423            let page = super::render_help(&spec, narrow, long);
1424            assert!(
1425                page.contains("--no-cache"),
1426                "long={long}: a long beats a negation, so this still binds here:\n{page}"
1427            );
1428            // And a negation *is* claimed by a nearer negation — which is what the dashes
1429            // matter for. `--colour` stays; the negation it used to carry does not.
1430            assert!(page.contains("--colour"), "long={long}:\n{page}");
1431            let global = page
1432                .split_once("Global flags:")
1433                .expect("a global section")
1434                .1;
1435            assert!(
1436                !global.contains("--colour / --no-colour"),
1437                "long={long}: the nearer negation owns that spelling:\n{page}"
1438            );
1439        }
1440    }
1441
1442    #[test]
1443    fn a_description_of_only_spaces_is_no_description() {
1444        // `usage-argv` filters a blank description wherever it reads one, and this template
1445        // asked only whether the string was there — so `help="   "` bought a column of padding
1446        // and a line of trailing spaces here and nothing there. Two renderings of one spec.
1447        //
1448        // Asserted on the trailing whitespace rather than by comparing the two renderers, so
1449        // the test says what is wrong with the line rather than only that they disagree.
1450        let spec = crate::spec! { r#"
1451bin "ex"
1452flag "--blank" help="   "
1453flag "--plain" help="plain"
1454        "# }
1455        .unwrap();
1456
1457        for long in [false, true] {
1458            let page = super::render_help(&spec, &spec.cmd, long);
1459            // In the flags section, not the usage line — `Usage: ex [--blank] [--plain]`
1460            // also contains the name and has no padding to get wrong.
1461            let listing = page.split_once("\nFlags:").expect("a flags section").1;
1462            let line = listing
1463                .lines()
1464                .find(|l| l.contains("--blank"))
1465                .unwrap_or_else(|| panic!("long={long}: {page}"));
1466            assert_eq!(
1467                line,
1468                line.trim_end(),
1469                "long={long}: trailing space on {line:?}"
1470            );
1471        }
1472    }
1473
1474    #[test]
1475    fn test_render_help_omits_hidden_entries() {
1476        let spec = crate::spec! { r#"
1477bin "ex"
1478flag "--visible" help="shown"
1479flag "--secret" hide=#true help="hidden"
1480flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1481arg "[SHOWN]" help="an arg"
1482arg "[HIDDEN]" hide=#true help="a hidden arg"
1483cmd open help="a command"
1484cmd sneaky hide=#true help="a hidden command"
1485        "# }
1486        .unwrap();
1487
1488        // `hide` keeps something out of help. The usage line filtered already — through
1489        // `SpecCommand::usage` — so before this, `ex --help` listed a `--secret` the line
1490        // above it did not mention. A heading whose every entry is hidden produces no
1491        // section, which is the rule markdown rendering already followed.
1492        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1493        Usage: ex [--visible] [SHOWN] [SUBCOMMAND]
1494
1495        Commands:
1496          open  a command
1497          help  Print this message or the help of the given subcommand(s)
1498
1499        Arguments:
1500          [SHOWN]  an arg
1501
1502        Flags:
1503              --visible  shown
1504          -h, --help     Print help
1505        ");
1506    }
1507
1508    #[test]
1509    fn test_render_help_groups_by_heading() {
1510        let spec = crate::spec! { r#"
1511bin "testcli"
1512flag "--verbose" help="Verbose output"
1513flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1514flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1515flag "--jobs <n>" help="How many at once" help_heading="Performance"
1516arg "<file>" help="The file"
1517arg "<mode>" help="How to run" help_heading="Behaviour"
1518        "# }
1519        .unwrap();
1520
1521        // Unheaded entries keep the default title and come first; each heading
1522        // then gets its own section, in the order the headings first appear.
1523        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1524        Usage: testcli [FLAGS] <file> <mode>
1525
1526        Arguments:
1527          <file>  The file
1528
1529        Behaviour:
1530          <mode>  How to run
1531
1532        Flags:
1533              --verbose            Verbose output
1534          -h, --help               Print help
1535
1536        Filtering:
1537              --filter <pattern>   Only matching
1538              --exclude <pattern>  Skip matching
1539
1540        Performance:
1541              --jobs <n>           How many at once
1542        ");
1543    }
1544
1545    #[test]
1546    fn test_render_help_with_only_headed_flags() {
1547        // No default section when nothing lands in it: a CLI that gives every
1548        // flag a heading should not get an empty "Flags:".
1549        let spec = crate::spec! { r#"
1550bin "testcli"
1551flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1552        "# }
1553        .unwrap();
1554
1555        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1556        Usage: testcli [--filter <pattern>]
1557
1558        Flags:
1559          -h, --help              Print help
1560
1561        Filtering:
1562              --filter <pattern>  Only matching
1563        ");
1564    }
1565
1566    #[test]
1567    fn test_render_help_with_env() {
1568        let spec = crate::spec! { r#"
1569bin "testcli"
1570flag "--color" env="MYCLI_COLOR" help="Enable color output"
1571flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1572flag "--debug" help="Debug mode"
1573        "# }
1574        .unwrap();
1575
1576        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1577        Usage: testcli [FLAGS]
1578
1579        Flags:
1580              --color    Enable color output [env: MYCLI_COLOR]
1581              --verbose  Verbose output [env: MYCLI_VERBOSE]
1582              --debug    Debug mode
1583          -h, --help     Print help
1584        ");
1585
1586        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1587        Usage: testcli [FLAGS]
1588
1589        Flags:
1590              --color    Enable color output
1591                         [env: MYCLI_COLOR]
1592              --verbose  Verbose output
1593                         [env: MYCLI_VERBOSE]
1594              --debug    Debug mode
1595          -h, --help     Print help
1596        ");
1597    }
1598
1599    #[test]
1600    fn test_render_help_with_arg_env() {
1601        let spec = crate::spec! { r#"
1602bin "testcli"
1603arg "<input>" env="MY_INPUT" help="Input file"
1604arg "<output>" env="MY_OUTPUT" help="Output file"
1605arg "<extra>" help="Extra arg without env"
1606arg "[default]" help="Arg with default value" default="default value"
1607        "# }
1608        .unwrap();
1609
1610        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1611        Usage: testcli <ARGS>…
1612
1613        Arguments:
1614          <input>    Input file [env: MY_INPUT]
1615          <output>   Output file [env: MY_OUTPUT]
1616          <extra>    Extra arg without env
1617          [default]  Arg with default value (default: default value)
1618
1619        Flags:
1620          -h, --help  Print help
1621        ");
1622
1623        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1624        Usage: testcli <ARGS>…
1625
1626        Arguments:
1627          <input>    Input file
1628                     [env: MY_INPUT]
1629          <output>   Output file
1630                     [env: MY_OUTPUT]
1631          <extra>    Extra arg without env
1632          [default]  Arg with default value
1633                     (default: default value)
1634
1635        Flags:
1636          -h, --help  Print help
1637        ");
1638    }
1639
1640    #[test]
1641    fn test_render_help_with_negated_flag() {
1642        let spec = crate::spec! { r#"
1643bin "testcli"
1644flag "--compress" negate="--no-compress" default=#true help="Compress output"
1645flag "--verbose" help="Verbose output"
1646        "# }
1647        .unwrap();
1648
1649        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1650        Usage: testcli [--compress] [--verbose]
1651
1652        Flags:
1653              --compress / --no-compress  Compress output (default: true)
1654              --verbose                   Verbose output
1655          -h, --help                      Print help
1656        ");
1657
1658        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1659        Usage: testcli [--compress] [--verbose]
1660
1661        Flags:
1662              --compress / --no-compress  Compress output
1663                                          (default: true)
1664              --verbose                   Verbose output
1665          -h, --help                      Print help
1666        ");
1667    }
1668
1669    #[test]
1670    fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1671        let spec = crate::spec! { r#"
1672bin "testcli"
1673flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1674  choices {
1675    choice "fast"
1676    choice "slow"
1677  }
1678}
1679flag "--short-only" help="short" hide_long_help=#true
1680flag "--long-only" help="long" hide_short_help=#true
1681arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1682        "# }
1683        .unwrap();
1684
1685        let short = render_help(&spec, &spec.cmd, false);
1686        assert!(short.contains("--mode <mode>"), "{short}");
1687        assert!(short.contains("--short-only"), "{short}");
1688        assert!(!short.contains("--long-only"), "{short}");
1689        assert!(
1690            !short.contains("MODE") && !short.contains("fast, slow"),
1691            "{short}"
1692        );
1693        assert!(
1694            !short.contains("default: fast") && !short.contains("default: file"),
1695            "{short}"
1696        );
1697
1698        let long = render_help(&spec, &spec.cmd, true);
1699        assert!(long.contains("--long-only"), "{long}");
1700        assert!(!long.contains("--short-only"), "{long}");
1701        assert!(
1702            !long.contains("MODE") && !long.contains("possible values"),
1703            "{long}"
1704        );
1705
1706        let rendered = spec.to_string();
1707        let reparsed: crate::Spec = rendered.parse().unwrap();
1708        assert!(reparsed.cmd.flags[0].hide_default_value);
1709        assert!(reparsed.cmd.flags[0].hide_env);
1710        assert!(reparsed.cmd.flags[0].hide_possible_values);
1711    }
1712
1713    #[test]
1714    fn a_help_template_reorders_omits_and_wraps_the_sections() {
1715        // The whole of what a template can do: `{{flags}}` before `{{args}}`, no
1716        // `{{commands}}` at all, and text of the author's own around them. Nothing else is
1717        // substituted, so the layout is the spec's and the sections' contents are not.
1718        let spec = crate::spec! { r#"
1719bin "ex"
1720about "An example"
1721help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1722flag "--force" help="Do it anyway"
1723arg "<file>" help="Which file"
1724cmd "run" help="Run it"
1725        "# }
1726        .unwrap();
1727
1728        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1729        An example
1730
1731        Usage: ex [--force] <file> [SUBCOMMAND]
1732
1733        Flags:
1734              --force  Do it anyway
1735          -h, --help   Print help
1736
1737        Arguments:
1738          <file>  Which file
1739
1740        -- ask a person --
1741        ");
1742    }
1743
1744    #[test]
1745    fn a_template_places_the_sections_a_page_actually_has() {
1746        // A template names every section, and this command has no arguments — the gap
1747        // `{{args}}` would leave closes up rather than pushing the commands down the page.
1748        // What lets one template serve a whole CLI, since most commands are missing most
1749        // sections. Here the version banner and description are last, and `after_help`
1750        // carries them nothing.
1751        let spec = crate::spec! { r#"
1752bin "ex"
1753version "1.2.3"
1754about "An example"
1755after_help "Read the docs."
1756help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1757flag "--force" help="Do it anyway"
1758cmd "run" help="Run it"
1759        "# }
1760        .unwrap();
1761
1762        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1763        Usage: ex [--force] [SUBCOMMAND]
1764
1765        Flags:
1766              --force    Do it anyway
1767          -h, --help     Print help
1768          -V, --version  Print version
1769
1770        Commands:
1771          run   Run it
1772          help  Print this message or the help of the given subcommand(s)
1773
1774        Read the docs.
1775
1776        ex 1.2.3
1777        An example
1778        ");
1779    }
1780
1781    #[test]
1782    fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1783        // `flatten_help` replaces a command list with the subcommands' own bodies, so a
1784        // template that places `{{commands}}` places whichever of the two this command has.
1785        let spec = crate::spec! { r#"
1786bin "ex"
1787flatten_help #true
1788help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1789cmd "run" help="Run it" {
1790    flag "--dry-run" help="Only show changes"
1791}
1792        "# }
1793        .unwrap();
1794
1795        let page = render_help(&spec, &spec.cmd, false);
1796        assert!(
1797            page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1798            "{page}"
1799        );
1800        assert!(page.contains("--dry-run"), "{page}");
1801    }
1802
1803    #[test]
1804    fn test_render_help_with_before_after_help() {
1805        let spec = crate::spec! { r#"
1806bin "testcli"
1807before_help "This text appears before the help"
1808after_help "This text appears after the help"
1809flag "--verbose" help="Enable verbose output"
1810        "# }
1811        .unwrap();
1812
1813        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1814        This text appears before the help
1815
1816        Usage: testcli [--verbose]
1817
1818        Flags:
1819              --verbose  Enable verbose output
1820          -h, --help     Print help
1821
1822        This text appears after the help
1823        ");
1824    }
1825
1826    #[test]
1827    fn test_render_help_with_before_after_help_long() {
1828        let spec = crate::spec! { r#"
1829bin "testcli"
1830before_help "short before"
1831before_help_long "This is the long version of before help"
1832after_help "short after"
1833after_help_long "This is the long version of after help"
1834flag "--verbose" help="Enable verbose output"
1835        "# }
1836        .unwrap();
1837
1838        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1839        short before
1840
1841        Usage: testcli [--verbose]
1842
1843        Flags:
1844              --verbose  Enable verbose output
1845          -h, --help     Print help
1846
1847        short after
1848        ");
1849
1850        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1851        This is the long version of before help
1852
1853        Usage: testcli [--verbose]
1854
1855        Flags:
1856              --verbose  Enable verbose output
1857          -h, --help     Print help
1858
1859        This is the long version of after help
1860        ");
1861    }
1862
1863    #[test]
1864    fn root_help_stays_on_the_root_page() {
1865        let spec = crate::spec! { r#"
1866bin "testcli"
1867author "Root Author"
1868license "MIT"
1869before_help "root before"
1870before_help_long "root before long"
1871after_help "root after"
1872after_help_long "root after long"
1873example "testcli --verbose"
1874cmd "run" help="run it" {
1875    after_help "run after"
1876    after_long_help "run after long"
1877    example "testcli run"
1878    cmd "now" help="run it now"
1879}
1880        "# }
1881        .unwrap();
1882        let run = spec.cmd.subcommands.get("run").expect("run command");
1883        let now = run.subcommands.get("now").expect("now command");
1884
1885        for long in [false, true] {
1886            let page = render_help(&spec, now, long);
1887            for root_only in [
1888                "root before",
1889                "root after",
1890                "$ testcli --verbose",
1891                "run after",
1892                "$ testcli run",
1893                "Root Author",
1894                "License: MIT",
1895            ] {
1896                assert!(
1897                    !page.contains(root_only),
1898                    "long={long}, inherited ancestor metadata {root_only:?}:\n{page}"
1899                );
1900            }
1901        }
1902
1903        let short = render_help(&spec, run, false);
1904        let long = render_help(&spec, run, true);
1905        assert!(short.contains("run after"), "{short}");
1906        assert!(short.contains("$ testcli run"), "{short}");
1907        assert!(long.contains("run after long"), "{long}");
1908        assert!(long.contains("$ testcli run"), "{long}");
1909    }
1910
1911    #[test]
1912    fn test_render_help_with_examples() {
1913        let spec = crate::spec! { r#"
1914bin "testcli"
1915flag "--verbose" help="Enable verbose output"
1916example "testcli --verbose" header="Run with verbose output"
1917example "testcli" header="Run normally" help="Just runs the tool"
1918        "# }
1919        .unwrap();
1920
1921        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1922        Usage: testcli [--verbose]
1923
1924        Flags:
1925              --verbose  Enable verbose output
1926          -h, --help     Print help
1927
1928        Examples:
1929          Run with verbose output:
1930            $ testcli --verbose
1931
1932          Run normally:
1933            $ testcli
1934        ");
1935
1936        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1937        Usage: testcli [--verbose]
1938
1939        Flags:
1940              --verbose  Enable verbose output
1941          -h, --help     Print help
1942
1943        Examples:
1944          Run with verbose output:
1945            $ testcli --verbose
1946
1947          Run normally:
1948            Just runs the tool
1949            $ testcli
1950        ");
1951    }
1952
1953    #[test]
1954    fn test_render_help_with_version() {
1955        let spec = crate::spec! { r#"
1956bin "testcli"
1957name "TestCLI"
1958version "1.2.3"
1959flag "--verbose" help="Enable verbose output"
1960        "# }
1961        .unwrap();
1962
1963        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1964        TestCLI 1.2.3
1965        Usage: testcli [--verbose]
1966
1967        Flags:
1968              --verbose  Enable verbose output
1969          -h, --help     Print help
1970          -V, --version  Print version
1971        ");
1972    }
1973
1974    #[test]
1975    fn test_render_help_with_only_long_version() {
1976        let spec = crate::spec! { r#"
1977bin "testcli"
1978long_version "1.2.3\ncommit abc123"
1979flag "--verbose" help="Enable verbose output"
1980        "# }
1981        .unwrap();
1982
1983        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1984        Usage: testcli [--verbose]
1985
1986        Flags:
1987              --verbose  Enable verbose output
1988          -h, --help     Print help
1989          -V, --version  Print version
1990        ");
1991    }
1992
1993    #[test]
1994    fn test_render_help_omits_help_when_disabled() {
1995        // `disable_help` turns the parser's answer off, so the page must not offer it: the same
1996        // rule as a spelling the CLI claimed, with the spec doing the claiming. `--version`
1997        // stays, because nothing disabled that.
1998        let spec = crate::spec! { r#"
1999bin "testcli"
2000version "1.2.3"
2001disable_help #true
2002flag "--verbose" help="Enable verbose output"
2003        "# }
2004        .unwrap();
2005
2006        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2007        testcli 1.2.3
2008        Usage: testcli [--verbose]
2009
2010        Flags:
2011              --verbose  Enable verbose output
2012          -V, --version  Print version
2013        ");
2014    }
2015
2016    #[test]
2017    fn test_render_help_with_author_license() {
2018        let spec = crate::spec! { r#"
2019bin "testcli"
2020author "Test Author"
2021license "MIT"
2022flag "--verbose" help="Enable verbose output"
2023        "# }
2024        .unwrap();
2025
2026        // Short help should not show author/license
2027        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2028        Usage: testcli [--verbose]
2029
2030        Flags:
2031              --verbose  Enable verbose output
2032          -h, --help     Print help
2033        ");
2034
2035        // Long help should show author/license at the bottom
2036        assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
2037        Usage: testcli [--verbose]
2038
2039        Flags:
2040              --verbose  Enable verbose output
2041          -h, --help     Print help
2042
2043        Author: Test Author
2044        License: MIT
2045        ");
2046    }
2047
2048    #[test]
2049    fn test_render_help_with_deprecated_command() {
2050        // The width is declared because the wrapping is the thing being pinned: a page that
2051        // asked the terminal would wrap this snapshot wherever the developer's window ends.
2052        let spec = crate::spec! { r#"
2053bin "testcli"
2054term_width 80
2055flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
2056cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
2057cmd "new-cmd" help="Do something better"
2058        "# }
2059        .unwrap();
2060
2061        assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2062        Usage: testcli [--old] [SUBCOMMAND]
2063
2064        Commands:
2065          new-cmd  Do something better
2066          old-cmd  Do something [deprecated: use new-cmd instead; warns at 6.2; removed
2067                   at 7.0]
2068          help     Print this message or the help of the given subcommand(s)
2069
2070        Flags:
2071              --old   Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
2072          -h, --help  Print help
2073        ");
2074    }
2075
2076    #[test]
2077    fn deprecation_milestones_do_not_need_a_message() {
2078        let spec = crate::spec! { r#"
2079bin "testcli"
2080flag "--old" help="Old switch" deprecated_remove_at="7.0"
2081cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
2082        "# }
2083        .unwrap();
2084
2085        let page = render_help(&spec, &spec.cmd, false);
2086        assert!(
2087            page.contains("old-cmd  Do something [deprecated: warns at 6.2]"),
2088            "{page}"
2089        );
2090        assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
2091        assert!(!page.contains("[deprecated:;"), "{page}");
2092    }
2093
2094    #[test]
2095    fn test_render_help_with_subcommand_presentation() {
2096        let spec = crate::spec! { r#"
2097bin "testcli"
2098subcommand_help_heading "Actions"
2099subcommand_value_name "ACTION"
2100cmd "run" help="Run it\n"
2101        "# }
2102        .unwrap();
2103
2104        let page = render_help(&spec, &spec.cmd, false);
2105        assert!(page.contains("Usage: testcli [ACTION]"), "{page}");
2106        assert!(page.contains("\nActions:\n"), "{page}");
2107    }
2108
2109    #[test]
2110    fn test_render_help_honors_explicit_display_order() {
2111        let spec = crate::spec! { r#"
2112bin "testcli"
2113flag "--unset" help="Unordered"
2114flag "--later" help="Later" display_order=20
2115flag "--first" help="First" display_order=10
2116cmd "zulu" help="Unordered"
2117cmd "later" help="Later" display_order=20
2118cmd "first" help="First" display_order=10
2119cmd "alpha" help="Unordered"
2120        "# }
2121        .unwrap();
2122
2123        let page = render_help(&spec, &spec.cmd, false);
2124        let commands = page.split_once("\nCommands:\n").unwrap().1;
2125        assert!(
2126            commands.find("first").unwrap() < commands.find("later").unwrap()
2127                && commands.find("later").unwrap() < commands.find("alpha").unwrap()
2128                && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
2129            "{page}"
2130        );
2131        let flags = page.split_once("\nFlags:\n").unwrap().1;
2132        assert!(
2133            flags.find("--first").unwrap() < flags.find("--later").unwrap()
2134                && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
2135            "{page}"
2136        );
2137    }
2138
2139    #[test]
2140    fn test_render_help_groups_subcommands_by_heading() {
2141        let spec = crate::spec! { r#"
2142bin "testcli"
2143cmd "run" help="Run it" help_heading="Core commands"
2144cmd "clean" help="Remove old state" help_heading="Maintenance"
2145cmd "status" help="Show status" help_heading="Commands"
2146        "# }
2147        .unwrap();
2148
2149        for page in [
2150            render_help(&spec, &spec.cmd, false),
2151            render_help(&spec, &spec.cmd, true),
2152        ] {
2153            let commands = page.find("\nCommands:\n").expect("default command section");
2154            assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
2155            let core = page.find("\nCore commands:\n").expect("core section");
2156            let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
2157            assert!(commands < core && commands < maintenance, "{page}");
2158            let default_end = core.min(maintenance);
2159            assert!(page[commands..default_end].contains("status"), "{page}");
2160            assert!(page[commands..default_end].contains("help"), "{page}");
2161            assert!(page[core..].contains("run"), "{page}");
2162            assert!(page[maintenance..].contains("clean"), "{page}");
2163        }
2164    }
2165
2166    #[test]
2167    fn test_render_help_with_next_line_layout() {
2168        let spec = crate::spec! { r#"
2169bin "testcli"
2170next_line_help #true
2171arg "<input>" help="Input file" env="INPUT" default="fast" {
2172    choices {
2173        choice "fast"
2174        choice "slow"
2175    }
2176}
2177flag "--verbose" help="Enable verbose output"
2178cmd "run" help="Run it"
2179        "# }
2180        .unwrap();
2181
2182        let short = render_help(&spec, &spec.cmd, false);
2183        assert!(!short.contains("    Run it\n\n  help"), "{short}");
2184        for page in [short, render_help(&spec, &spec.cmd, true)] {
2185            assert!(page.contains("  [input]\n    Input file"), "{page}");
2186            assert!(
2187                page.contains("--verbose\n    Enable verbose output"),
2188                "{page}"
2189            );
2190            assert!(
2191                page.contains(
2192                    "    [possible values: fast, slow]\n    [env: INPUT]\n    (default: fast)"
2193                ),
2194                "{page}"
2195            );
2196            assert!(page.contains("  run\n    Run it"), "{page}");
2197        }
2198    }
2199
2200    #[test]
2201    fn flatten_help_expands_subcommands_instead_of_listing_them() {
2202        let spec = crate::spec! { r#"
2203bin "testcli"
2204flatten_help #true
2205next_line_help #true
2206cmd "run" help="Run it" {
2207    arg "<task>" help="Task name" env="TASK" default="build" {
2208        choices {
2209            choice "build"
2210            choice "test"
2211        }
2212    }
2213    flag "--dry-run" help="Only show changes"
2214    flatten_help #true
2215    cmd "nested" help="Nested operation" {
2216        flag "--deep" help="Deep option"
2217    }
2218}
2219        "# }
2220        .unwrap();
2221
2222        for page in [
2223            render_help(&spec, &spec.cmd, false),
2224            render_help(&spec, &spec.cmd, true),
2225        ] {
2226            assert!(
2227                page.contains("Usage: testcli\n       testcli run"),
2228                "{page}"
2229            );
2230            assert!(!page.contains("\nCommands:\n"), "{page}");
2231            assert!(page.contains("\nrun:\nRun it"), "{page}");
2232            assert!(page.contains("[task]"), "{page}");
2233            assert!(page.contains("--dry-run"), "{page}");
2234            assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
2235            assert!(page.contains("--deep"), "{page}");
2236            assert!(
2237                page.contains(
2238                    "    [possible values: build, test]\n    [env: TASK]\n    (default: build)"
2239                ),
2240                "{page}"
2241            );
2242        }
2243    }
2244
2245    #[test]
2246    fn styled_help_colours_semantics_and_template_styles() {
2247        let mut spec = crate::spec! { r#"
2248bin "testcli"
2249help_template "{$bright-blue}My tool{/$}\n\n{{usage}}\n\n{{commands}}\n\n{{flags}}\n\n{{after_help}}"
2250cmd "build" help="Build it"
2251flag "--output <FILE>" help="Write **the file**"
2252        "# }
2253        .unwrap();
2254        spec.cmd.after_help_long = Some(
2255            "\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n    \u{1b}[1mtestcli run\u{1b}[22m"
2256                .to_string(),
2257        );
2258
2259        let plain = render_help(&spec, &spec.cmd, true);
2260        assert!(plain.contains("My tool"), "{plain}");
2261        assert!(plain.contains("Examples:\n\n    testcli run"), "{plain}");
2262        assert!(!plain.contains('\u{1b}'), "{plain:?}");
2263
2264        let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2265        assert!(
2266            coloured.contains("\u{1b}[94mMy tool\u{1b}[0m"),
2267            "{coloured:?}"
2268        );
2269        assert!(
2270            coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"),
2271            "{coloured:?}"
2272        );
2273        assert!(
2274            coloured.contains("\u{1b}[1;32m--output\u{1b}[0m"),
2275            "{coloured:?}"
2276        );
2277        assert!(
2278            coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"),
2279            "{coloured:?}"
2280        );
2281        assert!(
2282            coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m  Build it"),
2283            "{coloured:?}"
2284        );
2285        assert!(
2286            coloured.contains(
2287                "\u{1b}[1;32mhelp\u{1b}[0m   Print this message or the help of the given subcommand(s)"
2288            ),
2289            "{coloured:?}"
2290        );
2291        assert!(
2292            coloured.contains("\u{1b}[1mthe file\u{1b}[22m"),
2293            "{coloured:?}"
2294        );
2295        assert!(
2296            coloured.contains("\u{1b}[1m\u{1b}[4mExamples:"),
2297            "{coloured:?}"
2298        );
2299    }
2300
2301    #[test]
2302    fn command_style_does_not_match_command_group_prose() {
2303        let spec = crate::spec! { r#"
2304bin "testcli"
2305heading "Build commands" help="build these projects before publishing"
2306cmd "build" help="Build it" help_heading="Build commands"
2307        "# }
2308        .unwrap();
2309
2310        let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2311        assert!(
2312            coloured.contains("  build these projects before publishing"),
2313            "{coloured:?}"
2314        );
2315        assert!(
2316            coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m  Build it"),
2317            "{coloured:?}"
2318        );
2319        assert!(
2320            !coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m these projects"),
2321            "{coloured:?}"
2322        );
2323    }
2324
2325    #[test]
2326    fn flattened_help_does_not_apply_command_style() {
2327        let spec = crate::spec! { r#"
2328bin "testcli"
2329flatten_help #true
2330help_template "Intro\n  run  should stay prose\n\n{{commands}}"
2331cmd "run" help="Run it"
2332        "# }
2333        .unwrap();
2334
2335        let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2336        assert!(
2337            coloured.contains("  run  should stay prose"),
2338            "{coloured:?}"
2339        );
2340        assert!(
2341            !coloured.contains("\u{1b}[1;32mrun\u{1b}[0m"),
2342            "{coloured:?}"
2343        );
2344    }
2345}