use std::fmt::{self, Write as _};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub char: char,
pub fg: Color,
pub bg: Color,
pub attrs: Attributes,
}
impl Default for Cell {
fn default() -> Self {
Self {
char: ' ',
fg: Color::Default,
bg: Color::Default,
attrs: Attributes::empty(),
}
}
}
impl Cell {
#[must_use]
pub fn new(char: char) -> Self {
Self {
char,
..Default::default()
}
}
#[must_use]
pub const fn with_fg(mut self, color: Color) -> Self {
self.fg = color;
self
}
#[must_use]
pub const fn with_bg(mut self, color: Color) -> Self {
self.bg = color;
self
}
#[must_use]
pub const fn with_attrs(mut self, attrs: Attributes) -> Self {
self.attrs = attrs;
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.char == ' ' && self.fg == Color::Default && self.bg == Color::Default
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Color {
#[default]
Default,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Indexed(u8),
Rgb(u8, u8, u8),
}
impl Color {
#[must_use]
pub const fn from_ansi(code: u8) -> Self {
match code {
0 => Self::Black,
1 => Self::Red,
2 => Self::Green,
3 => Self::Yellow,
4 => Self::Blue,
5 => Self::Magenta,
6 => Self::Cyan,
7 => Self::White,
8 => Self::BrightBlack,
9 => Self::BrightRed,
10 => Self::BrightGreen,
11 => Self::BrightYellow,
12 => Self::BrightBlue,
13 => Self::BrightMagenta,
14 => Self::BrightCyan,
15 => Self::BrightWhite,
_ => Self::Indexed(code),
}
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Attributes: u8 {
const BOLD = 0b0000_0001;
const DIM = 0b0000_0010;
const ITALIC = 0b0000_0100;
const UNDERLINE = 0b0000_1000;
const BLINK = 0b0001_0000;
const INVERSE = 0b0010_0000;
const HIDDEN = 0b0100_0000;
const STRIKETHROUGH = 0b1000_0000;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Cursor {
pub row: usize,
pub col: usize,
pub visible: bool,
}
impl Cursor {
#[must_use]
pub const fn new() -> Self {
Self {
row: 0,
col: 0,
visible: true,
}
}
pub const fn goto(&mut self, row: usize, col: usize) {
self.row = row;
self.col = col;
}
pub fn move_by(&mut self, rows: i32, cols: i32) {
self.row = (self.row as i32 + rows).max(0) as usize;
self.col = (self.col as i32 + cols).max(0) as usize;
}
}
#[derive(Clone)]
pub struct ScreenBuffer {
rows: usize,
cols: usize,
cells: Vec<Cell>,
cursor: Cursor,
current_style: Cell,
scroll_region: (usize, usize),
saved_cursor: Option<Cursor>,
}
impl ScreenBuffer {
#[must_use]
pub fn new(rows: usize, cols: usize) -> Self {
let cells = vec![Cell::default(); rows * cols];
Self {
rows,
cols,
cells,
cursor: Cursor::new(),
current_style: Cell::default(),
scroll_region: (0, rows.saturating_sub(1)),
saved_cursor: None,
}
}
#[must_use]
pub const fn rows(&self) -> usize {
self.rows
}
#[must_use]
pub const fn cols(&self) -> usize {
self.cols
}
#[must_use]
pub fn get(&self, row: usize, col: usize) -> Option<&Cell> {
if row < self.rows && col < self.cols {
Some(&self.cells[row * self.cols + col])
} else {
None
}
}
pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut Cell> {
if row < self.rows && col < self.cols {
Some(&mut self.cells[row * self.cols + col])
} else {
None
}
}
pub fn set(&mut self, row: usize, col: usize, cell: Cell) {
if row < self.rows && col < self.cols {
self.cells[row * self.cols + col] = cell;
}
}
pub fn write_char(&mut self, c: char) {
if self.cursor.row < self.rows && self.cursor.col < self.cols {
let idx = self.cursor.row * self.cols + self.cursor.col;
self.cells[idx] = Cell {
char: c,
fg: self.current_style.fg,
bg: self.current_style.bg,
attrs: self.current_style.attrs,
};
self.cursor.col += 1;
if self.cursor.col >= self.cols {
self.cursor.col = 0;
self.cursor.row += 1;
if self.cursor.row >= self.rows {
self.scroll_up(1);
self.cursor.row = self.rows - 1;
}
}
}
}
#[must_use]
pub const fn cursor(&self) -> &Cursor {
&self.cursor
}
pub const fn cursor_mut(&mut self) -> &mut Cursor {
&mut self.cursor
}
pub fn goto(&mut self, row: usize, col: usize) {
self.cursor.row = row.min(self.rows.saturating_sub(1));
self.cursor.col = col.min(self.cols.saturating_sub(1));
}
pub fn clear(&mut self) {
self.cells.fill(Cell::default());
}
pub fn clear_to_end(&mut self) {
let start = self.cursor.row * self.cols + self.cursor.col;
for cell in &mut self.cells[start..] {
*cell = Cell::default();
}
}
pub fn clear_to_start(&mut self) {
let end = self.cursor.row * self.cols + self.cursor.col + 1;
for cell in &mut self.cells[..end] {
*cell = Cell::default();
}
}
pub fn clear_line(&mut self) {
let start = self.cursor.row * self.cols;
let end = start + self.cols;
for cell in &mut self.cells[start..end] {
*cell = Cell::default();
}
}
pub fn clear_line_to_end(&mut self) {
let start = self.cursor.row * self.cols + self.cursor.col;
let end = self.cursor.row * self.cols + self.cols;
for cell in &mut self.cells[start..end] {
*cell = Cell::default();
}
}
pub fn scroll_up(&mut self, n: usize) {
let (top, bottom) = self.scroll_region;
let scroll_height = bottom - top + 1;
let n = n.min(scroll_height);
if n == 0 {
return;
}
if n <= bottom.saturating_sub(top) {
for row in top..=bottom.saturating_sub(n) {
let src_start = (row + n) * self.cols;
let dst_start = row * self.cols;
for col in 0..self.cols {
self.cells[dst_start + col] = self.cells[src_start + col];
}
}
}
for row in bottom.saturating_sub(n).saturating_add(1)..=bottom {
let start = row * self.cols;
for col in 0..self.cols {
self.cells[start + col] = Cell::default();
}
}
}
pub fn scroll_down(&mut self, n: usize) {
let (top, bottom) = self.scroll_region;
let scroll_height = bottom - top + 1;
let n = n.min(scroll_height);
if n == 0 {
return;
}
for row in (top + n..=bottom).rev() {
let src_start = (row - n) * self.cols;
let dst_start = row * self.cols;
for col in 0..self.cols {
self.cells[dst_start + col] = self.cells[src_start + col];
}
}
for row in top..top + n {
let start = row * self.cols;
for col in 0..self.cols {
self.cells[start + col] = Cell::default();
}
}
}
pub fn set_scroll_region(&mut self, top: usize, bottom: usize) {
let top = top.min(self.rows.saturating_sub(1));
let bottom = bottom.min(self.rows.saturating_sub(1)).max(top);
self.scroll_region = (top, bottom);
}
pub const fn reset_scroll_region(&mut self) {
self.scroll_region = (0, self.rows.saturating_sub(1));
}
pub const fn save_cursor(&mut self) {
self.saved_cursor = Some(self.cursor);
}
pub const fn restore_cursor(&mut self) {
if let Some(cursor) = self.saved_cursor.take() {
self.cursor = cursor;
}
}
pub const fn set_style(&mut self, fg: Color, bg: Color, attrs: Attributes) {
self.current_style.fg = fg;
self.current_style.bg = bg;
self.current_style.attrs = attrs;
}
pub fn reset_style(&mut self) {
self.current_style = Cell::default();
}
pub fn insert_chars(&mut self, n: usize) {
if n == 0 || self.cursor.row >= self.rows || self.cursor.col >= self.cols {
return;
}
let row = self.cursor.row;
let col = self.cursor.col;
let n = n.min(self.cols - col);
let row_start = row * self.cols;
for c in (col + n..self.cols).rev() {
self.cells[row_start + c] = self.cells[row_start + c - n];
}
for c in col..col + n {
self.cells[row_start + c] = Cell::default();
}
}
pub fn delete_chars(&mut self, n: usize) {
if n == 0 || self.cursor.row >= self.rows || self.cursor.col >= self.cols {
return;
}
let row = self.cursor.row;
let col = self.cursor.col;
let n = n.min(self.cols - col);
let row_start = row * self.cols;
for c in col..self.cols - n {
self.cells[row_start + c] = self.cells[row_start + c + n];
}
for c in self.cols - n..self.cols {
self.cells[row_start + c] = Cell::default();
}
}
pub fn insert_lines(&mut self, n: usize) {
if n == 0 || self.cursor.row > self.scroll_region.1 {
return;
}
let (top, bottom) = self.scroll_region;
let start_row = self.cursor.row.max(top);
let region_height = bottom - start_row + 1;
let n = n.min(region_height);
for row in (start_row + n..=bottom).rev() {
let src_start = (row - n) * self.cols;
let dst_start = row * self.cols;
for col in 0..self.cols {
self.cells[dst_start + col] = self.cells[src_start + col];
}
}
for row in start_row..start_row + n {
let row_start = row * self.cols;
for col in 0..self.cols {
self.cells[row_start + col] = Cell::default();
}
}
}
pub fn delete_lines(&mut self, n: usize) {
if n == 0 || self.cursor.row > self.scroll_region.1 {
return;
}
let (top, bottom) = self.scroll_region;
let start_row = self.cursor.row.max(top);
let region_height = bottom - start_row + 1;
let n = n.min(region_height);
for row in start_row..=bottom.saturating_sub(n) {
let src_start = (row + n) * self.cols;
let dst_start = row * self.cols;
for col in 0..self.cols {
self.cells[dst_start + col] = self.cells[src_start + col];
}
}
for row in bottom - n + 1..=bottom {
let row_start = row * self.cols;
for col in 0..self.cols {
self.cells[row_start + col] = Cell::default();
}
}
}
#[must_use]
pub fn row_text(&self, row: usize) -> String {
if row >= self.rows {
return String::new();
}
let start = row * self.cols;
let end = start + self.cols;
self.cells[start..end]
.iter()
.map(|c| c.char)
.collect::<String>()
.trim_end()
.to_string()
}
#[must_use]
pub fn text(&self) -> String {
(0..self.rows)
.map(|r| self.row_text(r))
.collect::<Vec<_>>()
.join("\n")
}
pub fn resize(&mut self, new_rows: usize, new_cols: usize) {
let mut new_cells = vec![Cell::default(); new_rows * new_cols];
for row in 0..new_rows.min(self.rows) {
for col in 0..new_cols.min(self.cols) {
new_cells[row * new_cols + col] = self.cells[row * self.cols + col];
}
}
self.rows = new_rows;
self.cols = new_cols;
self.cells = new_cells;
self.cursor.row = self.cursor.row.min(new_rows.saturating_sub(1));
self.cursor.col = self.cursor.col.min(new_cols.saturating_sub(1));
self.scroll_region = (0, new_rows.saturating_sub(1));
}
}
impl fmt::Debug for ScreenBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScreenBuffer")
.field("rows", &self.rows)
.field("cols", &self.cols)
.field("cursor", &self.cursor)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeType {
None,
Char,
FgColor,
BgColor,
Attrs,
Multiple,
}
#[derive(Debug, Clone)]
pub struct CellChange {
pub row: usize,
pub col: usize,
pub old: Cell,
pub new: Cell,
pub change_type: ChangeType,
}
impl CellChange {
#[must_use]
pub fn new(row: usize, col: usize, old: Cell, new: Cell) -> Self {
let change_type = Self::compute_change_type(&old, &new);
Self {
row,
col,
old,
new,
change_type,
}
}
#[allow(clippy::useless_let_if_seq)] fn compute_change_type(old: &Cell, new: &Cell) -> ChangeType {
let mut changes = 0;
let mut last_type = ChangeType::None;
if old.char != new.char {
changes += 1;
last_type = ChangeType::Char;
}
if old.fg != new.fg {
changes += 1;
last_type = ChangeType::FgColor;
}
if old.bg != new.bg {
changes += 1;
last_type = ChangeType::BgColor;
}
if old.attrs != new.attrs {
changes += 1;
last_type = ChangeType::Attrs;
}
match changes {
0 => ChangeType::None,
1 => last_type,
_ => ChangeType::Multiple,
}
}
#[must_use]
pub fn is_significant(&self) -> bool {
!self.old.is_empty() || !self.new.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ScreenDiff {
changes: Vec<CellChange>,
cursor_changed: bool,
old_cursor: Cursor,
new_cursor: Cursor,
dimensions_changed: bool,
old_dims: (usize, usize),
new_dims: (usize, usize),
}
impl ScreenDiff {
#[must_use]
pub fn compute(old: &ScreenBuffer, new: &ScreenBuffer) -> Self {
let mut changes = Vec::new();
let dimensions_changed = old.rows != new.rows || old.cols != new.cols;
let cursor_changed = old.cursor != new.cursor;
let min_rows = old.rows.min(new.rows);
let min_cols = old.cols.min(new.cols);
for row in 0..min_rows {
for col in 0..min_cols {
let old_cell = old.get(row, col).copied().unwrap_or_default();
let new_cell = new.get(row, col).copied().unwrap_or_default();
if old_cell != new_cell {
changes.push(CellChange::new(row, col, old_cell, new_cell));
}
}
}
if new.rows > old.rows {
for row in old.rows..new.rows {
for col in 0..new.cols {
let new_cell = new.get(row, col).copied().unwrap_or_default();
if !new_cell.is_empty() {
changes.push(CellChange::new(row, col, Cell::default(), new_cell));
}
}
}
}
if new.cols > old.cols {
for row in 0..min_rows {
for col in old.cols..new.cols {
let new_cell = new.get(row, col).copied().unwrap_or_default();
if !new_cell.is_empty() {
changes.push(CellChange::new(row, col, Cell::default(), new_cell));
}
}
}
}
Self {
changes,
cursor_changed,
old_cursor: old.cursor,
new_cursor: new.cursor,
dimensions_changed,
old_dims: (old.rows, old.cols),
new_dims: (new.rows, new.cols),
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.changes.is_empty() && !self.cursor_changed && !self.dimensions_changed
}
#[must_use]
pub const fn change_count(&self) -> usize {
self.changes.len()
}
#[must_use]
pub fn changes(&self) -> &[CellChange] {
&self.changes
}
#[must_use]
pub fn significant_changes(&self) -> Vec<&CellChange> {
self.changes.iter().filter(|c| c.is_significant()).collect()
}
#[must_use]
pub const fn cursor_changed(&self) -> bool {
self.cursor_changed
}
#[must_use]
pub const fn old_cursor(&self) -> Cursor {
self.old_cursor
}
#[must_use]
pub const fn new_cursor(&self) -> Cursor {
self.new_cursor
}
#[must_use]
pub const fn dimensions_changed(&self) -> bool {
self.dimensions_changed
}
#[must_use]
pub fn changed_rows(&self) -> Vec<usize> {
let mut rows: Vec<usize> = self.changes.iter().map(|c| c.row).collect();
rows.sort_unstable();
rows.dedup();
rows
}
#[must_use]
pub fn row_changes(&self, row: usize) -> Vec<&CellChange> {
self.changes.iter().filter(|c| c.row == row).collect()
}
#[must_use]
pub fn report(&self) -> String {
let mut output = String::new();
if self.is_empty() {
output.push_str("No changes detected.\n");
return output;
}
if self.dimensions_changed {
let _ = writeln!(
output,
"Dimensions changed: {}x{} -> {}x{}",
self.old_dims.1, self.old_dims.0, self.new_dims.1, self.new_dims.0
);
}
if self.cursor_changed {
let _ = writeln!(
output,
"Cursor moved: ({}, {}) -> ({}, {})",
self.old_cursor.row, self.old_cursor.col, self.new_cursor.row, self.new_cursor.col
);
}
let significant = self.significant_changes();
if !significant.is_empty() {
let _ = writeln!(output, "{} cell(s) changed:", significant.len());
for row in self.changed_rows() {
let row_changes: Vec<_> = significant.iter().filter(|c| c.row == row).collect();
if !row_changes.is_empty() {
let _ = writeln!(output, " Row {row}:");
for change in row_changes {
let old_char = if change.old.char.is_control() {
format!("\\x{:02x}", change.old.char as u8)
} else {
change.old.char.to_string()
};
let new_char = if change.new.char.is_control() {
format!("\\x{:02x}", change.new.char as u8)
} else {
change.new.char.to_string()
};
let _ = writeln!(
output,
" Col {}: '{}' -> '{}' ({:?})",
change.col, old_char, new_char, change.change_type
);
}
}
}
}
output
}
#[must_use]
pub fn row_visual_diff(&self, old: &ScreenBuffer, new: &ScreenBuffer, row: usize) -> String {
let old_text = old.row_text(row);
let new_text = new.row_text(row);
if old_text == new_text {
format!(" {row}: {old_text}")
} else {
format!("- {row}: {old_text}\n+ {row}: {new_text}")
}
}
#[must_use]
pub fn visual_diff(&self, old: &ScreenBuffer, new: &ScreenBuffer) -> String {
let mut output = String::new();
let max_rows = old.rows.max(new.rows);
for row in 0..max_rows {
let old_text = if row < old.rows {
old.row_text(row)
} else {
String::new()
};
let new_text = if row < new.rows {
new.row_text(row)
} else {
String::new()
};
if old_text != new_text {
if !old_text.is_empty() || row < old.rows {
let _ = writeln!(output, "- {row}: {old_text}");
}
if !new_text.is_empty() || row < new.rows {
let _ = writeln!(output, "+ {row}: {new_text}");
}
} else if !old_text.is_empty() {
let _ = writeln!(output, " {row}: {old_text}");
}
}
output
}
}
impl ScreenBuffer {
#[must_use]
pub fn diff(&self, other: &Self) -> ScreenDiff {
ScreenDiff::compute(self, other)
}
#[must_use]
pub fn equals(&self, other: &Self) -> bool {
self.diff(other).is_empty()
}
#[must_use]
pub fn snapshot(&self) -> Self {
self.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn screen_buffer_basic() {
let mut buf = ScreenBuffer::new(24, 80);
assert_eq!(buf.rows(), 24);
assert_eq!(buf.cols(), 80);
buf.write_char('H');
buf.write_char('i');
assert_eq!(buf.row_text(0), "Hi");
}
#[test]
fn screen_buffer_cursor() {
let mut buf = ScreenBuffer::new(24, 80);
buf.goto(5, 10);
assert_eq!(buf.cursor().row, 5);
assert_eq!(buf.cursor().col, 10);
}
#[test]
fn screen_buffer_clear() {
let mut buf = ScreenBuffer::new(24, 80);
buf.write_char('A');
buf.clear();
assert!(buf.row_text(0).is_empty());
}
#[test]
fn screen_buffer_scroll() {
let mut buf = ScreenBuffer::new(3, 10);
buf.goto(0, 0);
for c in "Line 1".chars() {
buf.write_char(c);
}
buf.goto(1, 0);
for c in "Line 2".chars() {
buf.write_char(c);
}
buf.goto(2, 0);
for c in "Line 3".chars() {
buf.write_char(c);
}
buf.scroll_up(1);
assert_eq!(buf.row_text(0), "Line 2");
assert_eq!(buf.row_text(1), "Line 3");
assert!(buf.row_text(2).is_empty());
}
#[test]
fn insert_chars_shifts_right() {
let mut buf = ScreenBuffer::new(1, 10);
for c in "ABCDE".chars() {
buf.write_char(c);
}
buf.goto(0, 2);
buf.insert_chars(2);
assert_eq!(buf.row_text(0), "AB CDE");
}
#[test]
fn insert_chars_at_end_of_line() {
let mut buf = ScreenBuffer::new(1, 5);
for c in "ABCDE".chars() {
buf.write_char(c);
}
buf.goto(0, 3);
buf.insert_chars(3);
assert_eq!(buf.row_text(0), "ABC");
}
#[test]
fn delete_chars_shifts_left() {
let mut buf = ScreenBuffer::new(1, 10);
for c in "ABCDEFGH".chars() {
buf.write_char(c);
}
buf.goto(0, 2);
buf.delete_chars(3);
assert_eq!(buf.row_text(0), "ABFGH");
}
#[test]
fn insert_lines_pushes_down() {
let mut buf = ScreenBuffer::new(5, 10);
for (i, text) in ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"]
.iter()
.enumerate()
{
buf.goto(i, 0);
for c in text.chars() {
buf.write_char(c);
}
}
buf.goto(1, 0);
buf.insert_lines(2);
assert_eq!(buf.row_text(0), "Line 0");
assert!(buf.row_text(1).is_empty()); assert!(buf.row_text(2).is_empty()); assert_eq!(buf.row_text(3), "Line 1"); assert_eq!(buf.row_text(4), "Line 2"); }
#[test]
fn delete_lines_pulls_up() {
let mut buf = ScreenBuffer::new(5, 10);
for (i, text) in ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"]
.iter()
.enumerate()
{
buf.goto(i, 0);
for c in text.chars() {
buf.write_char(c);
}
}
buf.goto(1, 0);
buf.delete_lines(2);
assert_eq!(buf.row_text(0), "Line 0");
assert_eq!(buf.row_text(1), "Line 3"); assert_eq!(buf.row_text(2), "Line 4"); assert!(buf.row_text(3).is_empty()); assert!(buf.row_text(4).is_empty()); }
#[test]
fn diff_no_changes() {
let buf1 = ScreenBuffer::new(3, 10);
let buf2 = ScreenBuffer::new(3, 10);
let diff = buf1.diff(&buf2);
assert!(diff.is_empty());
assert_eq!(diff.change_count(), 0);
assert!(!diff.cursor_changed());
assert!(!diff.dimensions_changed());
}
#[test]
fn diff_character_changes() {
let mut buf1 = ScreenBuffer::new(3, 10);
let mut buf2 = ScreenBuffer::new(3, 10);
for c in "Hello".chars() {
buf1.write_char(c);
}
buf1.goto(0, 0);
for c in "World".chars() {
buf2.write_char(c);
}
buf2.goto(0, 0);
let diff = buf1.diff(&buf2);
assert!(!diff.is_empty());
assert_eq!(diff.change_count(), 4);
assert_eq!(diff.changed_rows(), vec![0]);
}
#[test]
fn diff_cursor_moved() {
let buf1 = ScreenBuffer::new(3, 10);
let mut buf2 = buf1.snapshot();
buf2.goto(2, 5);
let diff = buf1.diff(&buf2);
assert!(!diff.is_empty());
assert!(diff.cursor_changed());
assert_eq!(diff.old_cursor().row, 0);
assert_eq!(diff.old_cursor().col, 0);
assert_eq!(diff.new_cursor().row, 2);
assert_eq!(diff.new_cursor().col, 5);
}
#[test]
fn diff_dimensions_changed() {
let buf1 = ScreenBuffer::new(3, 10);
let buf2 = ScreenBuffer::new(5, 15);
let diff = buf1.diff(&buf2);
assert!(diff.dimensions_changed());
}
#[test]
fn diff_significant_changes() {
let buf1 = ScreenBuffer::new(3, 10);
let mut buf2 = ScreenBuffer::new(3, 10);
for c in "Hi".chars() {
buf2.write_char(c);
}
buf2.goto(0, 0);
let diff = buf1.diff(&buf2);
let significant = diff.significant_changes();
assert_eq!(significant.len(), 2); }
#[test]
fn diff_report() {
let buf1 = ScreenBuffer::new(3, 10);
let mut buf2 = ScreenBuffer::new(3, 10);
for c in "ABC".chars() {
buf2.write_char(c);
}
let diff = buf1.diff(&buf2);
let report = diff.report();
assert!(report.contains("3 cell(s) changed"));
assert!(report.contains("Row 0"));
}
#[test]
fn diff_visual_output() {
let mut buf1 = ScreenBuffer::new(3, 10);
let mut buf2 = ScreenBuffer::new(3, 10);
for c in "Line 1".chars() {
buf1.write_char(c);
}
buf1.goto(0, 0);
for c in "Line 2".chars() {
buf2.write_char(c);
}
buf2.goto(0, 0);
let diff = buf1.diff(&buf2);
let visual = diff.visual_diff(&buf1, &buf2);
assert!(visual.contains("- 0: Line 1"));
assert!(visual.contains("+ 0: Line 2"));
}
#[test]
fn buffer_equals() {
let buf1 = ScreenBuffer::new(3, 10);
let buf2 = ScreenBuffer::new(3, 10);
let buf3 = ScreenBuffer::new(5, 10);
assert!(buf1.equals(&buf2));
assert!(!buf1.equals(&buf3));
}
#[test]
fn buffer_snapshot() {
let mut buf = ScreenBuffer::new(3, 10);
for c in "Test".chars() {
buf.write_char(c);
}
let snapshot = buf.snapshot();
assert!(buf.equals(&snapshot));
buf.clear();
assert!(!buf.equals(&snapshot));
}
#[test]
fn cell_change_types() {
let old = Cell::new('A');
let new_char = Cell::new('B');
let change = CellChange::new(0, 0, old, new_char);
assert_eq!(change.change_type, ChangeType::Char);
let new_fg = Cell::new('A').with_fg(Color::Red);
let change = CellChange::new(0, 0, old, new_fg);
assert_eq!(change.change_type, ChangeType::FgColor);
let new_multiple = Cell::new('B').with_fg(Color::Red);
let change = CellChange::new(0, 0, old, new_multiple);
assert_eq!(change.change_type, ChangeType::Multiple);
}
#[test]
fn row_changes_filter() {
let buf1 = ScreenBuffer::new(3, 10);
let mut buf2 = ScreenBuffer::new(3, 10);
buf2.goto(0, 0);
for c in "Row 0".chars() {
buf2.write_char(c);
}
buf2.goto(1, 0);
for c in "Row 1".chars() {
buf2.write_char(c);
}
let diff = buf1.diff(&buf2);
let row0_changes = diff.row_changes(0);
let row1_changes = diff.row_changes(1);
assert_eq!(row0_changes.len(), 4); assert_eq!(row1_changes.len(), 4); }
}