Skip to main content

headwater_cli/
paint.rs

1// SPDX-License-Identifier: Apache-2.0
2//! How wide the help is, and who lays it out.
3//!
4//! # `clap` cannot wrap in this workspace, and `term_width` will not make it
5//!
6//! `StyledStr::wrap` is `pub(crate) fn wrap(&mut self, _hard_width: usize) {}`
7//! under `#[cfg(not(feature = "wrap_help"))]`, and this workspace takes `clap`
8//! with `derive` alone. So `Command::term_width` sets a number every renderer
9//! reads and nothing acts on, and every help string reached a caller on one
10//! line however long it was — 1,126 columns at the widest.
11//!
12//! Taking the `wrap_help` feature is the route the crate offers and it is the
13//! wrong one here. It pulls `terminal_size`, which measures the terminal the
14//! process is attached to, and a width that depends on the terminal makes a
15//! piped run and a run under a terminal write different bytes. Every recorded
16//! fixture and every test that reads this help would then be reading the
17//! terminal of whoever ran it.
18//!
19//! So the strings are folded here, before `clap` sees them, at a width this
20//! module decides. Nothing in the path reads a terminal: `dimensions()` is
21//! `(None, None)` without `wrap_help`, so `clap` asks no question about the
22//! stream it is writing to, and neither does this.
23//!
24//! # The one indent, and how it is known
25//!
26//! [`painted`] declares `next_line_help` on every command of the tree, which
27//! puts an argument's help on the line under the argument rather than in a
28//! column whose width is a function of the longest argument at that node. The
29//! indent is then `TAB` plus `NEXT_LINE_INDENT` — two spaces and eight — at
30//! every node, so [`INDENT`] is a constant rather than a computation, and one
31//! folded string is right wherever `clap` decides to print it.
32//!
33//! # What `clap` appends after a help string, and why folding has to know
34//!
35//! `HelpTemplate::help` writes the string this module folded and then appends
36//! the spec values — `[default: 0]`, `[possible values: …]`, `[aliases: …]` —
37//! on the same line after a space. A fold that did not account for them would
38//! be right about the text and wrong about the line. [`reserved`] measures what
39//! is coming and [`fold_at`] keeps the last word of the text and that suffix on
40//! one line together.
41//!
42//! # Color reads the terminal on purpose, and the masthead is why it must
43//!
44//! [`HW-DR-0045`](../../../../docs/decisions/0045-coloring-the-cli-and-where-the-banner-goes.md)
45//! departs from the rule two sections up, deliberately: an escape sequence
46//! leaked into a pipe or a log file actively harms whoever reads it, where a
47//! column-wrap choice never did, and every fixture this corpus pins already
48//! runs headless. So [`color_of`] is a pure function in exactly the shape
49//! [`width_of`] already is — unit-testable with a table and no real terminal —
50//! and its one live caller, [`stdout_color`] or [`stderr_color`], reads a
51//! stream's own terminal state, which nothing above this line ever does.
52//!
53//! `--no-color`, `--no-banner` and their environment variables are read the
54//! way `--wide` already is: scanned raw, before `clap` builds the tree,
55//! because [`banner`] runs inside `first_screen`, which is built before
56//! parsing runs.
57
58use clap::{Arg, ArgAction, Command};
59
60/// The width and the fill, which live in `headwater_check::fill` and are named
61/// here so that a caller of this module keeps writing `paint::WIDTH`.
62///
63/// They moved down when the check report gained a layout: the report and the
64/// help are laid out by one implementation, and `headwater-check` is the crate
65/// every report composer can reach. Nothing is re-implemented here.
66pub use headwater_check::fill::{fold, fold_at, WIDEST, WIDTH};
67
68/// The column an argument's help starts at, at every node of the tree.
69///
70/// `clap`'s `TAB` is two spaces and its `NEXT_LINE_INDENT` is eight, and
71/// [`painted`] declares `next_line_help` everywhere so that the pair is the
72/// whole indent at every node.
73pub const INDENT: usize = 10;
74
75/// The width this process lays the help out at.
76///
77/// `COLUMNS` is read here and nowhere else in this binary, and only when the
78/// command line carries `--wide`. The scan is over the raw arguments because
79/// the answer is needed to build the tree that parses them: `clap` renders help
80/// inside the parse, out of strings that were folded before it started.
81pub fn width() -> usize {
82    // The variable is read inside the `true` arm rather than beside the scan,
83    // so that a run with no `--wide` on its command line makes no call at all.
84    // Two interface contracts say what reaches this binary out of the
85    // environment, and the honest sentence is shorter for it.
86    match std::env::args_os().any(|one| one == "--wide") {
87        false => WIDTH,
88        true => width_of(true, std::env::var("COLUMNS").ok().as_deref()),
89    }
90}
91
92/// The width a `--wide` and a `COLUMNS` reading come to.
93///
94/// Without `--wide` the answer is [`WIDTH`] and the reading is not consulted, so
95/// a run in a 40-column terminal and a run piped into a file write the same
96/// bytes. With it the reading is held to `[WIDTH, WIDEST]`: a narrower terminal
97/// gets 80 because the text was written to be read at 80, and a wider one gets
98/// 120 because a line of prose past that is harder to read rather than easier.
99/// A `COLUMNS` that is absent or is not a number is the same answer as no
100/// `--wide` at all.
101pub fn width_of(wide: bool, columns: Option<&str>) -> usize {
102    if !wide {
103        return WIDTH;
104    }
105    match columns.and_then(|text| text.trim().parse::<usize>().ok()) {
106        Some(number) => number.clamp(WIDTH, WIDEST),
107        None => WIDTH,
108    }
109}
110
111/// The tree, with every string folded and every node laid out the same way.
112///
113/// It walks the whole tree rather than the verbs, so a second word and an
114/// argument `clap` propagated are folded by the same code as a verb, and an
115/// argument added later is folded without anybody remembering to.
116pub fn painted(command: Command, width: usize) -> Command {
117    let mut one = command.next_line_help(true);
118    if let Some(about) = one.get_about().map(ToString::to_string) {
119        one = one.about(fold(&about, width));
120    }
121    one = one.mut_args(|arg| {
122        let Some(help) = arg.get_help().map(ToString::to_string) else {
123            return arg;
124        };
125        let room = width.saturating_sub(INDENT);
126        let tail = reserved(&arg);
127        let folded = fold_at(&help, room, tail);
128        arg.help(folded)
129    });
130    let names: Vec<String> = one
131        .get_subcommands()
132        .map(|inner| inner.get_name().to_string())
133        .collect();
134    for name in names {
135        one = one.mut_subcommand(name, |inner| painted(inner, width));
136    }
137    one
138}
139
140/// The same tree with every string put back on one line.
141///
142/// # What this undoes, and for whom
143///
144/// [`painted`] folds every `about` and every `help` in the tree, and a help
145/// screen is what that is for. A completion script is the other reader of the
146/// same tree and it wants the opposite: a shell shows a description in a
147/// listing it lays out itself, so a fold this engine chose arrives as a break
148/// at a width the shell did not pick. `main`'s `completions` runs this over the
149/// painted tree before handing it to `clap_complete`.
150///
151/// Folding and then flattening returns the source string, because [`fold_at`]
152/// breaks only at a space and rejoins words with one space. So the flag and
153/// subcommand descriptions do not move a byte, and only the positionals change
154/// — which is where the whole defect was.
155///
156/// # Why here rather than a tree built at no width
157///
158/// `command_at(usize::MAX)` folds nothing and produces the same bytes today,
159/// and it is the wrong answer. [`fold_at`]'s own contract is that "a newline in
160/// the source is a break the author asked for and survives", so that route
161/// leaves a completion script one authored newline in one help string away
162/// from the defect returning. This one removes the newline whatever put it
163/// there.
164///
165/// # Why the tree rather than the `zsh` writer
166///
167/// `clap_complete` flattens a flag and a subcommand description and does not
168/// flatten a positional one: its `zsh` generator escapes a positional by hand
169/// rather than through the `escape_help` its other two paths use, and that
170/// hand-written chain omits the newline. Its `bash`, `fish` and PowerShell
171/// generators write no positional description at all, so those three are clean
172/// for a reason this repository does not control. Flattening the tree is
173/// therefore the fix that survives a dependency bump, and
174/// `engine/crates/cli/tests/completions.rs` reads all four scripts for the same
175/// reason.
176pub fn flattened(command: Command) -> Command {
177    let mut one = command;
178    if let Some(about) = one.get_about().map(ToString::to_string) {
179        one = one.about(one_line(&about));
180    }
181    one = one.mut_args(|arg| {
182        let Some(help) = arg.get_help().map(ToString::to_string) else {
183            return arg;
184        };
185        arg.help(one_line(&help))
186    });
187    let names: Vec<String> = one
188        .get_subcommands()
189        .map(|inner| inner.get_name().to_string())
190        .collect();
191    for name in names {
192        one = one.mut_subcommand(name, flattened);
193    }
194    one
195}
196
197/// The words of a string, on one line, separated by one space each.
198///
199/// It splits on whitespace rather than replacing the newline, so a fold that
200/// left a space before its break cannot leave two spaces behind. Every help
201/// string of this binary is written as one logical line of single-spaced words,
202/// so over a folded string this returns the source exactly.
203fn one_line(text: &str) -> String {
204    text.split_whitespace().collect::<Vec<&str>>().join(" ")
205}
206
207/// The width of what `clap` appends after an argument's help, with its space.
208///
209/// It is the `spec_vals` of `HelpTemplate`, measured rather than rendered. The
210/// `env` feature is not compiled in, so the environment-variable clause of that
211/// function cannot occur here and is not measured. Every other clause is, and
212/// the whole surface is held to [`WIDTH`] by `tests/width.rs`, so a clause
213/// measured wrong is reported as a wide line rather than passing quietly.
214fn reserved(arg: &Arg) -> usize {
215    let mut parts: Vec<usize> = Vec::new();
216
217    // `clap` prints a default and a possible-value set for an argument that
218    // takes a value and for no other. A flag declared `SetTrue` carries
219    // `true`/`false` on its value parser and `false` as its default, and
220    // neither reaches a caller. `get_num_args` is `None` until
221    // `Command::build` and this runs before that, so the action is what
222    // answers here. An alias is printed for a flag as well and is not gated.
223    let takes_a_value = matches!(arg.get_action(), ArgAction::Set | ArgAction::Append);
224
225    let defaults = arg.get_default_values();
226    if takes_a_value && !defaults.is_empty() && !arg.is_hide_default_value_set() {
227        let written: Vec<String> = defaults
228            .iter()
229            .map(|value| value.to_string_lossy().into_owned())
230            .collect();
231        parts.push("[default: ]".chars().count() + written.join(" ").chars().count());
232    }
233
234    let mut aliases: Vec<usize> = Vec::new();
235    aliases.extend(
236        arg.get_visible_short_aliases()
237            .unwrap_or_default()
238            .iter()
239            .map(|_| 2),
240    );
241    aliases.extend(
242        arg.get_visible_aliases()
243            .unwrap_or_default()
244            .iter()
245            .map(|name| 2 + name.chars().count()),
246    );
247    if !aliases.is_empty() {
248        let plural = if aliases.len() == 1 { 0 } else { 2 };
249        let separators = 2 * (aliases.len() - 1);
250        parts.push(
251            "[alias: ]".chars().count() + plural + separators + aliases.iter().sum::<usize>(),
252        );
253    }
254
255    if takes_a_value && !arg.is_hide_possible_values_set() {
256        // `get_visible_quoted_name` is `clap`'s and is private, so its two
257        // rules are read off it here: a hidden value is not printed, and a name
258        // holding a space is printed in quotes.
259        let possible: Vec<usize> = arg
260            .get_possible_values()
261            .iter()
262            .filter(|value| !value.is_hide_set())
263            .map(|value| {
264                let name = value.get_name();
265                let quotes = usize::from(name.contains(char::is_whitespace)) * 2;
266                name.chars().count() + quotes
267            })
268            .collect();
269        if !possible.is_empty() {
270            let separators = 2 * (possible.len() - 1);
271            parts.push(
272                "[possible values: ]".chars().count() + separators + possible.iter().sum::<usize>(),
273            );
274        }
275    }
276
277    match parts.is_empty() {
278        true => 0,
279        // `spec_vals` joins its parts with one space, and one more space
280        // separates the whole of it from the help text before it.
281        false => parts.iter().sum::<usize>() + parts.len(),
282    }
283}
284
285/// A block of text folded to `width` and indented, with its closing newline.
286///
287/// The whole block is indented, first line included, which is what separates it
288/// from [`row`]: a row hangs under a name and this stands under a heading.
289pub fn fold_indented(text: &str, width: usize, at: usize) -> String {
290    let indent = " ".repeat(at);
291    let folded = fold(text, width.saturating_sub(at));
292    let body = folded.replace('\n', &format!("\n{indent}"));
293    format!("{indent}{body}\n")
294}
295
296/// The `clap` color choice a [`ColorMode`] means, stated rather than sensed.
297///
298/// # Why never `ColorChoice::Auto`
299///
300/// `Auto` hands the decision to `anstream`, which senses the stream itself and
301/// then reads `CLICOLOR_FORCE`. Both halves are wrong here.
302/// [`HW-DR-0045`](../../../../docs/decisions/0045-coloring-the-cli-and-where-the-banner-goes.md)
303/// rules that no run may force color into a pipe — there is no `--color=always`
304/// for the same reason — so an environment variable that turns escape bytes on
305/// in a redirected run is a promise this binary broke, and
306/// `tests/width.rs`'s `CLICOLOR_FORCE` case is the one that reports it.
307/// `anstream` also knows nothing of this binary's own `--no-color`, so `Auto`
308/// colors a help page the caller asked to be plain.
309///
310/// [`stdout_color`] has already read the flag, the environment and the stream,
311/// which is every input the decision has. This turns that one answer into
312/// `clap`'s vocabulary and adds no input of its own.
313#[must_use]
314pub fn color_choice(mode: ColorMode) -> clap::ColorChoice {
315    match mode {
316        ColorMode::Ansi => clap::ColorChoice::Always,
317        ColorMode::Plain => clap::ColorChoice::Never,
318    }
319}
320
321/// The palette `clap` paints a help page with, in the roles HW-DR-0045 names.
322///
323/// # Why the base is `Styles::plain()` rather than `clap`'s own default
324///
325/// `Styles::styled()` is bold and underline over nine roles, and HW-DR-0045
326/// rules on three: a section heading is default bold, a file path or a flag
327/// name is cyan, a verb name is green bold. Starting from plain and setting
328/// only what the decision names keeps the help screen on the same palette as
329/// `headwater check`, `headwater sweep report` and `headwater explain`, which
330/// is what the decision asks for in those words.
331///
332/// # The one judgment the palette does not settle
333///
334/// `clap` paints a flag name and a subcommand name with a single `literal`
335/// style, where HW-DR-0045 gives them cyan and green bold. Cyan wins here,
336/// because a verb page is mostly flags and a subcommand name appears on the
337/// root screen and the four `taxonomy`-shaped pages alone. Those verb names are
338/// painted green by hand in `first_screen`, where the layout is this crate's
339/// own, so the decision is kept on the surface that shows the most of them.
340///
341/// # Why `error`, `invalid` and `valid` stay plain
342///
343/// `clap` writes a parse refusal to standard error and renders it with these
344/// same styles, while the choice above is read off **standard output**. A run
345/// with a terminal on one stream and a pipe on the other would then color a
346/// refusal nobody asked to be colored. `err` in `main.rs` already colors every
347/// refusal this binary composes, off [`stderr_color`], which is the stream that
348/// answers for it.
349#[must_use]
350pub fn help_styles(mode: ColorMode) -> clap::builder::Styles {
351    use clap::builder::styling::{AnsiColor, Style};
352    let base = clap::builder::Styles::plain();
353    match mode {
354        ColorMode::Plain => base,
355        // `usage` is set alongside `header` because `{usage-heading}` renders
356        // the word `Usage:` through it, and that is a section heading beside
357        // `Options:` on the same page rather than a role of its own.
358        ColorMode::Ansi => base
359            .header(Style::new().bold())
360            .usage(Style::new().bold())
361            .literal(Style::new().fg_color(Some(AnsiColor::Cyan.into()))),
362    }
363}
364
365/// One row of a two-column list, folded so that no line passes `width`.
366///
367/// `at` is the column the second field starts at. A row whose text does not fit
368/// is continued under itself rather than under the name, which is what keeps a
369/// list of verbs readable when one summary is long.
370pub fn row(name: &str, text: &str, at: usize, width: usize) -> String {
371    let pad = at.saturating_sub(2 + name.chars().count()).max(1);
372    let folded = fold(text, width.saturating_sub(at));
373    let indent = " ".repeat(at);
374    let body = folded.replace('\n', &format!("\n{indent}"));
375    format!("  {name}{}{body}\n", " ".repeat(pad))
376}
377
378/// The same row with the name painted, and the layout decided before it is.
379///
380/// [`row`] runs first over the plain name, so the pad and the fold are computed
381/// in characters a reader sees, and the escape sequence is substituted into the
382/// finished line afterwards. That is the order `Route::render` takes and the
383/// order `a_folded_pointer_breaks_where_an_unpainted_one_does` holds it to:
384/// paint before fold and every break moves by the width of an escape sequence
385/// nothing prints.
386///
387/// The substitution is anchored rather than searched. [`row`] opens the line
388/// with two spaces and the name, so replacing that prefix once cannot reach a
389/// second occurrence of the name inside the summary that follows it.
390pub fn painted_row(
391    name: &str,
392    text: &str,
393    at: usize,
394    width: usize,
395    role: Role,
396    mode: ColorMode,
397) -> String {
398    let plain = row(name, text, at, width);
399    match mode {
400        ColorMode::Plain => plain,
401        ColorMode::Ansi => plain.replacen(
402            &format!("  {name}"),
403            &format!("  {}", paint(role, name, mode)),
404            1,
405        ),
406    }
407}
408
409/// [`ColorMode`], [`Role`], [`color_of`], [`paint`] and [`dim`] moved to
410/// `headwater_check::paint`, and are re-exported here unchanged.
411///
412/// `Finding::render`, `Run::render`, `explain`'s renderer and the two sweep
413/// renderers all need them and none of the three crates that declare those
414/// functions may depend on `headwater-cli`, so the types live where every
415/// caller can reach them — see the module comment of
416/// `engine/crates/check/src/paint.rs` for the full reasoning. What stays here
417/// is [`stdout_color`] and [`stderr_color`]: the one fact only this crate
418/// holds, which stream this process is attached to.
419pub use headwater_check::paint::{color_of, dim, glyph, paint, severity_role, severity_word};
420pub use headwater_check::paint::{ColorMode, Role};
421
422/// Whether `--no-color` is on the raw command line, scanned the way
423/// [`width`] scans for `--wide`.
424fn no_color_flag() -> bool {
425    std::env::args_os().any(|one| one == "--no-color")
426}
427
428/// `NO_COLOR`'s convention: any value at all, including an empty one, turns
429/// color off. `tests/width.rs` asserts this over `NO_COLOR=1`, `NO_COLOR=` and
430/// `NO_COLOR=0` alike.
431fn no_color_env() -> bool {
432    std::env::var_os("NO_COLOR").is_some()
433}
434
435/// The mode standard output renders in, for this run of the binary.
436#[must_use]
437pub fn stdout_color() -> ColorMode {
438    color_of(
439        no_color_flag(),
440        no_color_env(),
441        std::io::IsTerminal::is_terminal(&std::io::stdout()),
442    )
443}
444
445/// The mode standard error renders in, for this run of the binary.
446#[must_use]
447pub fn stderr_color() -> ColorMode {
448    color_of(
449        no_color_flag(),
450        no_color_env(),
451        std::io::IsTerminal::is_terminal(&std::io::stderr()),
452    )
453}
454
455/// Whether `--no-banner` or `HEADWATER_NO_BANNER` suppress the masthead,
456/// scanned the way [`no_color_flag`] and `NO_COLOR` are.
457#[must_use]
458pub fn banner_suppressed() -> bool {
459    std::env::args_os().any(|one| one == "--no-banner")
460        || std::env::var_os("HEADWATER_NO_BANNER").is_some()
461}
462
463/// Whether the raw command line asks for the root help screen: `-h` or
464/// `--help` present, and no token that names a verb.
465///
466/// Read the way [`no_color_flag`] is, before `clap` decides anything, because
467/// the masthead is printed by plain I/O ahead of `clap`'s own help writer
468/// rather than inside the template it renders — see `first_screen`'s doc
469/// comment for why a template cannot carry it. A `--root <path>` whose value
470/// happens to equal a verb's name is the one case this reads wrong, and it
471/// costs a missing masthead rather than a wrong screen: `clap` still resolves
472/// the command line the same way regardless of what this function returns.
473#[must_use]
474pub fn wants_root_help() -> bool {
475    let mut has_help = false;
476    let mut has_verb = false;
477    for one in std::env::args_os().skip(1) {
478        if one == "-h" || one == "--help" {
479            has_help = true;
480        }
481        if one
482            .to_str()
483            .is_some_and(|text| headwater_verbs::VERBS.iter().any(|verb| verb.name == text))
484        {
485            has_verb = true;
486        }
487    }
488    has_help && !has_verb
489}
490
491/// The masthead `HW-DR-0045` rules on, or today's plain name line where
492/// [`banner_suppressed`] holds.
493///
494/// `version` is `headwater_resolve::release::ENGINE`, the same value
495/// `--version` prints, so a caller never reads two numbers for one binary.
496/// The blank line closing the string is the one `first_screen` used to open
497/// with, folded in here so the root screen keeps the same shape either way.
498#[must_use]
499pub fn banner(version: &str, mode: ColorMode) -> String {
500    let tagline = "a documentation corpus, governed and checked like code";
501    if banner_suppressed() {
502        return format!("headwater — {tagline}\n\n");
503    }
504    let name = paint(Role::Verb, &format!("headwater {version}"), mode);
505    let rule = dim(&"─".repeat(WIDTH), mode);
506    format!("{name} — {}\n{rule}\n\n", dim(tagline, mode))
507}
508
509#[cfg(test)]
510mod tests {
511    use super::{
512        banner, color_of, fold, fold_at, fold_indented, row, width_of, ColorMode, INDENT, WIDEST,
513        WIDTH,
514    };
515
516    #[test]
517    fn color_is_plain_off_a_terminal_and_ansi_on_one_unless_overridden() {
518        assert_eq!(color_of(false, false, false), ColorMode::Plain);
519        assert_eq!(color_of(false, false, true), ColorMode::Ansi);
520        assert_eq!(
521            color_of(true, false, true),
522            ColorMode::Plain,
523            "--no-color wins"
524        );
525        assert_eq!(
526            color_of(false, true, true),
527            ColorMode::Plain,
528            "NO_COLOR wins"
529        );
530        assert_eq!(color_of(true, true, false), ColorMode::Plain);
531    }
532
533    #[test]
534    fn the_masthead_names_the_version_once_above_a_rule_of_the_help_width() {
535        let text = banner("9.9.9", ColorMode::Plain);
536        let mut lines = text.lines();
537        assert_eq!(
538            lines.next(),
539            Some("headwater 9.9.9 — a documentation corpus, governed and checked like code")
540        );
541        let rule = lines.next().expect("a rule line follows");
542        assert_eq!(rule.chars().count(), WIDTH);
543        assert!(rule.chars().all(|c| c == '─'));
544    }
545
546    #[test]
547    fn nothing_reads_columns_until_a_caller_asks_for_it() {
548        assert_eq!(width_of(false, Some("500")), WIDTH);
549        assert_eq!(width_of(false, Some("40")), WIDTH);
550        assert_eq!(width_of(false, None), WIDTH);
551    }
552
553    #[test]
554    fn a_width_a_caller_asks_for_is_held_to_the_band() {
555        assert_eq!(width_of(true, Some("40")), WIDTH);
556        assert_eq!(width_of(true, Some("100")), 100);
557        assert_eq!(width_of(true, Some("500")), WIDEST);
558        assert_eq!(width_of(true, Some("80")), WIDTH);
559        assert_eq!(width_of(true, Some("120")), WIDEST);
560    }
561
562    /// A reading that is not a number is the width every other run takes.
563    #[test]
564    fn a_columns_that_is_not_a_number_is_the_default_width() {
565        assert_eq!(width_of(true, None), WIDTH);
566        assert_eq!(width_of(true, Some("")), WIDTH);
567        assert_eq!(width_of(true, Some("wide")), WIDTH);
568        assert_eq!(width_of(true, Some("-1")), WIDTH);
569    }
570
571    /// The fill this module re-exports is the one in `headwater-check`.
572    ///
573    /// Its own cases live beside it, in `crates/check/src/fill.rs`. This one
574    /// holds the re-export: a second implementation appearing here would pass
575    /// every case there and lay the help out differently.
576    #[test]
577    fn the_fold_this_module_names_is_the_one_the_check_layer_owns() {
578        let text = "see docs/spec/06-engine-architecture.md#the-command-line for it";
579        assert_eq!(fold(text, 20), headwater_check::fill::fold(text, 20));
580        assert_eq!(
581            fold_at(text, 20, 6),
582            headwater_check::fill::fold_at(text, 20, 6)
583        );
584        assert_eq!(WIDTH, headwater_check::fill::WIDTH);
585        assert_eq!(WIDEST, headwater_check::fill::WIDEST);
586    }
587
588    #[test]
589    fn a_row_that_does_not_fit_is_continued_under_itself() {
590        let written = row(
591            "check",
592            "run the pipeline over the corpus, against the lock",
593            15,
594            40,
595        );
596        let lines: Vec<&str> = written.trim_end().lines().collect();
597        assert_eq!(lines[0], "  check        run the pipeline over the");
598        for line in &lines[1..] {
599            assert!(line.starts_with(&" ".repeat(15)), "{line:?}");
600        }
601        for line in &lines {
602            assert!(line.chars().count() <= 40, "{line:?}");
603        }
604    }
605
606    #[test]
607    fn an_indented_block_holds_every_line_inside_the_width() {
608        let written = fold_indented("run the checks, and fail on an error", 20, 6);
609        assert!(written.ends_with('\n'));
610        for line in written.lines() {
611            assert!(line.starts_with("      "), "{line:?}");
612            assert!(line.chars().count() <= 20, "{line:?}");
613        }
614    }
615
616    /// The indent is `clap`'s two-space `TAB` and its eight-space next-line
617    /// indent, and the whole point of declaring `next_line_help` is that it is
618    /// the same number at every node.
619    #[test]
620    fn the_indent_is_the_pair_clap_writes() {
621        assert_eq!(INDENT, "  ".len() + "        ".len());
622    }
623}