use super::{Buffer, Cell, CellFlags, Modifiers, Rgb};
use crate::layout::Rect;
use std::io::Write;
#[derive(Debug, Clone)]
pub struct DiffState {
cursor_x: u16,
cursor_y: u16,
fg: Option<Rgb>,
bg: Option<Rgb>,
modifiers: Option<Modifiers>,
}
impl Default for DiffState {
fn default() -> Self {
Self::new()
}
}
impl DiffState {
pub const fn new() -> Self {
Self {
cursor_x: 0,
cursor_y: 0,
fg: None,
bg: None,
modifiers: None,
}
}
pub const fn reset(&mut self) {
self.fg = None;
self.bg = None;
self.modifiers = None;
self.cursor_x = u16::MAX;
self.cursor_y = u16::MAX;
}
}
#[derive(Debug, Clone, Default)]
pub struct DiffResult {
pub cells_changed: usize,
pub cursor_moves: usize,
pub color_changes: usize,
pub modifier_changes: usize,
}
pub fn render_diff(
current: &Buffer,
next: &Buffer,
dirty_rects: &[Rect],
output: &mut Vec<u8>,
state: &mut DiffState,
) -> DiffResult {
debug_assert_eq!(current.width(), next.width());
debug_assert_eq!(current.height(), next.height());
let mut result = DiffResult::default();
let width = current.width();
let height = current.height();
let full_rect = Rect::from_size(width, height);
let rects: &[Rect] = if dirty_rects.is_empty() {
std::slice::from_ref(&full_rect)
} else {
dirty_rects
};
for rect in rects {
diff_rect(current, next, *rect, output, state, &mut result);
}
result
}
fn diff_rect(
current: &Buffer,
next: &Buffer,
rect: Rect,
output: &mut Vec<u8>,
state: &mut DiffState,
result: &mut DiffResult,
) {
let width = current.width();
let x_end = (rect.x + rect.width).min(width);
let y_end = (rect.y + rect.height).min(current.height());
for y in rect.y..y_end {
for x in rect.x..x_end {
let idx = (y as usize) * (width as usize) + (x as usize);
let current_cell = ¤t.cells()[idx];
let next_cell = &next.cells()[idx];
if current_cell == next_cell {
continue;
}
if next_cell.is_wide_continuation() {
continue;
}
result.cells_changed += 1;
if state.cursor_y != y || state.cursor_x != x {
emit_cursor_move(output, x, y);
state.cursor_x = x;
state.cursor_y = y;
result.cursor_moves += 1;
}
let next_mods = next_cell.modifiers();
let current_mods = state.modifiers.unwrap_or(Modifiers::empty());
let removed_mods = current_mods.difference(next_mods);
if !removed_mods.is_empty() {
output.extend_from_slice(b"\x1b[0m");
state.fg = None;
state.bg = None;
state.modifiers = None;
}
if state.fg != Some(next_cell.fg()) {
emit_fg_color(output, next_cell.fg());
state.fg = Some(next_cell.fg());
result.color_changes += 1;
}
if state.bg != Some(next_cell.bg()) {
emit_bg_color(output, next_cell.bg());
state.bg = Some(next_cell.bg());
result.color_changes += 1;
}
if state.modifiers != Some(next_mods) {
emit_modifiers(output, next_mods, state.modifiers);
state.modifiers = Some(next_mods);
result.modifier_changes += 1;
}
emit_grapheme(output, next_cell, next);
let advance = u16::from(next_cell.display_width().max(1));
state.cursor_x += advance;
}
}
}
#[inline]
fn emit_cursor_move(output: &mut Vec<u8>, x: u16, y: u16) {
let row = y + 1;
let col = x + 1;
if row == 1 && col == 1 {
output.extend_from_slice(b"\x1b[H");
} else if col == 1 {
let _ = write!(output, "\x1b[{row}H");
} else {
let _ = write!(output, "\x1b[{row};{col}H");
}
}
#[inline]
fn emit_fg_color(output: &mut Vec<u8>, color: Rgb) {
let _ = write!(output, "\x1b[38;2;{};{};{}m", color.r, color.g, color.b);
}
#[inline]
fn emit_bg_color(output: &mut Vec<u8>, color: Rgb) {
let _ = write!(output, "\x1b[48;2;{};{};{}m", color.r, color.g, color.b);
}
fn emit_modifiers(output: &mut Vec<u8>, new: Modifiers, old: Option<Modifiers>) {
let old = old.unwrap_or(Modifiers::empty());
let removed = old.difference(new);
if removed.is_empty() {
let added = new.difference(old);
emit_modifier_set(output, added);
} else {
output.extend_from_slice(b"\x1b[0m");
emit_modifier_set(output, new);
}
}
fn emit_modifier_set(output: &mut Vec<u8>, modifiers: Modifiers) {
if modifiers.contains(Modifiers::BOLD) {
output.extend_from_slice(b"\x1b[1m");
}
if modifiers.contains(Modifiers::DIM) {
output.extend_from_slice(b"\x1b[2m");
}
if modifiers.contains(Modifiers::ITALIC) {
output.extend_from_slice(b"\x1b[3m");
}
if modifiers.contains(Modifiers::UNDERLINE) {
output.extend_from_slice(b"\x1b[4m");
}
if modifiers.contains(Modifiers::BLINK) {
output.extend_from_slice(b"\x1b[5m");
}
if modifiers.contains(Modifiers::REVERSED) {
output.extend_from_slice(b"\x1b[7m");
}
if modifiers.contains(Modifiers::HIDDEN) {
output.extend_from_slice(b"\x1b[8m");
}
if modifiers.contains(Modifiers::STRIKETHROUGH) {
output.extend_from_slice(b"\x1b[9m");
}
}
#[inline]
fn emit_grapheme(output: &mut Vec<u8>, cell: &Cell, buffer: &Buffer) {
if cell.flags().contains(CellFlags::OVERFLOW) {
if let Some(grapheme) = cell.overflow_index().and_then(|idx| buffer.get_overflow(idx)) {
output.extend_from_slice(grapheme.as_bytes());
return;
}
output.extend_from_slice("�".as_bytes());
} else if let Some(grapheme) = cell.grapheme() {
output.extend_from_slice(grapheme.as_bytes());
} else {
output.push(b' ');
}
}
pub fn render_full_diff(
current: &Buffer,
next: &Buffer,
output: &mut Vec<u8>,
state: &mut DiffState,
) -> DiffResult {
render_diff(current, next, &[], output, state)
}
pub fn render_full(buffer: &Buffer, output: &mut Vec<u8>) {
let width = buffer.width();
let height = buffer.height();
output.extend_from_slice(b"\x1b[?25l");
output.extend_from_slice(b"\x1b[2J");
output.extend_from_slice(b"\x1b[H");
let mut last_fg: Option<Rgb> = None;
let mut last_bg: Option<Rgb> = None;
let mut last_mods: Option<Modifiers> = None;
for y in 0..height {
if y > 0 {
output.extend_from_slice(b"\r\n");
}
for x in 0..width {
let idx = (y as usize) * (width as usize) + (x as usize);
let cell = &buffer.cells()[idx];
if cell.is_wide_continuation() {
continue;
}
if last_fg != Some(cell.fg()) {
emit_fg_color(output, cell.fg());
last_fg = Some(cell.fg());
}
if last_bg != Some(cell.bg()) {
emit_bg_color(output, cell.bg());
last_bg = Some(cell.bg());
}
if last_mods != Some(cell.modifiers()) {
emit_modifiers(output, cell.modifiers(), last_mods);
last_mods = Some(cell.modifiers());
}
emit_grapheme(output, cell, buffer);
}
}
output.extend_from_slice(b"\x1b[0m\x1b[?25h");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diff_identical_buffers() {
let a = Buffer::new(10, 5);
let b = Buffer::new(10, 5);
let mut output = Vec::new();
let mut state = DiffState::new();
let result = render_full_diff(&a, &b, &mut output, &mut state);
assert_eq!(result.cells_changed, 0);
assert!(output.is_empty());
}
#[test]
fn test_diff_single_cell_change() {
let a = Buffer::new(10, 5);
let mut b = Buffer::new(10, 5);
b.set(5, 2, Cell::new('X'));
let mut output = Vec::new();
let mut state = DiffState::new();
let result = render_full_diff(&a, &b, &mut output, &mut state);
assert_eq!(result.cells_changed, 1);
assert!(!output.is_empty());
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains('X'));
}
#[test]
fn test_diff_adjacent_cells_no_cursor_move() {
let a = Buffer::new(10, 5);
let mut b = Buffer::new(10, 5);
b.set(0, 0, Cell::new('A'));
b.set(1, 0, Cell::new('B'));
b.set(2, 0, Cell::new('C'));
let mut output = Vec::new();
let mut state = DiffState::new();
let result = render_full_diff(&a, &b, &mut output, &mut state);
assert_eq!(result.cells_changed, 3);
assert_eq!(result.cursor_moves, 0);
}
#[test]
fn test_diff_color_tracking() {
let a = Buffer::new(10, 5);
let mut b = Buffer::new(10, 5);
let red = Rgb::new(255, 0, 0);
b.set(0, 0, Cell::new('A').with_fg(red));
b.set(1, 0, Cell::new('B').with_fg(red));
let mut output = Vec::new();
let mut state = DiffState::new();
let result = render_full_diff(&a, &b, &mut output, &mut state);
assert_eq!(result.color_changes, 2);
}
#[test]
fn test_diff_dirty_rect() {
let a = Buffer::new(20, 10);
let mut b = Buffer::new(20, 10);
b.set(0, 0, Cell::new('X'));
b.set(10, 5, Cell::new('Y'));
let mut output = Vec::new();
let mut state = DiffState::new();
let dirty = vec![Rect::new(8, 4, 5, 3)];
let result = render_diff(&a, &b, &dirty, &mut output, &mut state);
assert_eq!(result.cells_changed, 1);
}
#[test]
fn test_cursor_move_optimization() {
let mut output = Vec::new();
emit_cursor_move(&mut output, 0, 0);
assert_eq!(&output, b"\x1b[H");
output.clear();
emit_cursor_move(&mut output, 0, 5);
assert_eq!(&output, b"\x1b[6H");
output.clear();
emit_cursor_move(&mut output, 10, 5);
assert_eq!(&output, b"\x1b[6;11H"); }
#[test]
fn test_render_full() {
let mut buffer = Buffer::new(3, 2);
buffer.set(0, 0, Cell::new('A'));
buffer.set(1, 0, Cell::new('B'));
buffer.set(2, 0, Cell::new('C'));
let mut output = Vec::new();
render_full(&buffer, &mut output);
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.starts_with("\x1b[?25l\x1b[2J\x1b[H"));
assert!(output_str.contains('A'));
assert!(output_str.contains('B'));
assert!(output_str.contains('C'));
assert!(output_str.ends_with("\x1b[0m\x1b[?25h"));
}
}