Skip to main content

winnow_args/
help.rs

1//! Help text: `static` data the derive emits, rendered only when asked for.
2//!
3//! Nothing here runs during a successful parse. A command's [`Command`] sits in
4//! read-only data until `--help` (or `-h`) returns it inside an
5//! [`ErrorKind::HelpRequested`](crate::ErrorKind::HelpRequested), and
6//! [`render`] turns it into text.
7
8use std::fmt::Write as _;
9
10use crate::color::Paint;
11pub use crate::color::{Depth, Palette, Theme};
12
13/// One command's help: what it is, what it takes, what it contains.
14///
15/// Written by the derive, as [`Item`] and [`Sub`] are. Fields are added as
16/// help learns to show more: a value written by hand ends in
17/// `..Command::EMPTY`, and so keeps compiling when one is.
18#[derive(Debug)]
19pub struct Command {
20    /// The command's own name; empty for a root that leaves it to `argv[0]`.
21    pub name: &'static str,
22    /// First paragraph of the description.
23    pub about: &'static str,
24    /// The whole description, for `--help`.
25    pub long_about: &'static str,
26    /// Text after the lists, for `-h`.
27    pub after_help: &'static str,
28    /// Text after the lists, for `--help`.
29    pub after_long_help: &'static str,
30    /// Flags and positionals, in declaration order.
31    pub items: &'static [Item],
32    /// Subcommands, in declaration order.
33    pub subcommands: &'static [Sub],
34    /// Whether a subcommand must be given.
35    pub subcommand_required: bool,
36    /// Whether `--help` is supplied (not declared or disabled).
37    pub help_flag: bool,
38    /// Whether the supplied help flag has `-h` as well as `--help`.
39    pub help_short: bool,
40    /// The version, when `-V`/`--version` are supplied.
41    pub version: Option<&'static str>,
42    /// The subcommand a line that names none runs: marked `(default)` in
43    /// the list.
44    pub default_subcommand: Option<&'static str>,
45    /// Whether a flag this command does not take selects the default
46    /// subcommand too.
47    pub default_subcommand_flags: bool,
48    /// Whether the default subcommand's page follows this one.
49    pub default_subcommand_help: bool,
50    /// Whether each subcommand's usage line, description and arguments stand
51    /// where the list of subcommands would.
52    pub flatten_help: bool,
53    /// Command lines to try, in an Examples section.
54    pub examples: &'static [Example],
55    /// What `--help` says under a heading, before its entries.
56    pub headings: &'static [Heading],
57}
58
59impl Command {
60    /// A command that takes nothing and says nothing. A value written by
61    /// hand ends in `..Command::EMPTY`, and a release that adds a field does
62    /// not break it.
63    pub const EMPTY: Command = Command {
64        name: "",
65        about: "",
66        long_about: "",
67        after_help: "",
68        after_long_help: "",
69        items: &[],
70        subcommands: &[],
71        subcommand_required: false,
72        help_flag: false,
73        help_short: false,
74        version: None,
75        default_subcommand: None,
76        default_subcommand_flags: false,
77        default_subcommand_help: false,
78        flatten_help: false,
79        examples: &[],
80        headings: &[],
81    };
82
83    /// The subcommand a line that names none runs.
84    pub(crate) fn default_target(&self) -> Option<&'static Sub> {
85        let name = self.default_subcommand?;
86        let subs = self.subcommands;
87        subs.iter()
88            .find(|sub| sub.name == name)
89            .or_else(|| subs.iter().find(|sub| sub.names.contains(&name)))
90    }
91
92    /// [`Command::default_target`], when help shows it.
93    fn default_sub(&self) -> Option<&'static Sub> {
94        self.default_target().filter(|sub| !sub.hide)
95    }
96
97    /// What is said under the heading `title`.
98    fn heading_help(&self, title: &str) -> Option<&'static str> {
99        let heading = self.headings.iter().find(|h| h.title == title)?;
100        Some(heading.help).filter(|help| !help.is_empty())
101    }
102}
103
104/// A command line to try.
105#[derive(Debug, Clone, Copy)]
106pub struct Example {
107    /// The line, as typed: `prog build --release`.
108    pub code: &'static str,
109    /// What it is an example of, on a line of its own above; empty for none.
110    pub header: &'static str,
111    /// What it does, for `--help`; empty for none.
112    pub help: &'static str,
113}
114
115/// What a section of the page says before its entries.
116#[derive(Debug, Clone, Copy)]
117pub struct Heading {
118    /// The heading, as an item's [`Item::heading`] spells it.
119    pub title: &'static str,
120    /// The paragraph under it, for `--help`.
121    pub help: &'static str,
122}
123
124/// A flag or positional.
125#[derive(Debug, Clone, Copy)]
126pub struct Item {
127    /// `-c`.
128    pub short: Option<char>,
129    /// `--name`.
130    pub long: Option<&'static str>,
131    /// Other long names it answers to, not shown.
132    pub aliases: &'static [&'static str],
133    /// `--no-name`, the spelling that sets it false.
134    pub negate: Option<&'static str>,
135    /// The value's placeholder; `None` for a switch.
136    pub value_name: Option<&'static str>,
137    /// First paragraph of the description.
138    pub help: &'static str,
139    /// The whole description.
140    pub long_help: &'static str,
141    /// Section to list it under instead of "Options".
142    pub heading: Option<&'static str>,
143    /// Left out of help.
144    pub hide: bool,
145    /// A positional rather than a flag.
146    pub positional: bool,
147    /// Must be given.
148    pub required: bool,
149    /// Takes several values or occurrences.
150    pub multiple: bool,
151    /// A positional that only takes words after `--`.
152    pub trailing: bool,
153    /// The value used when none is given.
154    pub default: Option<&'static str>,
155    /// The environment variable consulted.
156    pub env: Option<&'static str>,
157    /// The only values accepted.
158    pub choices: &'static [&'static str],
159    /// The value is only taken attached (`--name=value`), never the next word.
160    pub require_equals: bool,
161    /// Also accepted by every subcommand below.
162    pub global: bool,
163    /// Other letters it answers to: `-l`, `-L`.
164    pub more_shorts: &'static [char],
165    /// The letter of its `+c` spelling.
166    pub plus: Option<char>,
167    /// The value may be left out.
168    pub optional_value: bool,
169    /// Words each occurrence takes; 1 unless declared `values = N`.
170    pub values: usize,
171    /// It may take more than `values` (`values = 1..`): shown as `<V>...`.
172    pub more_values: bool,
173    /// What its value is, for the shell that completes it.
174    pub hint: crate::complete::Hint,
175    /// What its value can be, given what is typed of it: asked when a shell
176    /// completes it, in place of its choices and its hint.
177    pub complete: Option<fn(&str) -> Vec<crate::complete::Candidate>>,
178    /// What to know before using it, shown by `--help` as `Warning: …`;
179    /// empty for none.
180    pub warning: &'static str,
181}
182
183impl Item {
184    /// A switch with no name. A value written by hand ends in
185    /// `..Item::EMPTY`, and a release that adds a field does not break it.
186    pub const EMPTY: Item = Item {
187        short: None,
188        long: None,
189        aliases: &[],
190        negate: None,
191        value_name: None,
192        help: "",
193        long_help: "",
194        heading: None,
195        hide: false,
196        positional: false,
197        required: false,
198        multiple: false,
199        trailing: false,
200        default: None,
201        env: None,
202        choices: &[],
203        require_equals: false,
204        global: false,
205        more_shorts: &[],
206        plus: None,
207        optional_value: false,
208        values: 1,
209        more_values: false,
210        hint: crate::complete::Hint::Nothing,
211        complete: None,
212        warning: "",
213    };
214}
215
216/// Whether two items of different `parts` are spelled alike (a short letter,
217/// a long name or its negation): a struct and what it flattens, checked at
218/// compile time.
219#[doc(hidden)]
220pub const fn items_clash(parts: &[&[Item]]) -> bool {
221    const fn same(a: Option<&str>, b: Option<&str>) -> bool {
222        let (Some(a), Some(b)) = (a, b) else {
223            return false;
224        };
225        let (a, b) = (a.as_bytes(), b.as_bytes());
226        if a.len() != b.len() {
227            return false;
228        }
229        let mut i = 0;
230        while i < a.len() {
231            if a[i] != b[i] {
232                return false;
233            }
234            i += 1;
235        }
236        true
237    }
238    /// Whether `item` answers to the letter.
239    const fn has_short(item: &Item, letter: char) -> bool {
240        if matches!(item.short, Some(c) if c == letter) {
241            return true;
242        }
243        let mut i = 0;
244        while i < item.more_shorts.len() {
245            if item.more_shorts[i] == letter {
246                return true;
247            }
248            i += 1;
249        }
250        false
251    }
252    /// Whether `item` answers to the long name: its own, its negation, an alias.
253    const fn has_long(item: &Item, name: Option<&str>) -> bool {
254        if same(item.long, name) || same(item.negate, name) {
255            return true;
256        }
257        let mut i = 0;
258        while i < item.aliases.len() {
259            if same(Some(item.aliases[i]), name) {
260                return true;
261            }
262            i += 1;
263        }
264        false
265    }
266    const fn clash(a: &Item, b: &Item) -> bool {
267        if a.positional || b.positional {
268            return false;
269        }
270        if matches!(a.short, Some(c) if has_short(b, c)) {
271            return true;
272        }
273        let mut i = 0;
274        while i < a.more_shorts.len() {
275            if has_short(b, a.more_shorts[i]) {
276                return true;
277            }
278            i += 1;
279        }
280        if has_long(b, a.long) || has_long(b, a.negate) {
281            return true;
282        }
283        let mut i = 0;
284        while i < a.aliases.len() {
285            if has_long(b, Some(a.aliases[i])) {
286                return true;
287            }
288            i += 1;
289        }
290        false
291    }
292    let mut p = 0;
293    while p < parts.len() {
294        let mut q = p + 1;
295        while q < parts.len() {
296            let (mut i, a, b) = (0, parts[p], parts[q]);
297            while i < a.len() {
298                let mut j = 0;
299                while j < b.len() {
300                    if clash(&a[i], &b[j]) {
301                        return true;
302                    }
303                    j += 1;
304                }
305                i += 1;
306            }
307            q += 1;
308        }
309        p += 1;
310    }
311    false
312}
313
314/// `parts`, one after another, as one array: a struct's own items and those
315/// of the structs it flattens, joined at compile time. `N` is their total
316/// length.
317///
318/// # Panics
319///
320/// At compile time, if `N` is not the total length.
321#[doc(hidden)]
322pub const fn concat_items<const N: usize>(parts: &[&[Item]]) -> [Item; N] {
323    let mut out = [Item::EMPTY; N];
324    let (mut n, mut p) = (0, 0);
325    while p < parts.len() {
326        let part = parts[p];
327        let mut i = 0;
328        while i < part.len() {
329            out[n] = part[i];
330            n += 1;
331            i += 1;
332        }
333        p += 1;
334    }
335    assert!(n == N, "concat_items: the length is not the parts' total");
336    out
337}
338
339/// [`concat_items`] for what is said under headings: a struct's own, then
340/// those of the structs it flattens. The first that names a title is read.
341///
342/// # Panics
343///
344/// At compile time, if `N` is not the total length.
345#[doc(hidden)]
346pub const fn concat_headings<const N: usize>(parts: &[&[Heading]]) -> [Heading; N] {
347    let mut out = [Heading {
348        title: "",
349        help: "",
350    }; N];
351    let (mut n, mut p) = (0, 0);
352    while p < parts.len() {
353        let part = parts[p];
354        let mut i = 0;
355        while i < part.len() {
356            out[n] = part[i];
357            n += 1;
358            i += 1;
359        }
360        p += 1;
361    }
362    assert!(
363        n == N,
364        "concat_headings: the length is not the parts' total"
365    );
366    out
367}
368
369/// A subcommand as its parent lists it.
370#[derive(Debug)]
371pub struct Sub {
372    /// Its name.
373    pub name: &'static str,
374    /// Other spellings shown beside it.
375    pub aliases: &'static [&'static str],
376    /// Every spelling it answers to, hidden aliases included, for `help <name>`.
377    pub names: &'static [&'static str],
378    /// Its own help.
379    pub command: &'static Command,
380    /// One line about it.
381    pub about: &'static str,
382    /// Left out of the list.
383    pub hide: bool,
384}
385
386/// The command a `help a b …` line asks about: follow each word through
387/// `root`'s subcommands until one names none. Gives it and the names followed.
388pub fn resolve<'w>(
389    root: &'static Command,
390    words: impl IntoIterator<Item = &'w [u8]>,
391) -> (&'static Command, Vec<&'static str>) {
392    let (mut command, mut path) = (root, Vec::new());
393    for word in words {
394        let Some(sub) = command
395            .subcommands
396            .iter()
397            .find(|s| s.names.iter().any(|n| n.as_bytes() == word))
398        else {
399            break;
400        };
401        path.push(sub.name);
402        command = sub.command;
403    }
404    (command, path)
405}
406
407/// The width help wraps to: `COLUMNS` when it is a positive number, so a user
408/// or a test can say what to assume; else, with the `terminal-size` feature,
409/// the width of the terminal on standard output; else 100.
410///
411/// Under [`with_env`](crate::with_env) the terminal is not asked: the page
412/// depends only on the environment given.
413pub fn width() -> usize {
414    crate::env::var("COLUMNS")
415        .and_then(|columns| columns.to_str()?.trim().parse().ok())
416        .filter(|&columns| columns > 0)
417        .or_else(terminal_width)
418        .unwrap_or(100)
419}
420
421#[cfg(feature = "terminal-size")]
422fn terminal_width() -> Option<usize> {
423    if crate::env::overridden() {
424        return None;
425    }
426    terminal_size::terminal_size()
427        .map(|(width, _)| usize::from(width.0))
428        .filter(|&width| width > 0)
429}
430
431#[cfg(not(feature = "terminal-size"))]
432fn terminal_width() -> Option<usize> {
433    None
434}
435
436/// How help and errors are painted: a [`Palette`] for a terminal of some
437/// [`Depth`]. The escapes never count toward a column's width.
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub struct Style {
440    /// The paint for each kind of text.
441    pub palette: Palette,
442    /// What the terminal shows; colors deeper than this are mapped down.
443    pub depth: Depth,
444}
445
446/// A paint resolved for a depth: what the renderer writes with.
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub(crate) struct Ink {
449    paint: Paint,
450    depth: Depth,
451}
452
453impl Ink {
454    /// Plain text.
455    const NONE: Ink = Ink {
456        paint: Paint::NONE,
457        depth: Depth::None,
458    };
459}
460
461impl Style {
462    /// Nothing painted: for pipes, files and tests.
463    pub const PLAIN: Style = Style::at(Palette::PLAIN, Depth::None);
464
465    /// [`Palette::DEFAULT`] in the 16 basic colors.
466    pub const COLORED: Style = Style::at(Palette::DEFAULT, Depth::Ansi16);
467
468    /// [`Palette::CLAP`]: bold and underline in help, color only in errors.
469    pub const CLAP: Style = Style::at(Palette::CLAP, Depth::Ansi16);
470
471    /// `palette` on a terminal of `depth`.
472    pub const fn at(palette: Palette, depth: Depth) -> Style {
473        Style { palette, depth }
474    }
475
476    /// [`Theme::DEFAULT`] for standard output, at the depth
477    /// [`Depth::detect`] finds.
478    pub fn auto() -> Style {
479        use std::io::IsTerminal as _;
480        Style::themed(&Theme::DEFAULT, std::io::stdout().is_terminal())
481    }
482
483    /// [`Style::auto`] for standard error, where errors go.
484    pub fn auto_stderr() -> Style {
485        use std::io::IsTerminal as _;
486        Style::themed(&Theme::DEFAULT, std::io::stderr().is_terminal())
487    }
488
489    /// [`Theme::DEFAULT`] for a stream that is (or is not) a terminal.
490    pub fn auto_for(is_terminal: bool) -> Style {
491        Style::themed(&Theme::DEFAULT, is_terminal)
492    }
493
494    /// `theme`'s palette for the depth [`Depth::detect`] finds for a stream
495    /// that is (or is not) a terminal.
496    pub fn themed(theme: &Theme, is_terminal: bool) -> Style {
497        let depth = Depth::detect(is_terminal);
498        Style::at(theme.palette(depth), depth)
499    }
500
501    fn ink(self, paint: Paint) -> Ink {
502        Ink {
503            paint,
504            depth: self.depth,
505        }
506    }
507
508    pub(crate) fn header(self) -> Ink {
509        self.ink(self.palette.header)
510    }
511
512    pub(crate) fn program(self) -> Ink {
513        self.ink(self.palette.program)
514    }
515
516    pub(crate) fn flag(self) -> Ink {
517        self.ink(self.palette.flag)
518    }
519
520    pub(crate) fn command(self) -> Ink {
521        self.ink(self.palette.command)
522    }
523
524    pub(crate) fn dim(self) -> Ink {
525        self.ink(self.palette.dim)
526    }
527
528    pub(crate) fn code(self) -> Ink {
529        self.ink(self.palette.code)
530    }
531
532    pub(crate) fn emphasis(self) -> Ink {
533        self.ink(self.palette.emphasis)
534    }
535
536    pub(crate) fn strong(self) -> Ink {
537        self.ink(self.palette.strong)
538    }
539
540    pub(crate) fn placeholder(self) -> Ink {
541        self.ink(self.palette.placeholder)
542    }
543
544    pub(crate) fn error(self) -> Ink {
545        self.ink(self.palette.error)
546    }
547
548    pub(crate) fn invalid(self) -> Ink {
549        self.ink(self.palette.invalid)
550    }
551
552    pub(crate) fn valid(self) -> Ink {
553        self.ink(self.palette.valid)
554    }
555
556    pub(crate) fn env(self) -> Ink {
557        self.ink(self.palette.env)
558    }
559
560    pub(crate) fn default(self) -> Ink {
561        self.ink(self.palette.default)
562    }
563
564    pub(crate) fn choice(self) -> Ink {
565        self.ink(self.palette.choice)
566    }
567
568    /// `text` in `ink`, reset after.
569    pub(crate) fn paint(ink: Ink, text: &str) -> String {
570        let mut out = String::new();
571        push_painted(&mut out, ink, text);
572        out
573    }
574}
575
576/// Append `text` in `ink` (plain if it paints nothing), reset after.
577fn push_painted(out: &mut String, ink: Ink, text: &str) {
578    ink.paint.write(out, ink.depth, text);
579}
580
581/// Painted text and how many columns it takes on screen.
582#[derive(Default)]
583struct Cell {
584    text: String,
585    width: usize,
586}
587
588impl Cell {
589    fn push(&mut self, ink: Ink, text: &str) {
590        push_painted(&mut self.text, ink, text);
591        self.width += text.chars().count();
592    }
593
594    /// `<NAME>` painted whole, or `[NAME]` with only the name painted: square
595    /// brackets say "optional", they are not part of the value.
596    fn push_placeholder(&mut self, style: Style, required: bool, name: &str) {
597        if required {
598            self.push(style.placeholder(), &format!("<{name}>"));
599        } else {
600            self.push(style.dim(), "[");
601            self.push(style.placeholder(), name);
602            self.push(style.dim(), "]");
603        }
604    }
605}
606
607/// Render `command`'s help, plain, wrapped to [`width`]. `path` is the command
608/// line leading to it (program name, then subcommand names); `long` selects
609/// `--help` over `-h`.
610pub fn render(command: &Command, path: &[&str], long: bool) -> String {
611    render_styled(command, path, long, width(), Style::PLAIN)
612}
613
614/// [`render`], wrapped to `width` columns.
615pub fn render_width(command: &Command, path: &[&str], long: bool, width: usize) -> String {
616    render_styled(command, path, long, width, Style::PLAIN)
617}
618
619/// [`render`], wrapped to `width` columns and painted with `style`.
620pub fn render_styled(
621    command: &Command,
622    path: &[&str],
623    long: bool,
624    width: usize,
625    style: Style,
626) -> String {
627    render_page(command, path, long, width, style, &[])
628}
629
630/// [`render_styled`] for a subcommand, which also takes `inherited`: the
631/// global flags of the commands above it.
632pub(crate) fn render_page(
633    command: &Command,
634    path: &[&str],
635    long: bool,
636    width: usize,
637    style: Style,
638    inherited: &[&Item],
639) -> String {
640    let mut out = String::new();
641    let layout = Layout { long, width, style };
642    page(&mut out, command, path, layout, inherited);
643    // The default subcommand's own page. A flattened page has it already.
644    if let Some(sub) = command.default_sub()
645        && command.default_subcommand_help
646        && !command.flatten_help
647    {
648        trim_blank_lines(&mut out);
649        out.push('\n');
650        heading(&mut out, "Default command:", style);
651        out.push(' ');
652        push_painted(&mut out, style.command(), sub.name);
653        out.push_str("\n  It runs when no command is named.\n\n");
654        let mut path = path.to_vec();
655        path.push(sub.name);
656        // What it inherits is this command's, listed above.
657        page(&mut out, sub.command, &path, layout, &[]);
658    }
659    trim_blank_lines(&mut out);
660    out
661}
662
663/// End `out` with one newline.
664fn trim_blank_lines(out: &mut String) {
665    while out.ends_with("\n\n") {
666        out.pop();
667    }
668}
669
670/// What a page is rendered with.
671#[derive(Clone, Copy)]
672struct Layout {
673    long: bool,
674    width: usize,
675    style: Style,
676}
677
678/// One command's page, appended to `out`.
679fn page(out: &mut String, command: &Command, path: &[&str], layout: Layout, inherited: &[&Item]) {
680    let Layout { long, width, style } = layout;
681    let about = if long && !command.long_about.is_empty() {
682        command.long_about
683    } else {
684        command.about
685    };
686    if !about.is_empty() {
687        let about = painted_marks(about, style);
688        let _ = writeln!(out, "{}\n", wrap_text(&about, width));
689    }
690    let visible_subs: Vec<&Sub> = command.subcommands.iter().filter(|s| !s.hide).collect();
691    let flat = command.flatten_help && !visible_subs.is_empty();
692    push_painted(out, style.header(), "Usage:");
693    let mut lines = Vec::new();
694    if !flat || !command.subcommand_required {
695        lines.push(usage(command, path, style));
696    }
697    if flat {
698        lines.extend(visible_subs.iter().map(|sub| {
699            let mut path = path.to_vec();
700            path.push(sub.name);
701            usage(sub.command, &path, style)
702        }));
703    }
704    for (n, line) in lines.iter().enumerate() {
705        let _ = writeln!(out, "{}{line}", if n == 0 { " " } else { "       " });
706    }
707    out.push('\n');
708
709    if !flat && !visible_subs.is_empty() {
710        let default = command.default_sub().map(|sub| sub.name);
711        let rows: Vec<(Cell, String)> = visible_subs
712            .iter()
713            .map(|s| {
714                let mut name = Cell::default();
715                name.push(style.command(), s.name);
716                for alias in s.aliases {
717                    name.push(Ink::NONE, ", ");
718                    name.push(style.command(), alias);
719                }
720                let mut about = painted_marks(s.about, style);
721                if default == Some(s.name) {
722                    if !about.is_empty() {
723                        about.push(' ');
724                    }
725                    // As the value of a `[default: …]` is.
726                    push_painted(&mut about, style.default(), "(default)");
727                }
728                (name, about)
729            })
730            .collect();
731        // Both pages list a subcommand beside what it is for.
732        section(
733            out,
734            "Commands",
735            None,
736            &rows,
737            Layout {
738                long: false,
739                ..layout
740            },
741        );
742    }
743    if flat {
744        flat_commands(out, command, path, layout);
745    }
746
747    section(
748        out,
749        "Arguments",
750        None,
751        &argument_rows(command, layout),
752        layout,
753    );
754
755    let visible: Vec<&Item> = command.items.iter().filter(|i| !i.hide).collect();
756    let mut headings: Vec<Option<&str>> = vec![None];
757    for item in visible.iter().filter(|i| !i.positional) {
758        if !headings.contains(&item.heading) {
759            headings.push(item.heading);
760        }
761    }
762    let builtin = |short: &str, long_name: &str| {
763        let mut cell = Cell::default();
764        cell.push(style.flag(), short);
765        cell.push(Ink::NONE, ", ");
766        cell.push(style.flag(), long_name);
767        cell
768    };
769    for title in headings {
770        let mut rows: Vec<(Cell, String)> = visible
771            .iter()
772            .filter(|i| !i.positional && i.heading == title)
773            .map(|i| (flag_spec(i, style), describe(i, long, style)))
774            .collect();
775        if title.is_none() {
776            if command.help_flag {
777                let what = if long {
778                    "Print help (see a summary with '-h')"
779                } else {
780                    "Print help (see more with '--help')"
781                };
782                let cell = if command.help_short {
783                    builtin("-h", "--help")
784                } else {
785                    let mut cell = Cell::default();
786                    cell.push(Ink::NONE, "    ");
787                    cell.push(style.flag(), "--help");
788                    cell
789                };
790                let what = if command.help_short {
791                    what
792                } else {
793                    "Print help"
794                };
795                rows.push((cell, what.into()));
796            }
797            if command.version.is_some() {
798                rows.push((builtin("-V", "--version"), "Print version".into()));
799            }
800        }
801        let prose = title.and_then(|title| command.heading_help(title));
802        section(out, title.unwrap_or("Options"), prose, &rows, layout);
803    }
804
805    let rows: Vec<(Cell, String)> = inherited
806        .iter()
807        .map(|i| (flag_spec(i, style), describe(i, long, style)))
808        .collect();
809    section(out, "Global options", None, &rows, layout);
810
811    examples(out, command.examples, layout);
812
813    let after = if long && !command.after_long_help.is_empty() {
814        command.after_long_help
815    } else {
816        command.after_help
817    };
818    if !after.is_empty() {
819        let _ = writeln!(out, "{}", wrap_text(after, width));
820    }
821}
822
823/// A command's positionals, as the rows of its Arguments section.
824fn argument_rows(command: &Command, layout: Layout) -> Vec<(Cell, String)> {
825    let shown = command.items.iter().filter(|i| i.positional && !i.hide);
826    shown
827        .map(|i| {
828            let mut cell = Cell::default();
829            push_item_placeholder(&mut cell, layout.style, i);
830            (cell, describe(i, layout.long, layout.style))
831        })
832        .collect()
833}
834
835/// Each subcommand of a `flatten_help` command under its own command line:
836/// what it is, then what it takes, and its own subcommands if it flattens
837/// them too.
838fn flat_commands(out: &mut String, command: &Command, path: &[&str], layout: Layout) {
839    let Layout { long, style, .. } = layout;
840    for sub in command.subcommands.iter().filter(|sub| !sub.hide) {
841        let mut path = path.to_vec();
842        path.push(sub.name);
843        heading(out, &format!("{}:", path.join(" ")), style);
844        out.push('\n');
845        let about = if long && !sub.command.long_about.is_empty() {
846            sub.command.long_about
847        } else {
848            sub.about
849        };
850        indented(out, about, 2, layout);
851        let mut rows = argument_rows(sub.command, layout);
852        let flags = sub
853            .command
854            .items
855            .iter()
856            .filter(|i| !i.positional && !i.hide);
857        rows.extend(flags.map(|i| (flag_spec(i, style), describe(i, long, style))));
858        if long && !about.is_empty() && !rows.is_empty() {
859            out.push('\n');
860        }
861        list(out, &rows, layout);
862        if !long || rows.is_empty() {
863            out.push('\n');
864        }
865        if sub.command.flatten_help {
866            flat_commands(out, sub.command, &path, layout);
867        }
868    }
869}
870
871/// The Examples section: each line after `$`, under its header if it has
872/// one, and after what it does on the long page.
873fn examples(out: &mut String, examples: &[Example], layout: Layout) {
874    let Layout { long, style, .. } = layout;
875    if examples.is_empty() {
876        return;
877    }
878    heading(out, "Examples:", style);
879    out.push('\n');
880    let mut spaced = false;
881    for (n, example) in examples.iter().enumerate() {
882        let help = Some(example.help).filter(|help| long && !help.is_empty());
883        let said = !example.header.is_empty() || help.is_some();
884        // A line that is introduced stands apart from its neighbors.
885        if n > 0 && (said || spaced) {
886            out.push('\n');
887        }
888        spaced = said;
889        if !example.header.is_empty() {
890            let _ = writeln!(out, "  {}:", painted_marks(example.header, style));
891        }
892        if let Some(help) = help {
893            indented(out, help, 4, layout);
894        }
895        out.push_str(if said { "    " } else { "  " });
896        push_painted(out, style.dim(), "$");
897        out.push(' ');
898        push_painted(out, style.code(), example.code);
899        out.push('\n');
900    }
901    out.push('\n');
902}
903
904/// `prog [OPTIONS] <A> [B]... [COMMAND]`.
905pub(crate) fn usage(command: &Command, path: &[&str], style: Style) -> String {
906    let mut line = Style::paint(style.program(), &path.join(" "));
907    let mut cell = Cell::default();
908    if command.items.iter().any(|i| !i.positional && !i.hide) || command.help_flag {
909        cell.push(Ink::NONE, " ");
910        cell.push_placeholder(style, false, "OPTIONS");
911    }
912    for item in command
913        .items
914        .iter()
915        .filter(|i| i.positional && !i.hide && !i.trailing)
916    {
917        cell.push(Ink::NONE, " ");
918        push_item_placeholder(&mut cell, style, item);
919    }
920    if !command.subcommands.is_empty() {
921        cell.push(Ink::NONE, " ");
922        cell.push_placeholder(style, command.subcommand_required, "COMMAND");
923    }
924    line.push_str(&cell.text);
925    for item in command
926        .items
927        .iter()
928        .filter(|i| i.positional && !i.hide && i.trailing)
929    {
930        // The brackets and `--` are typed, so they are painted as literals.
931        let mut cell = Cell::default();
932        cell.push(Ink::NONE, " ");
933        cell.push(style.dim(), "[");
934        cell.push(style.flag(), "--");
935        cell.push(Ink::NONE, " ");
936        push_item_placeholder(&mut cell, style, item);
937        cell.push(style.dim(), "]");
938        line.push_str(&cell.text);
939    }
940    line
941}
942
943/// A positional's placeholder: `<NAME>` or `[NAME]`, then `...` if repeated.
944fn push_item_placeholder(cell: &mut Cell, style: Style, item: &Item) {
945    cell.push_placeholder(style, item.required, item.value_name.unwrap_or("VALUE"));
946    if item.multiple {
947        cell.push(style.dim(), "...");
948    }
949}
950
951fn flag_spec(item: &Item, style: Style) -> Cell {
952    let mut spec = Cell::default();
953    // Every spelling: `-l, -L`, `-e, +e`, `+o`, `-v, --verbose`; a long name
954    // alone is indented to line up with those after a short one.
955    let shorts = item.short.iter().chain(item.more_shorts);
956    let mut names: Vec<String> = shorts.map(|s| format!("-{s}")).collect();
957    names.extend(item.plus.map(|c| format!("+{c}")));
958    if names.is_empty() && item.long.is_some() {
959        spec.push(Ink::NONE, "    ");
960    }
961    names.extend(item.long.map(|l| format!("--{l}")));
962    for (i, name) in names.iter().enumerate() {
963        if i > 0 {
964            spec.push(Ink::NONE, ", ");
965        }
966        spec.push(style.flag(), name);
967    }
968    if let Some(no) = item.negate {
969        spec.push(Ink::NONE, " / ");
970        spec.push(style.flag(), &format!("--{no}"));
971    }
972    if let Some(value) = item.value_name {
973        // `<V>`, `=<V>` when only attached, in brackets when optional, and
974        // once a word for `values = N`.
975        let more = if item.more_values { "..." } else { "" };
976        let value = vec![format!("<{value}>"); item.values.max(1)].join(" ") + more;
977        let (open, close) = match (item.require_equals, item.optional_value) {
978            (true, true) => ("[=", "]"),
979            (true, false) => ("=", ""),
980            (false, true) => (" [", "]"),
981            (false, false) => (" ", ""),
982        };
983        spec.push(Ink::NONE, open);
984        spec.push(style.placeholder(), &value);
985        spec.push(Ink::NONE, close);
986    }
987    if item.multiple {
988        spec.push(style.dim(), "...");
989    }
990    spec
991}
992
993/// An item's description, then `[env: …]`, `[default: …]` and
994/// `[possible values: …]`, each value painted on its own (so a line never
995/// breaks inside a painted span).
996fn describe(item: &Item, long: bool, style: Style) -> String {
997    let help = if long && !item.long_help.is_empty() {
998        item.long_help
999    } else {
1000        item.help
1001    };
1002    let mut text = painted_marks(help, style);
1003    let mut add = |extra: String| {
1004        if !text.is_empty() {
1005            text.push(if long { '\n' } else { ' ' });
1006        }
1007        text.push_str(&extra);
1008    };
1009    let label = |label: &str| Style::paint(style.dim(), label);
1010    let close = || Style::paint(style.dim(), "]");
1011    if let Some(env) = item.env {
1012        let env = Style::paint(style.env(), env);
1013        add(format!("{}{env}{}", label("[env: "), close()));
1014    }
1015    if let Some(default) = item.default {
1016        let default = Style::paint(style.default(), default);
1017        add(format!("{}{default}{}", label("[default: "), close()));
1018    }
1019    if !item.choices.is_empty() {
1020        let choices: Vec<String> = item
1021            .choices
1022            .iter()
1023            .map(|c| Style::paint(style.choice(), c))
1024            .collect();
1025        add(format!(
1026            "{}{}{}",
1027            label("[possible values: "),
1028            choices.join(", "),
1029            close()
1030        ));
1031    }
1032    if long && !item.warning.is_empty() {
1033        let label = Style::paint(style.error(), "Warning:");
1034        add(format!("{label} {}", painted_marks(item.warning, style)));
1035    }
1036    text
1037}
1038
1039/// A heading: painted as one, and a marked span of it as both a heading and
1040/// what its marks say, in the heading's color. `*shown*` in a bold heading is
1041/// bold and italic.
1042fn heading(out: &mut String, title: &str, style: Style) {
1043    let header = style.header();
1044    let inks = [style.code(), style.emphasis(), style.strong()];
1045    let mut at = 0;
1046    if style.depth != Depth::None {
1047        while let Some((open, end, ink)) = marked(title, at, inks) {
1048            if at < open {
1049                push_painted(out, header, &title[at..open]);
1050            }
1051            let both = Paint {
1052                fg: header.paint.fg.or(ink.paint.fg),
1053                bold: header.paint.bold || ink.paint.bold,
1054                dim: header.paint.dim || ink.paint.dim,
1055                italic: header.paint.italic || ink.paint.italic,
1056                underline: header.paint.underline || ink.paint.underline,
1057            };
1058            push_painted(out, style.ink(both), &title[open..end]);
1059            at = end;
1060        }
1061    }
1062    push_painted(out, header, &title[at..]);
1063}
1064
1065/// `text` with each `` `quoted` ``, `*emphasized*` and `**strong**` span
1066/// painted, marks kept (so the painted page is the plain one plus escapes).
1067/// Each word of a span is painted on its own, so wrapping never breaks inside
1068/// a painted run; a mark with no pair on its line is left alone.
1069fn painted_marks(text: &str, style: Style) -> String {
1070    let inks = [style.code(), style.emphasis(), style.strong()];
1071    if style.depth == Depth::None
1072        || inks.iter().all(|ink| ink.paint == Paint::NONE)
1073        || !text.contains(['`', '*'])
1074    {
1075        return text.to_owned();
1076    }
1077    let mut out = String::with_capacity(text.len() + 16);
1078    for (n, line) in text.split('\n').enumerate() {
1079        if n > 0 {
1080            out.push('\n');
1081        }
1082        let mut at = 0;
1083        while let Some((open, end, ink)) = marked(line, at, inks) {
1084            out.push_str(&line[at..open]);
1085            for (i, word) in line[open..end].split(' ').enumerate() {
1086                if i > 0 {
1087                    out.push(' ');
1088                }
1089                if !word.is_empty() {
1090                    push_painted(&mut out, ink, word);
1091                }
1092            }
1093            at = end;
1094        }
1095        out.push_str(&line[at..]);
1096    }
1097    out
1098}
1099
1100/// The first marked span of `line` at or after `from`, marks included, and
1101/// its ink among those of a quoted, an emphasized and a strong one.
1102///
1103/// Stars whose ink paints nothing mark no span, so a quoted span between
1104/// them is still painted.
1105///
1106/// Stars mark a span only around it: the opening ones follow no letter or
1107/// digit and touch the span's first character, the closing ones touch its
1108/// last and precede no letter or digit. `2*3*4`, `*.rs or *.toml`, a
1109/// bullet's `* ` and `\*` are not spans. Three stars or more are left alone.
1110fn marked(
1111    line: &str,
1112    from: usize,
1113    [code, emphasis, strong]: [Ink; 3],
1114) -> Option<(usize, usize, Ink)> {
1115    let bytes = line.as_bytes();
1116    let mut at = from;
1117    while at < bytes.len() {
1118        match bytes[at] {
1119            b'`' => {
1120                if let Some(len) = line[at + 1..].find('`') {
1121                    return Some((at, at + len + 2, code));
1122                }
1123                at += 1;
1124            }
1125            b'*' => {
1126                let stars = run(bytes, at);
1127                let before = line[..at].chars().next_back();
1128                let after = line[at + stars..].chars().next();
1129                let ink = if stars == 2 { strong } else { emphasis };
1130                if stars <= 2
1131                    && ink.paint != Paint::NONE
1132                    && before.is_none_or(|c| !c.is_alphanumeric() && c != '\\')
1133                    && after.is_some_and(|c| !c.is_whitespace())
1134                    && let Some(end) = closing(line, at + stars, stars)
1135                {
1136                    return Some((at, end, ink));
1137                }
1138                at += stars;
1139            }
1140            _ => at += 1,
1141        }
1142    }
1143    None
1144}
1145
1146/// How many stars are in a row at `at`.
1147fn run(bytes: &[u8], at: usize) -> usize {
1148    bytes[at..].iter().take_while(|b| **b == b'*').count()
1149}
1150
1151/// The end of the span that `stars` stars opened before `from`: past the
1152/// next run of as many, if it closes one. If it does not, nothing was opened:
1153/// in `*.rs is *never* read` the span is `*never*`. A quoted span on the way
1154/// is skipped, stars and all.
1155fn closing(line: &str, from: usize, stars: usize) -> Option<usize> {
1156    let bytes = line.as_bytes();
1157    let mut at = from;
1158    while at < bytes.len() {
1159        match bytes[at] {
1160            b'`' => at += line[at + 1..].find('`').map_or(1, |len| len + 2),
1161            b'*' => {
1162                let found = run(bytes, at);
1163                let before = line[..at].chars().next_back();
1164                let after = line[at + found..].chars().next();
1165                if found == stars {
1166                    let closes = before.is_some_and(|c| !c.is_whitespace() && c != '\\')
1167                        && after.is_none_or(|c| !c.is_alphanumeric());
1168                    return closes.then_some(at + found);
1169                }
1170                at += found;
1171            }
1172            _ => at += 1,
1173        }
1174    }
1175    None
1176}
1177
1178/// A titled two-column list, after what `--help` says under the title.
1179fn section(
1180    out: &mut String,
1181    title: &str,
1182    prose: Option<&str>,
1183    rows: &[(Cell, String)],
1184    layout: Layout,
1185) {
1186    if rows.is_empty() {
1187        return;
1188    }
1189    heading(out, &format!("{title}:"), layout.style);
1190    out.push('\n');
1191    if let Some(prose) = prose.filter(|_| layout.long) {
1192        indented(out, prose, 2, layout);
1193        out.push('\n');
1194    }
1195    list(out, rows, layout);
1196    if !layout.long {
1197        out.push('\n');
1198    }
1199}
1200
1201/// A two-column list; long help puts each description under its item.
1202///
1203/// Descriptions wrap to the width. The column is at most two fifths of the
1204/// page, so one long spelling does not squeeze every
1205/// description; an item wider than that has its description on the next line.
1206fn list(out: &mut String, rows: &[(Cell, String)], layout: Layout) {
1207    let Layout { long, width, .. } = layout;
1208    let longest = rows.iter().map(|(left, _)| left.width).max().unwrap_or(0);
1209    let available = width.saturating_sub(4);
1210    let column = longest.min(available / 5 * 2 + available % 5 * 2 / 5);
1211    // Where descriptions start, and how much room they get (never too little to read).
1212    let (indent, room) = if long {
1213        (10, width.saturating_sub(10).max(20))
1214    } else {
1215        (column + 4, width.saturating_sub(column + 4).max(20))
1216    };
1217    for (left, right) in rows {
1218        let beside = !long && !right.is_empty() && left.width <= column;
1219        out.push_str("  ");
1220        out.push_str(&left.text);
1221        if beside {
1222            // Padded by what shows, not by the escapes' bytes.
1223            let _ = write!(out, "{:pad$}  ", "", pad = column - left.width);
1224        } else {
1225            out.push('\n');
1226        }
1227        let mut first = beside;
1228        for line in right.lines() {
1229            if line.is_empty() {
1230                out.push('\n');
1231                continue;
1232            }
1233            for piece in Wrap::new(line, room) {
1234                if !first {
1235                    let _ = write!(out, "{:indent$}", "");
1236                }
1237                first = false;
1238                out.push_str(piece);
1239                out.push('\n');
1240            }
1241        }
1242        if long {
1243            // A blank line between items.
1244            out.push('\n');
1245        }
1246    }
1247}
1248
1249/// `text` painted and wrapped, each line after `indent` spaces; a blank line
1250/// between two paragraphs stays empty.
1251fn indented(out: &mut String, text: &str, indent: usize, layout: Layout) {
1252    let text = painted_marks(text, layout.style);
1253    let room = layout.width.saturating_sub(indent).max(20);
1254    for line in text.lines() {
1255        if line.trim().is_empty() {
1256            out.push('\n');
1257        }
1258        for piece in Wrap::new(line, room) {
1259            let _ = writeln!(out, "{:indent$}{piece}", "");
1260        }
1261    }
1262}
1263
1264/// Each line of `text` wrapped to `width`, blank lines kept.
1265fn wrap_text(text: &str, width: usize) -> String {
1266    let mut out = String::with_capacity(text.len() + text.len() / width.max(1));
1267    for (i, line) in text.lines().enumerate() {
1268        if i > 0 {
1269            out.push('\n');
1270        }
1271        for (j, piece) in Wrap::new(line, width.max(20)).enumerate() {
1272            if j > 0 {
1273                out.push('\n');
1274            }
1275            out.push_str(piece);
1276        }
1277    }
1278    out
1279}
1280
1281/// The pieces of `line` broken at spaces into at most `width` characters; a
1282/// word longer than that stands alone. Leading indentation stays on the first
1283/// piece. Borrowed slices, so a line that fits costs one scan.
1284struct Wrap<'a> {
1285    rest: &'a str,
1286    width: usize,
1287}
1288
1289impl<'a> Wrap<'a> {
1290    fn new(line: &'a str, width: usize) -> Self {
1291        Wrap {
1292            rest: line.trim_end(),
1293            width,
1294        }
1295    }
1296}
1297
1298impl<'a> Iterator for Wrap<'a> {
1299    type Item = &'a str;
1300
1301    fn next(&mut self) -> Option<&'a str> {
1302        if self.rest.is_empty() {
1303            return None;
1304        }
1305        // The byte after the last character that fits, and the last space before it.
1306        let (mut end, mut space, mut count, mut text) = (self.rest.len(), None, 0, false);
1307        let mut escape = false;
1308        for (at, c) in self.rest.char_indices() {
1309            // SGR sequences (`ESC [ … m`) take no columns.
1310            if escape {
1311                escape = c != 'm';
1312                continue;
1313            }
1314            if c == '\u{1b}' {
1315                escape = true;
1316                continue;
1317            }
1318            if count == self.width {
1319                end = at;
1320                break;
1321            }
1322            // Spaces before the first word are indentation, not a place to break.
1323            if c != ' ' {
1324                text = true;
1325            } else if text {
1326                space = Some(at);
1327            }
1328            count += 1;
1329        }
1330        let cut = match (end == self.rest.len(), space) {
1331            (true, _) => end,
1332            // The next character is a space: break right there.
1333            (false, _) if self.rest[end..].starts_with(' ') => end,
1334            (false, Some(at)) => at,
1335            // One long word: up to its end.
1336            (false, None) => self.rest[end..]
1337                .find(' ')
1338                .map_or(self.rest.len(), |at| end + at),
1339        };
1340        let piece = self.rest[..cut].trim_end();
1341        self.rest = self.rest[cut..].trim_start();
1342        Some(piece)
1343    }
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348    use super::Wrap;
1349
1350    fn wrap(line: &str, width: usize) -> Vec<&str> {
1351        Wrap::new(line, width).collect()
1352    }
1353
1354    #[test]
1355    fn pieces_break_at_spaces_within_the_width() {
1356        assert_eq!(wrap("aa bb cc", 5), ["aa bb", "cc"]);
1357        assert_eq!(wrap("aa bb", 5), ["aa bb"]);
1358        assert_eq!(wrap("aa  bb   cc", 6), ["aa  bb", "cc"]);
1359    }
1360
1361    #[test]
1362    fn a_long_word_stands_alone() {
1363        assert_eq!(wrap("a verylongword b", 5), ["a", "verylongword", "b"]);
1364    }
1365
1366    #[test]
1367    fn indentation_stays_on_the_first_piece() {
1368        assert_eq!(wrap("    $ mise use node", 12), ["    $ mise", "use node"]);
1369    }
1370
1371    #[test]
1372    fn escapes_take_no_columns() {
1373        let green = |s: &str| format!("\u{1b}[32m{s}\u{1b}[0m");
1374        let line = format!("[possible values: {}, {}]", green("auto"), green("never"));
1375        let pieces = wrap(&line, 24);
1376        assert_eq!(
1377            pieces,
1378            [
1379                format!("[possible values: {},", green("auto")),
1380                format!("{}]", green("never"))
1381            ]
1382        );
1383    }
1384
1385    #[test]
1386    fn width_counts_characters() {
1387        assert_eq!(wrap("éé éé éé", 5), ["éé éé", "éé"]);
1388    }
1389}