fn paint(text: &str, code: &str, color: bool) -> String {
if color {
format!("\x1b[{code}m{text}\x1b[0m")
} else {
text.to_string()
}
}
const LETTERS: &[&[&str]] = &[
&[
" ",
" ",
" █████████████ ",
"▒▒███▒▒███▒▒███ ",
" ▒███ ▒███ ▒███ ",
" ▒███ ▒███ ▒███ ",
" █████▒███ █████",
"▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ",
],
&[
" ",
" ",
" ██████ ",
" ▒▒▒▒▒███ ",
" ███████ ",
" ███▒▒███ ",
"▒▒████████",
" ▒▒▒▒▒▒▒▒ ",
],
&[
" ",
" ",
" ████████ ",
"▒▒███▒▒███ ",
" ▒███ ▒███ ",
" ▒███ ▒███ ",
" ████ █████",
"▒▒▒▒ ▒▒▒▒▒ ",
],
&[
" █████",
" ▒▒███ ",
" ███████ ",
" ███▒▒███ ",
"▒███ ▒███ ",
"▒███ ▒███ ",
"▒▒████████",
" ▒▒▒▒▒▒▒▒ ",
],
&[
" ███ ",
" ▒▒▒ ",
" ████ ",
"▒▒███ ",
" ▒███ ",
" ▒███ ",
" █████",
"▒▒▒▒▒ ",
],
&[
" █████ ",
"▒▒███ ",
" ▒███████ ",
" ▒███▒▒███",
" ▒███ ▒███",
" ▒███ ▒███",
" ████████ ",
"▒▒▒▒▒▒▒▒ ",
],
&[
" ████ ",
"▒▒███ ",
" ▒███ ",
" ▒███ ",
" ▒███ ",
" ▒███ ",
" █████",
"▒▒▒▒▒ ",
],
&[
" ",
" ",
" ██████ ",
" ███▒▒███",
"▒███████ ",
"▒███▒▒▒ ",
"▒▒██████ ",
" ▒▒▒▒▒▒ ",
],
];
const TRAJECTORY: &[usize] = &[4, 3, 2, 1, 0, 0, 1, 2, 3, 4];
const LETTER_DELAY: usize = 2;
const LETTER_HEIGHT: usize = 8;
const LETTER_SPACING: &str = " ";
const FPS: u64 = 20;
fn max_shift() -> usize {
TRAJECTORY.iter().copied().max().unwrap_or(0)
}
fn frame(offsets: &[usize]) -> Vec<String> {
let canvas_height = LETTER_HEIGHT + max_shift();
(0..canvas_height)
.map(|row| {
let mut line = String::new();
for (i, letter) in LETTERS.iter().enumerate() {
let width = letter[0].chars().count();
match row.checked_sub(offsets[i]) {
Some(idx) if idx < LETTER_HEIGHT => line.push_str(letter[idx]),
_ => line.push_str(&" ".repeat(width)),
}
line.push_str(LETTER_SPACING);
}
line.trim_end().to_string()
})
.collect()
}
fn animate(code: Option<&str>) {
use std::io::Write;
let canvas_height = LETTER_HEIGHT + max_shift();
let cycle = LETTERS.len() * LETTER_DELAY + TRAJECTORY.len();
let mut out = std::io::stdout().lock();
let _ = write!(out, "\x1b[?25l"); for tick in 0..cycle {
let offsets: Vec<usize> = (0..LETTERS.len())
.map(|i| {
let local = (tick + cycle - (i * LETTER_DELAY) % cycle) % cycle;
TRAJECTORY.get(local).copied().unwrap_or(max_shift())
})
.collect();
if tick > 0 {
let _ = write!(out, "\x1b[{canvas_height}A");
}
for line in frame(&offsets) {
let _ = writeln!(
out,
"\x1b[2K{}",
paint(&line, code.unwrap_or_default(), code.is_some())
);
}
let _ = out.flush();
std::thread::sleep(std::time::Duration::from_millis(1000 / FPS));
}
let _ = write!(out, "\x1b[?25h"); let _ = out.flush();
}
fn banner_possible() -> bool {
mandible_tui::terminal::Sink::Stdout.is_tty()
&& mandible_tui::glyphs::from_env() == mandible_tui::glyphs::UNICODE
}
pub fn print() {
let colors = mandible_core::config::load().config.colors;
let palette = mandible_tui::style::Palette::from_env().themed(colors.theme, colors.custom);
println!();
if banner_possible() {
animate(palette.sgr(mandible_core::config::Role::Accent).as_deref());
println!();
}
print!("{}", text(palette));
}
fn text(palette: mandible_tui::style::Palette) -> String {
use mandible_core::config::Role;
use std::fmt::Write;
let color = palette.color;
let muted = palette.sgr(Role::Muted);
let dim = muted.as_deref().unwrap_or_default();
let bold = "1";
let mut out = String::new();
let _ = writeln!(
out,
" {} {}",
paint(env!("CARGO_PKG_NAME"), bold, color),
paint(&format!("v{}", env!("CARGO_PKG_VERSION")), dim, color),
);
let _ = writeln!(
out,
" {}",
paint(
"A TUI manual for every command-line tool you have.",
dim,
color
)
);
out.push('\n');
let _ = writeln!(out, " {}", paint("The rule:", bold, color));
let _ = writeln!(
out,
" No per-tool logic, ever. Help text isn't written by hand, it's"
);
let _ = writeln!(
out,
" generated — so mandible learns the generators (clap, cobra, argparse,"
);
let _ = writeln!(
out,
" click, GNU argp, and the rest), not the tools. Fixing the argparse"
);
let _ = writeln!(out, " grammar improves every Python CLI ever written.");
out.push('\n');
let _ = writeln!(
out,
" {}",
paint("When it can't parse something:", bold, color)
);
let _ = writeln!(
out,
" It shows you the author's own text, untouched, and says so."
);
let _ = writeln!(out, " It never invents structure it didn't find.");
out.push('\n');
let _ = writeln!(
out,
" {} {}",
paint("repository", dim, color),
env!("CARGO_PKG_REPOSITORY")
);
let _ = writeln!(
out,
" {} {}",
paint("license", dim, color),
env!("CARGO_PKG_LICENSE")
);
out.push('\n');
let _ = writeln!(
out,
" {}",
paint(
"try: mandible git mandible docker mandible --doctor <tool>",
dim,
color
)
);
out.push('\n');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn letter_blocks_are_uniform_height() {
for (i, letter) in LETTERS.iter().enumerate() {
assert_eq!(
letter.len(),
LETTER_HEIGHT,
"letter {i} is the wrong height"
);
}
}
#[test]
fn each_letter_has_uniform_row_width() {
for (i, letter) in LETTERS.iter().enumerate() {
let w = letter[0].chars().count();
for (r, row) in letter.iter().enumerate() {
assert_eq!(row.chars().count(), w, "letter {i} row {r} is ragged");
}
}
}
#[test]
fn resting_frame_is_flat() {
let rest = vec![max_shift(); LETTERS.len()];
let lines = frame(&rest);
assert_eq!(lines.len(), LETTER_HEIGHT + max_shift());
for line in lines.iter().take(max_shift()) {
assert!(line.is_empty(), "expected blank lead-in, got {line:?}");
}
assert!(lines[max_shift()..].iter().any(|l| l.contains('█')));
}
#[test]
fn a_lifted_letter_reaches_higher_than_the_baseline() {
const D: usize = 3;
assert!(
LETTERS[D][0].contains('█'),
"this test needs a letter with ink on its first row"
);
let resting = frame(&vec![max_shift(); LETTERS.len()]);
assert!(resting[0].is_empty(), "top row should be blank at rest");
let mut offsets = vec![max_shift(); LETTERS.len()];
offsets[D] = 0;
let lifted = frame(&offsets);
assert!(
lifted[0].contains('█'),
"top row should carry the lifted letter: {:?}",
lifted[0]
);
}
#[test]
fn paint_is_a_no_op_without_color() {
assert_eq!(paint("hi", "1", false), "hi");
}
#[test]
fn paint_wraps_and_resets_with_color() {
let painted = paint("hi", "1", true);
assert!(painted.starts_with("\x1b[1m"));
assert!(painted.ends_with("\x1b[0m"));
}
#[test]
fn the_text_follows_the_theme_and_headings_stay_bold() {
use mandible_core::config::{ColorIndex, Role, Roles, Theme};
use mandible_tui::style::Palette;
let version = format!("v{}", env!("CARGO_PKG_VERSION"));
let mut mine = Roles::DEFAULT;
mine.set(Role::Muted, ColorIndex(200));
for (theme, custom, code) in [
(Theme::Default, Roles::DEFAULT, "90"),
(Theme::Light, Roles::DEFAULT, "38;5;237"),
(Theme::NordStyle, Roles::DEFAULT, "38;5;102"),
(Theme::HighContrast, Roles::DEFAULT, "38;5;248"),
(Theme::Custom, mine, "38;5;200"),
] {
let out = text(Palette::extended().themed(theme, custom));
assert!(
out.contains(&format!("\x1b[{code}m{version}\x1b[0m")),
"{theme:?}: {out:?}"
);
assert!(out.contains("\x1b[1mThe rule:\x1b[0m"), "{theme:?}");
assert!(!out.contains("\x1b[2m"), "no SGR 2 dim is left");
}
}
#[test]
fn no_theme_and_no_color_print_plain_text() {
use mandible_core::config::{Roles, Theme};
use mandible_tui::style::Palette;
for palette in [
Palette::extended().themed(Theme::None, Roles::DEFAULT),
Palette::plain().themed(Theme::Light, Roles::DEFAULT),
] {
let out = text(palette);
assert!(!out.contains('\x1b'), "{out:?}");
assert!(out.contains("The rule:"));
}
}
#[test]
fn a_numbered_color_falls_back_at_sixteen_colors() {
use mandible_core::config::{Roles, Theme};
use mandible_tui::style::Palette;
let out = text(Palette::basic().themed(Theme::NordStyle, Roles::DEFAULT));
assert!(out.contains("\x1b[90mv"), "{out:?}");
}
}