use crate::screen::cell::Row;
use crate::screen::traits::TerminalEmulator;
use super::core::hash_row;
const DIRTY_SENTINEL: u64 = u64::MAX;
#[derive(Default)]
pub struct DirtyTracker {
row_hashes: Vec<u64>,
}
impl DirtyTracker {
pub fn new() -> Self {
Self {
row_hashes: Vec::new(),
}
}
pub fn invalidate(&mut self) {
self.row_hashes.clear();
}
pub fn ensure_len(&mut self, rows: usize) {
self.row_hashes.resize(rows, DIRTY_SENTINEL);
}
pub fn check_row(&mut self, y: usize, row: &Row) -> bool {
debug_assert!(
y < self.row_hashes.len(),
"DirtyTracker::check_row: y={y} out of bounds; call ensure_len first"
);
let hash = hash_row(row);
let dirty = self.row_hashes[y] != hash;
self.row_hashes[y] = hash;
dirty
}
#[cfg(test)]
pub(in crate::screen) fn is_empty(&self) -> bool {
self.row_hashes.is_empty()
}
pub fn dirty_rows<E: TerminalEmulator + ?Sized>(&mut self, emu: &E) -> Vec<u16> {
let rows = emu.rows() as usize;
let all_dirty = self.row_hashes.len() != rows;
if all_dirty {
self.row_hashes.clear();
self.row_hashes.resize(rows, 0);
}
let mut dirty = Vec::new();
for y in 0..rows {
let hash = hash_row(emu.visible_row(y as u16));
if all_dirty || self.row_hashes[y] != hash {
dirty.push(y as u16);
self.row_hashes[y] = hash;
}
}
dirty
}
}
#[cfg(test)]
mod tests {
use super::DirtyTracker;
use crate::screen::Screen;
#[test]
fn first_call_reports_all_rows() {
let s = Screen::new(10, 4, 0);
let mut t = DirtyTracker::new();
assert_eq!(t.dirty_rows(&s), vec![0, 1, 2, 3]);
}
#[test]
fn unchanged_screen_reports_nothing() {
let s = Screen::new(10, 4, 0);
let mut t = DirtyTracker::new();
t.dirty_rows(&s);
assert!(t.dirty_rows(&s).is_empty());
}
#[test]
fn single_row_change_reports_exactly_that_row() {
let mut s = Screen::new(10, 4, 0);
let mut t = DirtyTracker::new();
s.process(b"a\r\nb");
t.dirty_rows(&s);
s.process(b"\x1b[1;5Hx"); assert_eq!(t.dirty_rows(&s), vec![0]);
}
#[test]
fn resize_reports_all_rows() {
let mut s = Screen::new(10, 4, 0);
let mut t = DirtyTracker::new();
t.dirty_rows(&s);
s.resize(10, 6);
assert_eq!(t.dirty_rows(&s), vec![0, 1, 2, 3, 4, 5]);
}
#[test]
fn invalidate_reports_all_rows() {
let s = Screen::new(10, 4, 0);
let mut t = DirtyTracker::new();
t.dirty_rows(&s);
t.invalidate();
assert_eq!(t.dirty_rows(&s), vec![0, 1, 2, 3]);
}
#[test]
fn ensure_len_preserves_prefix_and_new_slots_start_dirty() {
let mut s = Screen::new(10, 2, 0);
s.process(b"a\r\nb");
let mut t = DirtyTracker::new();
t.dirty_rows(&s); t.ensure_len(4);
let rows: Vec<_> = s.visible_rows().collect();
assert!(!t.check_row(0, rows[0]), "prefix hash preserved -> clean");
assert!(!t.check_row(1, rows[1]), "prefix hash preserved -> clean");
assert!(t.check_row(2, rows[0]), "new slot starts dirty");
assert!(t.check_row(3, rows[1]), "new slot starts dirty");
}
#[test]
fn check_row_detects_change_and_updates() {
let mut s = Screen::new(10, 2, 0);
let mut t = DirtyTracker::new();
t.ensure_len(2);
{
let rows: Vec<_> = s.visible_rows().collect();
assert!(t.check_row(0, rows[0]), "fresh slot is dirty");
assert!(!t.check_row(0, rows[0]), "hash stored -> clean");
}
s.process(b"x");
let rows: Vec<_> = s.visible_rows().collect();
assert!(t.check_row(0, rows[0]), "content changed -> dirty");
}
#[test]
fn invalidate_then_ensure_len_marks_all_dirty() {
let s = Screen::new(10, 3, 0);
let mut t = DirtyTracker::new();
t.dirty_rows(&s);
t.invalidate();
t.ensure_len(3);
for (y, row) in s.visible_rows().enumerate() {
assert!(t.check_row(y, row), "row {y} must be dirty after invalidate");
}
}
#[test]
fn agrees_with_ansi_renderer_dirty_decision() {
let mut s = Screen::new(10, 4, 0);
let mut t = DirtyTracker::new();
let mut r = crate::screen::AnsiRenderer::new();
s.process(b"aaa\r\nbbb\r\nccc");
t.dirty_rows(&s);
r.render(&s, true);
s.process(b"\x1b[2;1HXXX"); assert_eq!(t.dirty_rows(&s), vec![1]);
let delta = r.render(&s, false);
let delta_str = String::from_utf8_lossy(&delta);
assert!(
delta_str.contains("\x1b[2;1H"),
"renderer re-emits row 1 (CUP row 2): {delta_str:?}"
);
assert!(
!delta_str.contains("\x1b[1;1H"),
"row 0 must not be re-emitted: {delta_str:?}"
);
}
}