Skip to main content

usage/docs/cli/
mod.rs

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