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