use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
fn glyph(c: char) -> [&'static str; 5] {
match c {
'0' => ["###", "# #", "# #", "# #", "###"],
'1' => [" # ", "## ", " # ", " # ", "###"],
'2' => ["###", " #", "###", "# ", "###"],
'3' => ["###", " #", "###", " #", "###"],
'4' => ["# #", "# #", "###", " #", " #"],
'5' => ["###", "# ", "###", " #", "###"],
'6' => ["###", "# ", "###", "# #", "###"],
'7' => ["###", " #", " #", " #", " #"],
'8' => ["###", "# #", "###", "# #", "###"],
'9' => ["###", "# #", "###", " #", "###"],
'.' => [" ", " ", " ", " ", "##"],
'°' => ["##", "##", " ", " ", " "],
'C' => ["###", "# ", "# ", "# ", "###"],
'W' => ["# #", "# #", "# # #", "## ##", "# #"],
'G' => ["####", "# ", "# ##", "# #", "####"],
'%' => ["# #", " #", " # ", " # ", "# #"],
'-' => [" ", " ", "###", " ", " "],
' ' => [" ", " ", " ", " ", " "],
'H' => ["# #", "# #", "###", "# #", "# #"],
'I' => ["###", " # ", " # ", " # ", "###"],
'R' => ["## ", "# #", "## ", "# #", "# #"],
_ => ["###", " #", " # ", " ", " # "],
}
}
pub fn big_text(s: &str) -> Vec<String> {
let mut rows = vec![String::new(); 5];
for c in s.chars() {
let g = glyph(c);
for (i, row) in rows.iter_mut().enumerate() {
row.push_str(g[i]);
row.push(' ');
}
}
rows
}
pub fn big_text_fit(precise: &str, coarse: &str, width: u16) -> Vec<String> {
let rows = big_text(precise);
if rows[0].chars().count() <= width as usize {
rows
} else {
big_text(coarse)
}
}
pub(crate) fn bitmap_lines(rows: &[String], color: Color) -> Vec<Line<'static>> {
rows.iter()
.map(|row| {
let spans: Vec<Span<'static>> = row
.chars()
.map(|px| {
if px == '#' {
Span::styled(" ", Style::default().bg(color))
} else {
Span::raw(" ")
}
})
.collect();
Line::from(spans)
})
.collect()
}
pub fn hero_lines(precise: &str, coarse: &str, width: u16, color: Color) -> Vec<Line<'static>> {
bitmap_lines(&big_text_fit(precise, coarse, width), color)
}
pub fn hero_fits(inner: Rect) -> bool {
inner.height >= 10 && inner.width >= 28
}
#[cfg(test)]
mod tests {
use ratatui::layout::Rect;
use ratatui::style::Color;
pub(crate) fn filled_count(rows: &[String]) -> usize {
rows.iter().flat_map(|r| r.chars()).filter(|&c| c == '#').count()
}
#[test]
fn five_uniform_rows() {
let rows = super::big_text("42.0 W");
assert_eq!(rows.len(), 5);
let w = rows[0].chars().count();
assert!(rows.iter().all(|r| r.chars().count() == w));
}
#[test]
fn all_required_glyphs_exist() {
for c in "0123456789.°CW%G- ".chars() {
let g = super::glyph(c);
assert_eq!(g.len(), 5, "glyph {c:?} must have 5 rows");
let w = g[0].chars().count();
assert!(g.iter().all(|r| r.chars().count() == w), "glyph {c:?} rows uneven");
assert_ne!(g, super::glyph('\u{1}'), "glyph {c:?} falls back to the fallback glyph");
}
}
fn hamming(a: [&str; 5], b: [&str; 5]) -> usize {
a.iter()
.zip(b.iter())
.map(|(ra, rb)| ra.chars().zip(rb.chars()).filter(|(ca, cb)| ca != cb).count())
.sum()
}
#[test]
fn confusable_digit_pairs_are_distinguishable() {
for (a, b) in [('0', '8'), ('3', '9'), ('5', '6'), ('2', '3'), ('6', '8'), ('8', '9')] {
let dist = hamming(super::glyph(a), super::glyph(b));
assert!(dist >= 1, "{a} and {b} render identically");
}
for c in '0'..='9' {
let g = super::glyph(c);
let filled: usize = g.iter().flat_map(|r| r.chars()).filter(|&ch| ch == '#').count();
let total: usize = g.iter().map(|r| r.chars().count()).sum();
assert!(filled * 10 < total * 9, "{c} is solid/near-solid: {filled}/{total} pixels filled");
}
}
#[test]
fn hero_fits_thresholds() {
let r = |w, h| Rect::new(0, 0, w, h);
assert!(super::hero_fits(r(28, 10)));
assert!(!super::hero_fits(r(28, 9)));
assert!(!super::hero_fits(r(27, 10)));
}
#[test]
fn hero_width_budgets_stay_under_28() {
for (s, want) in [("88.0°C", 22), ("100%", 17), ("24.3G", 20), ("7.9 W", 19)] {
let w = super::big_text(s)[0].chars().count();
assert_eq!(w, want, "{s} rendered width changed");
assert!(w <= 28, "{s} exceeds the 28-col hero budget: {w}");
}
}
#[test]
fn hero_lines_paints_filled_pixels_as_background_and_leaves_the_rest_unstyled() {
let color = Color::Rgb(1, 2, 3);
let lines = super::hero_lines("41%", "41%", 28, color);
let bitmap = super::big_text("41%");
assert_eq!(lines.len(), 5);
for (line, row) in lines.iter().zip(&bitmap) {
assert_eq!(line.spans.len(), row.chars().count(), "one span per bitmap pixel");
for (span, px) in line.spans.iter().zip(row.chars()) {
assert_eq!(span.content, " ", "hero cells must be spaces, not glyph characters");
if px == '#' {
assert_eq!(span.style.bg, Some(color), "filled pixel must carry the hero colour as bg");
} else {
assert_eq!(span.style.bg, None, "empty pixel must not carry a background");
}
}
}
assert_eq!(
lines.iter().flat_map(|l| l.spans.iter()).filter(|s| s.style.bg == Some(color)).count(),
filled_count(&bitmap),
);
let fallback = super::hero_lines("1000.0°C", "1000°C", 28, color);
let coarse_bitmap = super::big_text("1000°C");
for (line, row) in fallback.iter().zip(&coarse_bitmap) {
let rendered: String =
line.spans.iter().map(|s| if s.style.bg == Some(color) { '#' } else { ' ' }).collect();
assert_eq!(&rendered, row);
}
}
#[test]
fn big_text_fit_uses_precise_when_it_fits() {
let rows = super::big_text_fit("41%", "41%", 28);
assert_eq!(rows, super::big_text("41%"));
}
#[test]
fn big_text_fit_falls_back_to_coarse_when_precise_overflows() {
let rows = super::big_text_fit("1000.0°C", "1000°C", 28);
assert!(rows[0].chars().count() <= 28);
assert_eq!(rows, super::big_text("1000°C"));
assert_ne!(rows, super::big_text("1000.0°C"));
}
}