use std::time::Duration;
use termlens::{Graphics, GraphicsPayload, GraphicsSeen, Key, Screen, Terminal};
const PREVIEW: [&str; 2] = ["--file", "art/vyncint-2027.json"];
const SIZE: (u16, u16) = (176, 34);
const CELL: (u16, u16) = (9, 19);
const GRID_CELLS: (u16, u16) = (53 * 2, 7);
const GRID_X: u16 = 1 + 4;
const CANVAS: [u8; 3] = [0x15, 0x1b, 0x23];
const BRIGHT: [u8; 3] = [0x56, 0xd3, 0x64];
fn chart(
graphics: Graphics,
cell: Option<(u16, u16)>,
size: (u16, u16),
args: &[&str],
) -> termlens::Result<Terminal> {
let mut builder = Terminal::builder()
.size(size.0, size.1)
.env_clear()
.env("COLORTERM", "truecolor")
.env("TERM", "xterm-256color")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.timeout(Duration::from_secs(20))
.graphics(graphics)
.args(args);
if let Some((width, height)) = cell {
builder = builder.cell_size(width, height);
}
builder.spawn(env!("CARGO_BIN_EXE_mossaic"))
}
fn loaded(screen: &Screen) -> bool {
screen.contains("q quit") && screen.contains("contributions in")
}
fn style(screen: &Screen) -> String {
screen
.text()
.lines()
.find(|line| line.contains(" cells"))
.unwrap_or_default()
.rsplit('·')
.next()
.unwrap_or_default()
.trim()
.trim_end_matches('│')
.trim()
.trim_start_matches("auto: ")
.to_string()
}
#[allow(dead_code)]
fn is_auto(screen: &Screen) -> bool {
screen
.text()
.lines()
.any(|line| line.contains("auto: ") && line.contains(" cells"))
}
fn monday(screen: &Screen) -> u16 {
(0..screen.rows())
.find(|row| {
screen
.row_text(*row)
.trim_start_matches('│')
.starts_with("Mon")
})
.expect("a weekday gutter")
}
fn year(seen: &GraphicsSeen) -> &GraphicsPayload {
seen.payloads()
.iter()
.max_by_key(|payload| payload.size().map_or(0, |(width, _)| width))
.unwrap_or_else(|| panic!("no image at all went out: {seen:?}"))
}
fn is(pixel: Option<[u8; 4]>, token: [u8; 3]) -> bool {
pixel.is_some_and(|pixel| {
pixel[3] == 0xff
&& (0..3)
.all(|channel| i32::from(pixel[channel]).abs_diff(i32::from(token[channel])) <= 2)
})
}
fn active_days(screen: &Screen) -> usize {
screen
.text()
.lines()
.find(|line| line.contains("active days"))
.and_then(|line| {
let digits: String = line
.trim_start_matches('│')
.trim()
.chars()
.take_while(char::is_ascii_digit)
.collect();
digits.parse().ok()
})
.unwrap_or_else(|| panic!("no active-day count in:\n{screen}"))
}
#[test]
fn the_probe_finds_pixels_without_being_told() -> termlens::Result<()> {
for (label, graphics, cell, wanted, images) in [
(
"kitty",
Graphics::Kitty,
Some(CELL),
"pixel cells (kitty)",
true,
),
(
"sixel",
Graphics::Sixel,
Some(CELL),
"pixel cells (sixel)",
true,
),
(
"neither",
Graphics::None,
Some(CELL),
"rounded cells",
false,
),
(
"kitty, no cell",
Graphics::Kitty,
None,
"rounded cells",
false,
),
] {
let mut terminal = chart(graphics, cell, SIZE, &PREVIEW)?;
let screen = terminal.wait_frame(loaded)?;
assert_eq!(style(&screen), wanted, "{label}:\n{screen}");
assert_eq!(
!screen.graphics().is_empty(),
images,
"{label}: transmitted {:?}",
screen.graphics()
);
}
Ok(())
}
#[test]
fn the_grid_rows_belong_to_the_image_and_the_labels_stay_text() -> termlens::Result<()> {
let mut terminal = chart(Graphics::Kitty, Some(CELL), SIZE, &PREVIEW)?;
let screen = terminal.wait_frame(loaded)?;
assert_eq!(style(&screen), "pixel cells (kitty)");
assert!(
!screen.graphics().is_empty(),
"the year went out as an image"
);
let top = monday(&screen) - 1;
for row in top..top + 7 {
let cells: String = screen
.row_text(row)
.chars()
.skip(5) .take_while(|character| *character != '│')
.collect();
assert!(
cells.trim().is_empty(),
"row {row} belongs to the painter, but the text layer wrote {cells:?}"
);
}
let months = screen.row_text(top - 1);
for month in ["Jan", "Apr", "Aug", "Dec"] {
assert!(months.contains(month), "months row reads {months:?}");
}
Ok(())
}
#[test]
fn the_year_is_pinned_to_the_cells_the_layout_reserved() -> termlens::Result<()> {
for (label, graphics) in [("kitty", Graphics::Kitty), ("sixel", Graphics::Sixel)] {
let mut terminal = chart(graphics, Some(CELL), SIZE, &PREVIEW)?;
let screen = terminal.wait_frame(loaded)?;
let seen = screen.graphics();
let year = year(&seen);
assert_eq!(
year.size(),
Some((
u32::from(GRID_CELLS.0) * u32::from(CELL.0),
u32::from(GRID_CELLS.1) * u32::from(CELL.1),
)),
"{label}: {year:?}"
);
assert_eq!(
year.at(),
(monday(&screen) - 1, GRID_X),
"{label}: {year:?}"
);
if graphics == Graphics::Kitty {
assert_eq!(year.cells(), Some(GRID_CELLS), "{label}: {year:?}");
}
}
Ok(())
}
#[test]
fn the_year_is_one_image_however_many_escapes_it_takes() -> termlens::Result<()> {
let mut terminal = chart(Graphics::Kitty, Some(CELL), SIZE, &PREVIEW)?;
let screen = terminal.wait_frame(loaded)?;
let seen = screen.graphics();
let year = year(&seen);
assert!(year.chunks() > 1, "the year needs chunking: {year:?}");
assert_eq!(seen.kitty(), seen.payloads().len() as u32, "{seen:?}");
Ok(())
}
#[test]
fn the_year_drawn_is_the_year_described() -> termlens::Result<()> {
for (label, graphics) in [("kitty", Graphics::Kitty), ("sixel", Graphics::Sixel)] {
let mut terminal = chart(graphics, Some(CELL), SIZE, &PREVIEW)?;
let screen = terminal.wait_frame(loaded)?;
let seen = screen.graphics();
let image = year(&seen)
.decode()
.unwrap_or_else(|error| panic!("{label}: {error}"));
let (mut lit, mut empty, mut outside) = (0usize, 0usize, 0usize);
for week in 0..u32::from(GRID_CELLS.0) / 2 {
for weekday in 0..u32::from(GRID_CELLS.1) {
let pixel = image.pixel(
week * 2 * u32::from(CELL.0) + u32::from(CELL.0),
weekday * u32::from(CELL.1) + u32::from(CELL.1) / 2,
);
if is(pixel, BRIGHT) {
lit += 1;
} else if is(pixel, CANVAS) {
empty += 1;
} else {
outside += 1;
}
}
}
assert_eq!(
lit,
active_days(&screen),
"{label}: the image draws {lit} bright days where the footer counts \
{} active:\n{screen}",
active_days(&screen)
);
assert_eq!(outside, 6, "{label}: {outside} cells outside the year");
assert_eq!(
lit + empty + outside,
usize::from(GRID_CELLS.0 / 2 * GRID_CELLS.1),
"{label}: every cell of the grid is accounted for"
);
}
Ok(())
}
#[test]
fn a_year_costs_what_the_design_notes_say() -> termlens::Result<()> {
let mut kitty = chart(Graphics::Kitty, Some(CELL), SIZE, &PREVIEW)?;
let kitty_bytes = kitty.wait_frame(loaded)?.graphics().bytes();
let mut sixel = chart(Graphics::Sixel, Some(CELL), SIZE, &PREVIEW)?;
let sixel_bytes = sixel.wait_frame(loaded)?.graphics().bytes();
println!("MEASURED kitty={kitty_bytes} sixel={sixel_bytes}");
assert!(
(1_000..=12_000).contains(&kitty_bytes),
"a year over kitty is quoted at 4.9 KB, got {kitty_bytes} bytes"
);
assert!(
(20_000..=80_000).contains(&sixel_bytes),
"a year over sixel is quoted at 38 KB, got {sixel_bytes} bytes"
);
assert!(
sixel_bytes > kitty_bytes * 4,
"sixel {sixel_bytes} should dwarf kitty {kitty_bytes}"
);
Ok(())
}
#[test]
fn text_mode_transmits_no_image() -> termlens::Result<()> {
let mut terminal = chart(
Graphics::Kitty,
Some(CELL),
SIZE,
&["--file", "art/vyncint-2027.json", "--graphics", "text"],
)?;
let screen = terminal.wait_frame(loaded)?;
assert!(
screen.graphics().is_empty(),
"--graphics text sent {:?}",
screen.graphics()
);
assert_eq!(style(&screen), "rounded cells", "{screen}");
assert!(screen.contains("300 contributions in 2027"), "{screen}");
Ok(())
}
#[test]
fn moving_the_cursor_sends_a_cell_not_a_year() -> termlens::Result<()> {
for (label, graphics) in [("kitty", Graphics::Kitty), ("sixel", Graphics::Sixel)] {
let mut terminal = chart(graphics, Some(CELL), SIZE, &PREVIEW)?;
let base = terminal.wait_frame(loaded)?.graphics().bytes();
terminal.send(Key::End)?;
let settled = terminal
.wait_frame(|screen| screen.contains("Fri, Dec 31 2027"))?
.graphics()
.bytes();
terminal.send(Key::Left)?;
let moved = terminal
.wait_frame(|screen| screen.contains("Fri, Dec 24 2027"))?
.graphics()
.bytes();
let cost = moved - settled;
println!("MEASURED {label}: year={base} one-move={cost}");
assert!(cost > 0, "{label}: the ring has to be drawn somehow");
assert!(
cost * 10 < base,
"{label}: one cursor move cost {cost} bytes against a {base}-byte year — \
that is the whole grid being re-sent"
);
let after = terminal.screen();
let seen = after.graphics();
let last = seen.last().expect("a payload");
assert_eq!(
last.size(),
Some((2 * u32::from(CELL.0), u32::from(CELL.1))),
"{label}: one moved cursor sent {last:?}"
);
assert_eq!(
last.at().0,
monday(&after) + 4,
"{label}: the ring is drawn on the day's own row: {last:?}"
);
assert_eq!(
(last.at().1 - GRID_X) % 2,
0,
"{label}: and on a day boundary, two columns to the day: {last:?}"
);
}
Ok(())
}
#[test]
fn the_chart_takes_its_images_down_before_it_leaves() -> termlens::Result<()> {
let mut terminal = chart(Graphics::Kitty, Some(CELL), SIZE, &PREVIEW)?;
let drawn = terminal.wait_frame(loaded)?.graphics();
assert_eq!(drawn.deletes(), 0, "nothing to take down yet: {drawn:?}");
terminal.send(Key::Char('q'))?;
assert!(terminal.wait_exit()?.success());
let seen = terminal.screen().graphics();
assert!(
seen.deletes() > 0,
"the chart left its images behind: {seen:?}"
);
assert_eq!(seen.kitty(), drawn.kitty(), "{seen:?}");
Ok(())
}
#[test]
fn auto_never_asks_for_pixels_it_cannot_fit() -> termlens::Result<()> {
for (width, pixels) in [(176u16, true), (120, true), (111, false), (80, false)] {
let mut terminal = chart(Graphics::Kitty, Some(CELL), (width, 34), &PREVIEW)?;
let screen = terminal.wait_frame(loaded)?;
assert_eq!(
style(&screen).starts_with("pixel"),
pixels,
"at {width} columns Auto chose {:?}",
style(&screen)
);
assert_eq!(
!screen.graphics().is_empty(),
pixels,
"at {width} columns it transmitted {:?}",
screen.graphics()
);
}
Ok(())
}
#[test]
fn a_resize_puts_the_year_back() -> termlens::Result<()> {
let mut terminal = chart(Graphics::Kitty, Some(CELL), SIZE, &PREVIEW)?;
let before = terminal.wait_frame(loaded)?.graphics();
assert!(!before.is_empty());
terminal.resize(140, 30)?;
let after = terminal.wait_frame(|screen| loaded(screen) && screen.cols() == 140)?;
assert!(
after.graphics().total() > before.total(),
"the year was not re-sent: {:?} then {:?}",
before,
after.graphics()
);
assert_eq!(style(&after), "pixel cells (kitty)", "{after}");
assert!(after.contains("300 contributions in 2027"), "{after}");
Ok(())
}
#[test]
fn the_capability_report_matches_what_the_chart_draws() -> termlens::Result<()> {
for (graphics, cell, protocol, other) in [
(Graphics::Kitty, (9u16, 19u16), "kitty", "sixel"),
(Graphics::Sixel, (10, 20), "sixel", "kitty"),
] {
let mut terminal = chart(graphics, Some(cell), (100, 24), &["--capabilities"])?;
let status = terminal.wait_exit()?;
assert!(status.success(), "{status:?}");
let report = terminal.screen().text();
let answer = |label: &str| -> String {
report
.lines()
.find(|line| line.trim_start().starts_with(label))
.unwrap_or_default()
.split_whitespace()
.nth(1)
.unwrap_or_default()
.to_string()
};
assert_eq!(answer(protocol), "yes", "{protocol}:\n{report}");
assert_eq!(answer(other), "no", "{other}:\n{report}");
assert_eq!(
answer("cell"),
format!("{}x{}", cell.0, cell.1),
"the cell it measured:\n{report}"
);
assert_eq!(answer("cells"), protocol, "the decision:\n{report}");
}
Ok(())
}
#[test]
fn a_flood_of_motion_is_drained_not_replayed() -> termlens::Result<()> {
let mut terminal = chart(Graphics::Kitty, Some(CELL), SIZE, &PREVIEW)?;
let ready = terminal
.wait_frame(|screen| loaded(screen) && screen.mouse_mode() != termlens::MouseMode::None)?;
let base = ready.graphics();
assert!(!base.is_empty(), "the year should be on screen first");
let row = monday(&ready) + 2;
const CROSSED: u32 = 40;
let from = 5 + 5 * 2;
let to = from + CROSSED as u16 * 2;
terminal.drag(termlens::MouseButton::Left, (from, row), (to, row))?;
let after = terminal.wait_frame(|screen| screen.text().contains(" on "))?;
let payloads = after.graphics().total() - base.total();
let bytes = after.graphics().bytes() - base.bytes();
assert!(
payloads > 0 && payloads <= 12,
"crossing {CROSSED} cells produced {payloads} images — one per event \
would be {CROSSED}"
);
assert!(
after
.graphics()
.payloads()
.iter()
.rev()
.take(payloads as usize)
.all(|payload| payload.chunks() == 1),
"a ring should not need chunking: {:?}",
after.graphics().payloads()
);
assert!(
bytes * 4 < base.bytes(),
"crossing {CROSSED} cells cost {bytes} bytes against a {}-byte year",
base.bytes()
);
Ok(())
}
#[test]
fn pixel_cells_are_refused_rather_than_written_past_the_edge() -> termlens::Result<()> {
let mut terminal = chart(Graphics::Sixel, Some(CELL), (176, 34), &PREVIEW)?;
terminal.wait_frame(|screen| style(screen) == "pixel cells (sixel)")?;
terminal.resize(80, 24)?;
let narrow = terminal.wait_frame(|screen| loaded(screen) && screen.cols() == 80)?;
assert!(
narrow.contains("Mon") && narrow.contains("Wed"),
"the weekday gutter was overwritten:\n{narrow}"
);
assert!(
narrow.contains("q quit"),
"the footer was overwritten:\n{narrow}"
);
for row in 0..narrow.rows() {
let line = narrow.row_text(row);
if !line.trim().is_empty() && line.starts_with('│') {
assert!(
line.trim_end().ends_with('│'),
"row {row} lost its right border:\n{narrow}"
);
}
}
Ok(())
}
#[test]
fn a_year_that_has_not_happened_can_be_asked_what_it_will_look_like() -> termlens::Result<()> {
let mut plain = chart(Graphics::None, None, SIZE, &PREVIEW)?;
let all_elapsed = plain.wait_frame(loaded)?;
assert!(
!all_elapsed.contains("still to come"),
"with no date, every day of a saved year is drawn:\n{all_elapsed}"
);
let mut dated = chart(
Graphics::None,
None,
SIZE,
&["--file", "art/vyncint-2027.json", "--today", "2027-06-30"],
)?;
let half = dated.wait_frame(loaded)?;
assert!(
half.contains("blank = still to come"),
"half the year is ahead now, and the legend says so:\n{half}"
);
dated.send(Key::End)?;
let ahead = dated.wait_frame(|screen| screen.contains("still to come"))?;
assert!(
ahead.contains("Fri, Dec 31 2027"),
"the detail line names the day:\n{ahead}"
);
Ok(())
}