use clap::{Arg, ArgAction, Command};
pub use headwater_check::fill::{fold, fold_at, WIDEST, WIDTH};
pub const INDENT: usize = 10;
pub fn width() -> usize {
match std::env::args_os().any(|one| one == "--wide") {
false => WIDTH,
true => width_of(true, std::env::var("COLUMNS").ok().as_deref()),
}
}
pub fn width_of(wide: bool, columns: Option<&str>) -> usize {
if !wide {
return WIDTH;
}
match columns.and_then(|text| text.trim().parse::<usize>().ok()) {
Some(number) => number.clamp(WIDTH, WIDEST),
None => WIDTH,
}
}
pub fn painted(command: Command, width: usize) -> Command {
let mut one = command.next_line_help(true);
if let Some(about) = one.get_about().map(ToString::to_string) {
one = one.about(fold(&about, width));
}
one = one.mut_args(|arg| {
let Some(help) = arg.get_help().map(ToString::to_string) else {
return arg;
};
let room = width.saturating_sub(INDENT);
let tail = reserved(&arg);
let folded = fold_at(&help, room, tail);
arg.help(folded)
});
let names: Vec<String> = one
.get_subcommands()
.map(|inner| inner.get_name().to_string())
.collect();
for name in names {
one = one.mut_subcommand(name, |inner| painted(inner, width));
}
one
}
pub fn flattened(command: Command) -> Command {
let mut one = command;
if let Some(about) = one.get_about().map(ToString::to_string) {
one = one.about(one_line(&about));
}
one = one.mut_args(|arg| {
let Some(help) = arg.get_help().map(ToString::to_string) else {
return arg;
};
arg.help(one_line(&help))
});
let names: Vec<String> = one
.get_subcommands()
.map(|inner| inner.get_name().to_string())
.collect();
for name in names {
one = one.mut_subcommand(name, flattened);
}
one
}
fn one_line(text: &str) -> String {
text.split_whitespace().collect::<Vec<&str>>().join(" ")
}
fn reserved(arg: &Arg) -> usize {
let mut parts: Vec<usize> = Vec::new();
let takes_a_value = matches!(arg.get_action(), ArgAction::Set | ArgAction::Append);
let defaults = arg.get_default_values();
if takes_a_value && !defaults.is_empty() && !arg.is_hide_default_value_set() {
let written: Vec<String> = defaults
.iter()
.map(|value| value.to_string_lossy().into_owned())
.collect();
parts.push("[default: ]".chars().count() + written.join(" ").chars().count());
}
let mut aliases: Vec<usize> = Vec::new();
aliases.extend(
arg.get_visible_short_aliases()
.unwrap_or_default()
.iter()
.map(|_| 2),
);
aliases.extend(
arg.get_visible_aliases()
.unwrap_or_default()
.iter()
.map(|name| 2 + name.chars().count()),
);
if !aliases.is_empty() {
let plural = if aliases.len() == 1 { 0 } else { 2 };
let separators = 2 * (aliases.len() - 1);
parts.push(
"[alias: ]".chars().count() + plural + separators + aliases.iter().sum::<usize>(),
);
}
if takes_a_value && !arg.is_hide_possible_values_set() {
let possible: Vec<usize> = arg
.get_possible_values()
.iter()
.filter(|value| !value.is_hide_set())
.map(|value| {
let name = value.get_name();
let quotes = usize::from(name.contains(char::is_whitespace)) * 2;
name.chars().count() + quotes
})
.collect();
if !possible.is_empty() {
let separators = 2 * (possible.len() - 1);
parts.push(
"[possible values: ]".chars().count() + separators + possible.iter().sum::<usize>(),
);
}
}
match parts.is_empty() {
true => 0,
false => parts.iter().sum::<usize>() + parts.len(),
}
}
pub fn fold_indented(text: &str, width: usize, at: usize) -> String {
let indent = " ".repeat(at);
let folded = fold(text, width.saturating_sub(at));
let body = folded.replace('\n', &format!("\n{indent}"));
format!("{indent}{body}\n")
}
#[must_use]
pub fn color_choice(mode: ColorMode) -> clap::ColorChoice {
match mode {
ColorMode::Ansi => clap::ColorChoice::Always,
ColorMode::Plain => clap::ColorChoice::Never,
}
}
#[must_use]
pub fn help_styles(mode: ColorMode) -> clap::builder::Styles {
use clap::builder::styling::{AnsiColor, Style};
let base = clap::builder::Styles::plain();
match mode {
ColorMode::Plain => base,
ColorMode::Ansi => base
.header(Style::new().bold())
.usage(Style::new().bold())
.literal(Style::new().fg_color(Some(AnsiColor::Cyan.into()))),
}
}
pub fn row(name: &str, text: &str, at: usize, width: usize) -> String {
let pad = at.saturating_sub(2 + name.chars().count()).max(1);
let folded = fold(text, width.saturating_sub(at));
let indent = " ".repeat(at);
let body = folded.replace('\n', &format!("\n{indent}"));
format!(" {name}{}{body}\n", " ".repeat(pad))
}
pub fn painted_row(
name: &str,
text: &str,
at: usize,
width: usize,
role: Role,
mode: ColorMode,
) -> String {
let plain = row(name, text, at, width);
match mode {
ColorMode::Plain => plain,
ColorMode::Ansi => plain.replacen(
&format!(" {name}"),
&format!(" {}", paint(role, name, mode)),
1,
),
}
}
pub use headwater_check::paint::{color_of, dim, glyph, paint, severity_role, severity_word};
pub use headwater_check::paint::{ColorMode, Role};
fn no_color_flag() -> bool {
std::env::args_os().any(|one| one == "--no-color")
}
fn no_color_env() -> bool {
std::env::var_os("NO_COLOR").is_some()
}
#[must_use]
pub fn stdout_color() -> ColorMode {
color_of(
no_color_flag(),
no_color_env(),
std::io::IsTerminal::is_terminal(&std::io::stdout()),
)
}
#[must_use]
pub fn stderr_color() -> ColorMode {
color_of(
no_color_flag(),
no_color_env(),
std::io::IsTerminal::is_terminal(&std::io::stderr()),
)
}
#[must_use]
pub fn banner_suppressed() -> bool {
std::env::args_os().any(|one| one == "--no-banner")
|| std::env::var_os("HEADWATER_NO_BANNER").is_some()
}
#[must_use]
pub fn wants_root_help() -> bool {
let mut has_help = false;
let mut has_verb = false;
for one in std::env::args_os().skip(1) {
if one == "-h" || one == "--help" {
has_help = true;
}
if one
.to_str()
.is_some_and(|text| headwater_verbs::VERBS.iter().any(|verb| verb.name == text))
{
has_verb = true;
}
}
has_help && !has_verb
}
#[must_use]
pub fn banner(version: &str, mode: ColorMode) -> String {
let tagline = "a documentation corpus, governed and checked like code";
if banner_suppressed() {
return format!("headwater — {tagline}\n\n");
}
let name = paint(Role::Verb, &format!("headwater {version}"), mode);
let rule = dim(&"─".repeat(WIDTH), mode);
format!("{name} — {}\n{rule}\n\n", dim(tagline, mode))
}
#[cfg(test)]
mod tests {
use super::{
banner, color_of, fold, fold_at, fold_indented, row, width_of, ColorMode, INDENT, WIDEST,
WIDTH,
};
#[test]
fn color_is_plain_off_a_terminal_and_ansi_on_one_unless_overridden() {
assert_eq!(color_of(false, false, false), ColorMode::Plain);
assert_eq!(color_of(false, false, true), ColorMode::Ansi);
assert_eq!(
color_of(true, false, true),
ColorMode::Plain,
"--no-color wins"
);
assert_eq!(
color_of(false, true, true),
ColorMode::Plain,
"NO_COLOR wins"
);
assert_eq!(color_of(true, true, false), ColorMode::Plain);
}
#[test]
fn the_masthead_names_the_version_once_above_a_rule_of_the_help_width() {
let text = banner("9.9.9", ColorMode::Plain);
let mut lines = text.lines();
assert_eq!(
lines.next(),
Some("headwater 9.9.9 — a documentation corpus, governed and checked like code")
);
let rule = lines.next().expect("a rule line follows");
assert_eq!(rule.chars().count(), WIDTH);
assert!(rule.chars().all(|c| c == '─'));
}
#[test]
fn nothing_reads_columns_until_a_caller_asks_for_it() {
assert_eq!(width_of(false, Some("500")), WIDTH);
assert_eq!(width_of(false, Some("40")), WIDTH);
assert_eq!(width_of(false, None), WIDTH);
}
#[test]
fn a_width_a_caller_asks_for_is_held_to_the_band() {
assert_eq!(width_of(true, Some("40")), WIDTH);
assert_eq!(width_of(true, Some("100")), 100);
assert_eq!(width_of(true, Some("500")), WIDEST);
assert_eq!(width_of(true, Some("80")), WIDTH);
assert_eq!(width_of(true, Some("120")), WIDEST);
}
#[test]
fn a_columns_that_is_not_a_number_is_the_default_width() {
assert_eq!(width_of(true, None), WIDTH);
assert_eq!(width_of(true, Some("")), WIDTH);
assert_eq!(width_of(true, Some("wide")), WIDTH);
assert_eq!(width_of(true, Some("-1")), WIDTH);
}
#[test]
fn the_fold_this_module_names_is_the_one_the_check_layer_owns() {
let text = "see docs/spec/06-engine-architecture.md#the-command-line for it";
assert_eq!(fold(text, 20), headwater_check::fill::fold(text, 20));
assert_eq!(
fold_at(text, 20, 6),
headwater_check::fill::fold_at(text, 20, 6)
);
assert_eq!(WIDTH, headwater_check::fill::WIDTH);
assert_eq!(WIDEST, headwater_check::fill::WIDEST);
}
#[test]
fn a_row_that_does_not_fit_is_continued_under_itself() {
let written = row(
"check",
"run the pipeline over the corpus, against the lock",
15,
40,
);
let lines: Vec<&str> = written.trim_end().lines().collect();
assert_eq!(lines[0], " check run the pipeline over the");
for line in &lines[1..] {
assert!(line.starts_with(&" ".repeat(15)), "{line:?}");
}
for line in &lines {
assert!(line.chars().count() <= 40, "{line:?}");
}
}
#[test]
fn an_indented_block_holds_every_line_inside_the_width() {
let written = fold_indented("run the checks, and fail on an error", 20, 6);
assert!(written.ends_with('\n'));
for line in written.lines() {
assert!(line.starts_with(" "), "{line:?}");
assert!(line.chars().count() <= 20, "{line:?}");
}
}
#[test]
fn the_indent_is_the_pair_clap_writes() {
assert_eq!(INDENT, " ".len() + " ".len());
}
}