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