use std::fmt;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::style::Style;
const CHAR_WIDTH: usize = 5;
const CHAR_HEIGHT: usize = 7;
const BLOCK: char = '\u{2588}';
const CHAR_GAP: usize = 1;
const SPACE_WIDTH: usize = 3;
fn glyph(ch: char) -> Option<[u8; CHAR_HEIGHT]> {
match ch {
'A' => Some([
0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001,
]),
'B' => Some([
0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110,
]),
'C' => Some([
0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110,
]),
'D' => Some([
0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100,
]),
'E' => Some([
0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111,
]),
'F' => Some([
0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000,
]),
'G' => Some([
0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110,
]),
'H' => Some([
0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001,
]),
'I' => Some([
0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111,
]),
'J' => Some([
0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100,
]),
'K' => Some([
0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001,
]),
'L' => Some([
0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111,
]),
'M' => Some([
0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001,
]),
'N' => Some([
0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001,
]),
'O' => Some([
0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110,
]),
'P' => Some([
0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000,
]),
'Q' => Some([
0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101,
]),
'R' => Some([
0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001,
]),
'S' => Some([
0b01110, 0b10001, 0b10000, 0b01110, 0b00001, 0b10001, 0b01110,
]),
'T' => Some([
0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100,
]),
'U' => Some([
0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110,
]),
'V' => Some([
0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b01010, 0b00100,
]),
'W' => Some([
0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001,
]),
'X' => Some([
0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001,
]),
'Y' => Some([
0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100,
]),
'Z' => Some([
0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111,
]),
'a'..='z' => glyph(ch.to_ascii_uppercase()),
'0' => Some([
0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110,
]),
'1' => Some([
0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111,
]),
'2' => Some([
0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111,
]),
'3' => Some([
0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110,
]),
'4' => Some([
0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010,
]),
'5' => Some([
0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110,
]),
'6' => Some([
0b01110, 0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110,
]),
'7' => Some([
0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000,
]),
'8' => Some([
0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110,
]),
'9' => Some([
0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110,
]),
'!' => Some([
0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100,
]),
'?' => Some([
0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b00000, 0b00100,
]),
'.' => Some([
0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100,
]),
',' => Some([
0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b01000,
]),
':' => Some([
0b00000, 0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b00000,
]),
';' => Some([
0b00000, 0b00000, 0b00100, 0b00000, 0b00000, 0b00100, 0b01000,
]),
'-' => Some([
0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000,
]),
'+' => Some([
0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000,
]),
'=' => Some([
0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000,
]),
'/' => Some([
0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000,
]),
'(' => Some([
0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010,
]),
')' => Some([
0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000,
]),
'[' => Some([
0b01110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b01110,
]),
']' => Some([
0b01110, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01110,
]),
'#' => Some([
0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010,
]),
'@' => Some([
0b01110, 0b10001, 0b10111, 0b10101, 0b10110, 0b10000, 0b01110,
]),
'*' => Some([
0b00000, 0b10101, 0b01110, 0b11111, 0b01110, 0b10101, 0b00000,
]),
'_' => Some([
0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111,
]),
'\'' => Some([
0b00100, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000,
]),
'"' => Some([
0b01010, 0b01010, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000,
]),
'<' => Some([
0b00010, 0b00100, 0b01000, 0b10000, 0b01000, 0b00100, 0b00010,
]),
'>' => Some([
0b01000, 0b00100, 0b00010, 0b00001, 0b00010, 0b00100, 0b01000,
]),
'&' => Some([
0b01100, 0b10010, 0b10100, 0b01000, 0b10101, 0b10010, 0b01101,
]),
'%' => Some([
0b11001, 0b11010, 0b00010, 0b00100, 0b01000, 0b01011, 0b10011,
]),
'$' => Some([
0b00100, 0b01111, 0b10100, 0b01110, 0b00101, 0b11110, 0b00100,
]),
'^' => Some([
0b00100, 0b01010, 0b10001, 0b00000, 0b00000, 0b00000, 0b00000,
]),
'~' => Some([
0b00000, 0b00000, 0b01000, 0b10101, 0b00010, 0b00000, 0b00000,
]),
'`' => Some([
0b01000, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000,
]),
'{' => Some([
0b00110, 0b00100, 0b00100, 0b01000, 0b00100, 0b00100, 0b00110,
]),
'}' => Some([
0b01100, 0b00100, 0b00100, 0b00010, 0b00100, 0b00100, 0b01100,
]),
'|' => Some([
0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100,
]),
'\\' => Some([
0b10000, 0b01000, 0b01000, 0b00100, 0b00010, 0b00010, 0b00001,
]),
_ => None,
}
}
fn rendered_width(text: &str) -> usize {
if text.is_empty() {
return 0;
}
let mut width = 0;
let mut first = true;
for ch in text.chars() {
if !first {
width += CHAR_GAP;
}
first = false;
if ch == ' ' {
width += SPACE_WIDTH;
} else {
width += CHAR_WIDTH;
}
}
width
}
#[derive(Debug, Clone)]
pub struct Figlet {
text: String,
style: Style,
width: Option<usize>,
}
impl Figlet {
pub fn new(text: &str) -> Self {
Self {
text: text.to_string(),
style: Style::null(),
width: None,
}
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
fn render_lines(&self) -> Vec<String> {
if self.text.is_empty() {
return Vec::new();
}
let chunks = self.split_into_chunks();
let mut all_rows: Vec<String> = Vec::new();
for chunk in &chunks {
let mut rows = vec![String::new(); CHAR_HEIGHT];
let mut first_in_chunk = true;
for &ch in chunk {
if ch == ' ' {
let gap = if first_in_chunk { 0 } else { CHAR_GAP };
for row in rows.iter_mut() {
row.push_str(&" ".repeat(gap + SPACE_WIDTH));
}
} else if let Some(bitmap) = glyph(ch) {
let gap = if first_in_chunk { 0 } else { CHAR_GAP };
for (r, &bits) in bitmap.iter().enumerate() {
if gap > 0 {
rows[r].push_str(&" ".repeat(gap));
}
for col in (0..CHAR_WIDTH).rev() {
if bits & (1 << col) != 0 {
rows[r].push(BLOCK);
} else {
rows[r].push(' ');
}
}
}
} else {
let gap = if first_in_chunk { 0 } else { CHAR_GAP };
for row in rows.iter_mut() {
row.push_str(&" ".repeat(gap + CHAR_WIDTH));
}
}
first_in_chunk = false;
}
all_rows.extend(rows);
}
all_rows
}
fn split_into_chunks(&self) -> Vec<Vec<char>> {
let chars: Vec<char> = self.text.chars().collect();
let max_width = match self.width {
Some(w) => w,
None => return vec![chars],
};
if chars.is_empty() {
return vec![];
}
let mut chunks: Vec<Vec<char>> = Vec::new();
let mut current: Vec<char> = Vec::new();
let mut current_width: usize = 0;
for &ch in &chars {
let ch_width = if ch == ' ' { SPACE_WIDTH } else { CHAR_WIDTH };
let needed = if current.is_empty() {
ch_width
} else {
CHAR_GAP + ch_width
};
if !current.is_empty() && current_width + needed > max_width {
chunks.push(std::mem::take(&mut current));
current_width = 0;
}
if current.is_empty() {
current_width = ch_width;
} else {
current_width += CHAR_GAP + ch_width;
}
current.push(ch);
}
if !current.is_empty() {
chunks.push(current);
}
chunks
}
pub fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
let natural = rendered_width(&self.text);
let max = match self.width {
Some(w) => w.min(options.max_width),
None => natural.min(options.max_width),
};
let min = if self.text.is_empty() { 0 } else { CHAR_WIDTH };
Measurement::new(min.min(max), max)
}
}
impl Renderable for Figlet {
fn rich_console(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let lines = self.render_lines();
let mut segments = Vec::new();
for line in &lines {
if self.style.is_null() {
segments.push(Segment::text(line));
} else {
segments.push(Segment::styled(line, self.style.clone()));
}
segments.push(Segment::line());
}
segments
}
}
impl fmt::Display for Figlet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lines = self.render_lines();
for (i, line) in lines.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "{}", line)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_console(width: usize) -> Console {
Console::builder()
.width(width)
.force_terminal(true)
.no_color(true)
.markup(false)
.build()
}
#[test]
fn test_single_character() {
let f = Figlet::new("A");
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
for line in &lines {
assert_eq!(line.chars().count(), CHAR_WIDTH);
}
}
#[test]
fn test_single_character_pixels() {
let f = Figlet::new("I");
let lines = f.render_lines();
assert_eq!(lines.len(), 7);
let top = &lines[0];
let block_count = top.chars().filter(|&c| c == BLOCK).count();
assert_eq!(block_count, 5);
}
#[test]
fn test_multiple_characters() {
let f = Figlet::new("AB");
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
for line in &lines {
assert_eq!(line.chars().count(), 11);
}
}
#[test]
fn test_full_uppercase_alphabet() {
for ch in 'A'..='Z' {
assert!(glyph(ch).is_some(), "Missing glyph for '{}'", ch);
}
}
#[test]
fn test_lowercase_maps_to_uppercase() {
for ch in 'a'..='z' {
let lower = glyph(ch);
let upper = glyph(ch.to_ascii_uppercase());
assert_eq!(lower, upper);
}
}
#[test]
fn test_digits() {
for ch in '0'..='9' {
assert!(glyph(ch).is_some(), "Missing glyph for '{}'", ch);
}
let f = Figlet::new("0123456789");
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
}
#[test]
fn test_unknown_character_fallback() {
let f = Figlet::new("\u{1F600}"); let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
for line in &lines {
assert!(line.chars().all(|c| c == ' '));
assert_eq!(line.chars().count(), CHAR_WIDTH);
}
}
#[test]
fn test_empty_string() {
let f = Figlet::new("");
let lines = f.render_lines();
assert!(lines.is_empty());
}
#[test]
fn test_space_handling() {
let f = Figlet::new("A B");
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
for line in &lines {
assert_eq!(line.chars().count(), 15);
}
}
#[test]
fn test_space_is_blank() {
let f = Figlet::new(" ");
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
for line in &lines {
assert!(line.chars().all(|c| c == ' '));
assert_eq!(line.chars().count(), SPACE_WIDTH);
}
}
#[test]
fn test_style_application() {
let style = Style::parse("bold").unwrap();
let f = Figlet::new("A").with_style(style);
let console = make_console(80);
let opts = console.options();
let segments = f.rich_console(&console, &opts);
let styled_segs: Vec<_> = segments.iter().filter(|s| s.text != "\n").collect();
assert!(!styled_segs.is_empty());
for seg in styled_segs {
assert!(seg.style.is_some());
}
}
#[test]
fn test_display_trait() {
let f = Figlet::new("X");
let s = format!("{}", f);
assert!(!s.is_empty());
let line_count = s.lines().count();
assert_eq!(line_count, CHAR_HEIGHT);
}
#[test]
fn test_display_empty() {
let f = Figlet::new("");
let s = format!("{}", f);
assert!(s.is_empty());
}
#[test]
fn test_measure_single_char() {
let f = Figlet::new("A");
let console = make_console(80);
let opts = console.options();
let m = f.measure(&console, &opts);
assert_eq!(m.maximum, CHAR_WIDTH);
}
#[test]
fn test_measure_multiple_chars() {
let f = Figlet::new("AB");
let console = make_console(80);
let opts = console.options();
let m = f.measure(&console, &opts);
assert_eq!(m.maximum, 11);
}
#[test]
fn test_measure_empty() {
let f = Figlet::new("");
let console = make_console(80);
let opts = console.options();
let m = f.measure(&console, &opts);
assert_eq!(m.minimum, 0);
assert_eq!(m.maximum, 0);
}
#[test]
fn test_width_constraint() {
let f = Figlet::new("ABCD").with_width(12);
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT * 2);
}
#[test]
fn test_width_constraint_no_split() {
let f = Figlet::new("AB").with_width(80);
let lines = f.render_lines();
assert_eq!(lines.len(), CHAR_HEIGHT);
}
#[test]
fn test_renderable_produces_segments() {
let f = Figlet::new("A");
let console = make_console(80);
let opts = console.options();
let segments = f.rich_console(&console, &opts);
assert_eq!(segments.len(), CHAR_HEIGHT * 2);
}
#[test]
fn test_punctuation() {
let puncts = "!?.,-:;+=/()[]#@*_'\"<>&%$^~`{}|\\";
for ch in puncts.chars() {
assert!(glyph(ch).is_some(), "Missing glyph for '{}'", ch);
}
}
#[test]
fn test_rendered_width() {
assert_eq!(rendered_width(""), 0);
assert_eq!(rendered_width("A"), 5);
assert_eq!(rendered_width("AB"), 11);
assert_eq!(rendered_width("A B"), 15); assert_eq!(rendered_width(" "), 3);
}
}