use std::time::Duration;
use termlens::{Graphics, Key, Screen, Terminal};
const PREVIEW: [&str; 2] = ["--file", "art/vyncint-2027.json"];
const SIZE: (u16, u16) = (176, 34);
const CELL: (u16, u16) = (9, 19);
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()
.to_string()
}
fn monday(screen: &Screen) -> u16 {
(0..screen.rows())
.find(|row| {
screen
.row_text(*row)
.trim_start_matches('│')
.starts_with("Mon")
})
.expect("a weekday gutter")
}
#[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 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();
assert!(
(1_000..=12_000).contains(&kitty_bytes),
"a year over kitty is quoted at ~8 KB, got {kitty_bytes} bytes"
);
assert!(
(20_000..=80_000).contains(&sixel_bytes),
"a year over sixel is quoted at ~45 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;
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"
);
}
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} image payloads — one per \
event would be about {}",
CROSSED * 2
);
assert!(
bytes * 4 < base.bytes(),
"crossing {CROSSED} cells cost {bytes} bytes against a {}-byte year",
base.bytes()
);
Ok(())
}