#![forbid(unsafe_code)]
use ftui_render::buffer::Buffer;
use ftui_render::cell::Cell;
use ftui_render::diff::BufferDiff;
use ftui_render::presenter::{Presenter, TerminalCapabilities};
use ftui_render::roaring_bitmap::RoaringBitmap;
use std::collections::BTreeSet;
const SIZES: &[(u16, u16)] = &[(80, 24), (120, 40), (200, 60), (400, 100)];
#[derive(serde::Serialize)]
struct FrameLog {
event: &'static str,
frame_id: u64,
screen: String,
scenario: String,
width: u16,
height: u16,
mutations: usize,
dirty_cells_roaring: usize,
dirty_cells_buffer: usize,
dirty_rows_roaring: usize,
dirty_rows_buffer: usize,
diff_changes_full: usize,
diff_changes_dirty: usize,
diff_output_hash: String,
roaring_superset: bool,
diff_match: bool,
}
fn blake3_hex(data: &[u8]) -> String {
let hash = blake3_hash(data);
hex_encode(&hash)
}
fn blake3_hash(data: &[u8]) -> [u8; 32] {
let mut hash = [0u8; 32];
for (i, &byte) in data.iter().enumerate() {
hash[i % 32] ^= byte;
let idx = i % 32;
hash[idx] = hash[idx].wrapping_add(byte).wrapping_mul(31);
}
hash
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
struct ShadowTracker {
bitmap: RoaringBitmap,
width: u16,
}
impl ShadowTracker {
fn new(width: u16) -> Self {
Self {
bitmap: RoaringBitmap::new(),
width,
}
}
fn mark_cell(&mut self, x: u16, y: u16) {
let idx = y as u32 * self.width as u32 + x as u32;
self.bitmap.insert(idx);
}
fn dirty_cell_count(&self) -> usize {
self.bitmap.cardinality()
}
fn dirty_row_count(&self, height: u16) -> usize {
let mut dirty_rows = BTreeSet::new();
for idx in self.bitmap.iter() {
dirty_rows.insert(idx / self.width as u32);
}
dirty_rows.len().min(height as usize)
}
fn is_superset_of_changes(&self, changes: &[(u16, u16)]) -> bool {
changes.iter().all(|&(x, y)| {
self.bitmap
.contains(y as u32 * self.width as u32 + x as u32)
})
}
fn clear(&mut self) {
self.bitmap.clear();
}
}
fn apply_sparse_edits(buf: &mut Buffer, shadow: &mut ShadowTracker) -> usize {
let positions = [
(0, 0),
(5, 3),
(10, 7),
(buf.width() - 1, 0),
(buf.width() / 2, buf.height() / 2),
];
let mut count = 0;
for &(x, y) in &positions {
if x < buf.width() && y < buf.height() {
buf.set(x, y, Cell::from_char('X'));
shadow.mark_cell(x, y);
count += 1;
}
}
count
}
fn apply_dense_row_edits(buf: &mut Buffer, shadow: &mut ShadowTracker) -> usize {
let rows_to_edit = [0, buf.height() / 4, buf.height() / 2, buf.height() - 1];
let mut count = 0;
for &y in &rows_to_edit {
if y < buf.height() {
for x in 0..buf.width() {
buf.set(x, y, Cell::from_char('#'));
shadow.mark_cell(x, y);
count += 1;
}
}
}
count
}
fn apply_alternating_rows(buf: &mut Buffer, shadow: &mut ShadowTracker) -> usize {
let mut count = 0;
for y in (0..buf.height()).step_by(2) {
for x in 0..buf.width() {
buf.set(x, y, Cell::from_char('-'));
shadow.mark_cell(x, y);
count += 1;
}
}
count
}
fn apply_single_cell(buf: &mut Buffer, shadow: &mut ShadowTracker) -> usize {
let x = buf.width() / 2;
let y = buf.height() / 2;
buf.set(x, y, Cell::from_char('*'));
shadow.mark_cell(x, y);
1
}
fn apply_full_screen(buf: &mut Buffer, shadow: &mut ShadowTracker) -> usize {
let mut count = 0;
for y in 0..buf.height() {
for x in 0..buf.width() {
buf.set(x, y, Cell::from_char('.'));
shadow.mark_cell(x, y);
count += 1;
}
}
count
}
fn apply_no_mutations(_buf: &mut Buffer, _shadow: &mut ShadowTracker) -> usize {
0
}
fn run_frame_comparison(
frame_id: u64,
scenario: &str,
old: &Buffer,
new: &Buffer,
shadow: &ShadowTracker,
) -> FrameLog {
let w = new.width();
let h = new.height();
let full_diff = BufferDiff::compute(old, new);
let full_changes: Vec<(u16, u16)> = full_diff.iter().collect();
let dirty_diff = BufferDiff::compute_dirty(old, new);
let dirty_changes: Vec<(u16, u16)> = dirty_diff.iter().collect();
let caps = TerminalCapabilities::default();
let full_output = {
let mut sink = Vec::new();
let mut presenter = Presenter::new(&mut sink, caps);
presenter.present(new, &full_diff).unwrap();
drop(presenter);
sink
};
let dirty_output = {
let mut sink = Vec::new();
let mut presenter = Presenter::new(&mut sink, caps);
presenter.present(new, &dirty_diff).unwrap();
drop(presenter);
sink
};
let full_hash = blake3_hex(&full_output);
let dirty_hash = blake3_hex(&dirty_output);
let diff_match = full_hash == dirty_hash;
let roaring_superset = shadow.is_superset_of_changes(&full_changes);
FrameLog {
event: "roaring_frame",
frame_id,
screen: format!("{w}x{h}"),
scenario: scenario.to_string(),
width: w,
height: h,
mutations: shadow.dirty_cell_count(),
dirty_cells_roaring: shadow.dirty_cell_count(),
dirty_cells_buffer: 0, dirty_rows_roaring: shadow.dirty_row_count(h),
dirty_rows_buffer: new.dirty_row_count(),
diff_changes_full: full_changes.len(),
diff_changes_dirty: dirty_changes.len(),
diff_output_hash: full_hash,
roaring_superset,
diff_match,
}
}
fn run_scenario(
scenario_name: &str,
mutate: fn(&mut Buffer, &mut ShadowTracker) -> usize,
logs: &mut Vec<FrameLog>,
frame_counter: &mut u64,
) {
for &(w, h) in SIZES {
let old = Buffer::new(w, h);
let mut new = Buffer::new(w, h);
new.clear_dirty();
let mut shadow = ShadowTracker::new(w);
let _mutations = mutate(&mut new, &mut shadow);
let log = run_frame_comparison(*frame_counter, scenario_name, &old, &new, &shadow);
assert!(
log.roaring_superset,
"[{scenario_name} {w}x{h}] Roaring bitmap must be a superset of actual changes"
);
assert!(
log.diff_match,
"[{scenario_name} {w}x{h}] Dirty diff output must match full diff output"
);
assert!(
log.diff_changes_dirty <= log.diff_changes_full
|| log.diff_changes_dirty == log.diff_changes_full,
"[{scenario_name} {w}x{h}] Dirty diff found {} changes vs full diff {} changes",
log.diff_changes_dirty,
log.diff_changes_full,
);
logs.push(log);
*frame_counter += 1;
}
}
#[test]
fn roaring_dirty_tracking_equivalence() {
let mut logs = Vec::new();
let mut frame_id = 0u64;
run_scenario("sparse_edits", apply_sparse_edits, &mut logs, &mut frame_id);
run_scenario(
"dense_row_edits",
apply_dense_row_edits,
&mut logs,
&mut frame_id,
);
run_scenario(
"alternating_rows",
apply_alternating_rows,
&mut logs,
&mut frame_id,
);
run_scenario("single_cell", apply_single_cell, &mut logs, &mut frame_id);
run_scenario("full_screen", apply_full_screen, &mut logs, &mut frame_id);
run_scenario("empty_dirty", apply_no_mutations, &mut logs, &mut frame_id);
for log in &logs {
let json = serde_json::to_string(log).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["event"], "roaring_frame");
}
let total_frames = logs.len();
let all_match = logs.iter().all(|l| l.diff_match);
let all_superset = logs.iter().all(|l| l.roaring_superset);
assert_eq!(
total_frames,
SIZES.len() * 6,
"expected {} frames (6 scenarios x {} sizes)",
SIZES.len() * 6,
SIZES.len()
);
assert!(all_match, "all frames must have matching diff output");
assert!(
all_superset,
"roaring must be superset of changes in all frames"
);
eprintln!("--- roaring_dirty_e2e summary ---");
eprintln!("frames: {total_frames}");
eprintln!("all_diff_match: {all_match}");
eprintln!("all_roaring_superset: {all_superset}");
for log in &logs {
eprintln!(
" {} {:>7} mutations={:<6} roaring_cells={:<6} buffer_cells={:<6} diff_full={:<6} diff_dirty={:<6} match={}",
log.scenario,
log.screen,
log.mutations,
log.dirty_cells_roaring,
log.dirty_cells_buffer,
log.diff_changes_full,
log.diff_changes_dirty,
log.diff_match,
);
}
}
#[test]
fn roaring_incremental_multi_frame() {
let w = 120u16;
let h = 40u16;
let mut old = Buffer::new(w, h);
let mut new = Buffer::new(w, h);
let mut shadow = ShadowTracker::new(w);
let mut logs = Vec::new();
for y in 0..h {
for x in 0..w {
new.set(x, y, Cell::from_char(' '));
shadow.mark_cell(x, y);
}
}
let log = run_frame_comparison(0, "initial", &old, &new, &shadow);
assert!(log.diff_match);
assert!(log.roaring_superset);
logs.push(log);
old = new.clone();
new.clear_dirty();
shadow.clear();
let text = "Hello, Roaring Bitmap!";
for (i, ch) in text.chars().enumerate() {
let x = i as u16;
if x < w {
new.set(x, 5, Cell::from_char(ch));
shadow.mark_cell(x, 5);
}
}
let log = run_frame_comparison(1, "type_text", &old, &new, &shadow);
assert!(log.diff_match);
assert!(log.roaring_superset);
assert_eq!(log.dirty_rows_roaring, 1);
assert_eq!(log.dirty_rows_buffer, 1);
logs.push(log);
old = new.clone();
new.clear_dirty();
shadow.clear();
for y in 0..h - 1 {
for x in 0..w {
if let Some(&cell) = old.get(x, y + 1) {
new.set(x, y, cell);
}
shadow.mark_cell(x, y);
}
}
for x in 0..w {
new.set(x, h - 1, Cell::from_char(' '));
shadow.mark_cell(x, h - 1);
}
let log = run_frame_comparison(2, "scroll", &old, &new, &shadow);
assert!(log.diff_match);
assert!(log.roaring_superset);
logs.push(log);
old = new.clone();
new.clear_dirty();
shadow.clear();
let log = run_frame_comparison(3, "idle", &old, &new, &shadow);
assert!(log.diff_match);
assert!(log.roaring_superset);
assert_eq!(log.diff_changes_full, 0);
assert_eq!(log.diff_changes_dirty, 0);
assert_eq!(log.dirty_cells_roaring, 0);
logs.push(log);
for log in &logs {
let json = serde_json::to_string(log).unwrap();
let _: serde_json::Value = serde_json::from_str(&json).unwrap();
}
eprintln!("--- roaring_incremental_multi_frame summary ---");
for log in &logs {
eprintln!(
" frame={} scenario={} mutations={} diff_full={} diff_dirty={} match={}",
log.frame_id,
log.scenario,
log.mutations,
log.diff_changes_full,
log.diff_changes_dirty,
log.diff_match,
);
}
}
#[test]
fn roaring_tracks_union_correctly() {
for &(w, h) in SIZES {
let mut shadow_a = ShadowTracker::new(w);
let mut shadow_b = ShadowTracker::new(w);
for x in (1..w).step_by(2) {
shadow_a.mark_cell(x, 0);
}
for x in (0..w).step_by(2) {
shadow_b.mark_cell(x, 0);
}
let union = shadow_a.bitmap.union(&shadow_b.bitmap);
for x in 0..w {
assert!(
union.contains(x as u32),
"union missing cell ({x}, 0) at {w}x{h}"
);
}
assert_eq!(union.cardinality(), w as usize);
}
}
#[test]
fn roaring_intersection_finds_overlapping_dirty() {
let w = 120u16;
let h = 40u16;
let mut shadow_a = ShadowTracker::new(w);
let mut shadow_b = ShadowTracker::new(w);
for x in 0..60 {
shadow_a.mark_cell(x, 10);
}
for x in 30..90 {
shadow_b.mark_cell(x, 10);
}
let intersection = shadow_a.bitmap.intersection(&shadow_b.bitmap);
assert_eq!(intersection.cardinality(), 30);
for x in 30..60 {
let idx = 10u32 * w as u32 + x as u32;
assert!(intersection.contains(idx));
}
for x in 0..30 {
let idx = 10u32 * w as u32 + x as u32;
assert!(!intersection.contains(idx));
}
for x in 60..90u16 {
let idx = 10u32 * w as u32 + x as u32;
assert!(!intersection.contains(idx));
}
let _ = h; }
#[test]
fn jsonl_schema_compliance() {
let old = Buffer::new(80, 24);
let mut new = Buffer::new(80, 24);
new.clear_dirty();
new.set(0, 0, Cell::from_char('A'));
let mut shadow = ShadowTracker::new(80);
shadow.mark_cell(0, 0);
let log = run_frame_comparison(0, "schema_test", &old, &new, &shadow);
let json = serde_json::to_string(&log).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["event"].is_string());
assert!(parsed["frame_id"].is_u64());
assert!(parsed["screen"].is_string());
assert!(parsed["scenario"].is_string());
assert!(parsed["width"].is_u64());
assert!(parsed["height"].is_u64());
assert!(parsed["mutations"].is_u64());
assert!(parsed["dirty_cells_roaring"].is_u64());
assert!(parsed["dirty_cells_buffer"].is_u64());
assert!(parsed["dirty_rows_roaring"].is_u64());
assert!(parsed["dirty_rows_buffer"].is_u64());
assert!(parsed["diff_changes_full"].is_u64());
assert!(parsed["diff_changes_dirty"].is_u64());
assert!(parsed["diff_output_hash"].is_string());
assert!(parsed["roaring_superset"].is_boolean());
assert!(parsed["diff_match"].is_boolean());
assert_eq!(parsed["event"], "roaring_frame");
assert_eq!(parsed["width"], 80);
assert_eq!(parsed["height"], 24);
assert!(parsed["diff_match"].as_bool().unwrap());
assert!(parsed["roaring_superset"].as_bool().unwrap());
}