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(color: bool) {
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, "38;5;173", color));
}
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::stdout_is_tty()
&& mandible_tui::glyphs::from_env() == mandible_tui::glyphs::UNICODE
}
pub fn print() {
let color = mandible_tui::style::color_enabled_from_env();
let dim = "2";
let bold = "1";
println!();
if banner_possible() {
animate(color);
println!();
}
println!(
" {} {}",
paint(env!("CARGO_PKG_NAME"), bold, color),
paint(&format!("v{}", env!("CARGO_PKG_VERSION")), dim, color),
);
println!(
" {}",
paint(
"A TUI manual for every command-line tool you have.",
dim,
color
)
);
println!();
println!(" {}", paint("The rule:", bold, color));
println!(" No per-tool logic, ever. Help text isn't written by hand, it's");
println!(" generated — so mandible learns the generators (clap, cobra, argparse,");
println!(" click, GNU argp, and the rest), not the tools. Fixing the argparse");
println!(" grammar improves every Python CLI ever written.");
println!();
println!(" {}", paint("When it can't parse something:", bold, color));
println!(" It shows you the author's own text, untouched, and says so.");
println!(" It never invents structure it didn't find.");
println!();
println!(
" {} {}",
paint("repository", dim, color),
env!("CARGO_PKG_REPOSITORY")
);
println!(
" {} {}",
paint("license", dim, color),
env!("CARGO_PKG_LICENSE")
);
println!();
println!(
" {}",
paint(
"try: mandible git mandible docker mandible --doctor <tool>",
dim,
color
)
);
println!();
}
#[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"));
}
}