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