use crate::docs::layout::visible_width;
use crate::docs::strip_ansi;
pub const GUTTER: usize = 2;
pub const MIN_PAGE: usize = 50;
pub fn margin(logo: &str, width: usize) -> Option<(usize, usize)> {
let art = trimmed_lines(logo);
if art.is_empty() || width == usize::MAX {
return None;
}
let art_width = art_width(&art);
let page = width.checked_sub(art_width + GUTTER)?;
(page >= MIN_PAGE).then_some((page, width - art_width))
}
fn art_width(art: &[&str]) -> usize {
art.iter().copied().map(shown).max().unwrap_or(0)
}
pub fn place(
page: &str,
logo: &str,
style: Option<&str>,
width: usize,
reserved: Option<usize>,
coloured: bool,
) -> String {
let art = trimmed_lines(logo);
if art.is_empty() {
return page.to_string();
}
let column = reserved.filter(|column| fits_beside(page, &art, *column));
match column {
Some(column) => beside(page, &art, style, column, coloured),
None if art_width(&art) <= width => above(page, &art, style, coloured),
None => page.to_string(),
}
}
fn fits_beside(page: &str, art: &[&str], column: usize) -> bool {
page.lines()
.take(art.len())
.all(|line| shown(line) + GUTTER <= column)
}
fn shown(line: &str) -> usize {
visible_width(&strip_ansi(line))
}
fn beside(page: &str, art: &[&str], style: Option<&str>, column: usize, coloured: bool) -> String {
let mut lines: Vec<String> = page.lines().map(str::to_string).collect();
if lines.len() < art.len() {
lines.resize(art.len(), String::new());
}
for (line, art) in lines.iter_mut().zip(art) {
if art.is_empty() {
continue;
}
let padding = column.saturating_sub(shown(line));
line.push_str(&" ".repeat(padding));
line.push_str(&paint(art, style, coloured));
}
finish(lines.join("\n"))
}
fn above(page: &str, art: &[&str], style: Option<&str>, coloured: bool) -> String {
let mut out = String::new();
for art in art {
out.push_str(&paint(art, style, coloured));
out.push('\n');
}
out.push('\n');
out.push_str(page);
finish(out)
}
fn paint(line: &str, style: Option<&str>, coloured: bool) -> String {
if !coloured {
return strip_ansi(line).into_owned();
}
match style {
Some(style) if !line.trim().is_empty() => {
crate::help_template::semantic(style, line, coloured)
}
_ => line.to_string(),
}
}
fn trimmed_lines(logo: &str) -> Vec<&str> {
let mut lines: Vec<&str> = logo.lines().map(|line| line.trim_end()).collect();
while lines.first().is_some_and(|line| line.is_empty()) {
lines.remove(0);
}
while lines.last().is_some_and(|line| line.is_empty()) {
lines.pop();
}
lines
}
fn finish(page: String) -> String {
let mut out = page.trim_end().to_string();
out.push('\n');
out
}
#[cfg(test)]
mod tests {
use super::*;
const ART: &str = "\n /\\\n / \\\n/____\\\n";
fn render(page: &str, logo: &str, style: Option<&str>, width: usize, coloured: bool) -> String {
let reserved = margin(logo, width).map(|(_, column)| column);
place(page, logo, style, width, reserved, coloured)
}
#[test]
fn a_wide_terminal_reserves_the_margin_and_puts_the_art_in_it() {
assert_eq!(margin(ART, 80), Some((72, 74)));
let page = render("usage 1.0\nAbout it\n", ART, None, 80, false);
let lines: Vec<&str> = page.lines().collect();
assert_eq!(
lines
.iter()
.map(|line| line.chars().count())
.collect::<Vec<_>>(),
vec![78, 79, 80]
);
assert!(lines[0].starts_with("usage 1.0 "), "{:?}", lines[0]);
assert!(lines[0].ends_with(" /\\"), "{:?}", lines[0]);
assert!(lines[2].ends_with("/____\\"), "{:?}", lines[2]);
}
#[test]
fn a_line_that_overruns_the_margin_takes_the_banner_instead() {
let long = "x".repeat(73);
let page = render(&format!("{long}\nshort\n"), ART, None, 80, false);
assert!(page.starts_with(" /\\\n / \\\n/____\\\n\n"), "{page:?}");
}
#[test]
fn one_column_less_fits_beside_it() {
let page = render(
&format!("{}\nshort\n", "x".repeat(72)),
ART,
None,
80,
false,
);
assert!(page.lines().next().unwrap().ends_with(" /\\"));
}
#[test]
fn a_terminal_with_no_room_to_spare_keeps_its_width_and_takes_the_banner() {
assert_eq!(margin(ART, 50), None);
let page = render("usage 1.0\n", ART, None, 50, false);
assert_eq!(page, " /\\\n / \\\n/____\\\n\nusage 1.0\n");
}
#[test]
fn a_terminal_narrower_than_the_art_gets_no_logo() {
assert_eq!(render("hello\n", ART, None, 5, false), "hello\n");
}
#[test]
fn art_taller_than_the_page_keeps_going_below_it() {
let page = render("one line\n", ART, None, 80, false);
assert_eq!(page.lines().count(), 3);
assert!(page.lines().nth(2).unwrap().ends_with("/____\\"));
}
#[test]
fn a_style_colours_each_line_on_its_own() {
let page = render("hi\n", "ab\ncd\n", Some("cyan"), 80, true);
assert_eq!(page.lines().count(), 2);
for line in page.lines() {
assert!(line.ends_with("\u{1b}[0m"), "{line:?}");
assert_eq!(line.matches("\u{1b}[36m").count(), 1, "{line:?}");
}
}
#[test]
fn a_coloured_page_is_measured_by_what_it_prints() {
let line = format!("\u{1b}[36m{}\u{1b}[0m", "x".repeat(72));
let page = render(&format!("{line}\n"), ART, None, 80, true);
assert!(page.lines().next().unwrap().ends_with(" /\\"), "{page:?}");
}
#[test]
fn a_plain_page_gets_the_art_and_none_of_the_colour() {
let page = render("hi\n", "ab\n", Some("cyan"), 80, false);
assert!(page.ends_with("ab\n"));
assert!(!page.contains('\u{1b}'), "{page:?}");
}
#[test]
fn an_unbounded_page_has_no_right_margin_to_put_a_logo_in() {
assert_eq!(margin("ab\n", usize::MAX), None);
assert_eq!(
render("hi\n", "ab\n", None, usize::MAX, false),
"ab\n\nhi\n"
);
}
#[test]
fn a_blank_line_inside_the_art_leaves_no_trailing_spaces_behind() {
let page = render("one\ntwo\nthree\n", "ab\n\ncd\n", None, 80, false);
assert_eq!(page.lines().nth(1).unwrap(), "two");
}
#[test]
fn nothing_but_whitespace_is_no_logo() {
assert_eq!(render("hi\n", "\n \n", None, 80, false), "hi\n");
}
#[test]
fn nothing_but_whitespace_reserves_nothing_either() {
assert_eq!(margin("", 80), None);
assert_eq!(margin("\n \n\t\n", 80), None);
}
#[test]
fn a_plain_page_loses_the_escapes_the_art_brought_with_it() {
let page = render("hi\n", "\u{1b}[31mab\u{1b}[0m\n", None, 80, false);
assert!(!page.contains('\u{1b}'), "{page:?}");
assert!(page.ends_with("ab\n"), "{page:?}");
}
#[test]
fn a_coloured_page_keeps_the_escapes_the_art_brought_with_it() {
let page = render("hi\n", "\u{1b}[31mab\u{1b}[0m\n", None, 80, true);
assert!(page.ends_with("\u{1b}[31mab\u{1b}[0m\n"), "{page:?}");
}
}