1use super::{Buffer, Cell, CellFlags, Modifiers, Rgb};
12use crate::layout::Rect;
13use std::io::Write;
14
15#[derive(Debug, Clone)]
20pub struct DiffState {
21 cursor_x: u16,
23 cursor_y: u16,
25 fg: Option<Rgb>,
27 bg: Option<Rgb>,
29 modifiers: Option<Modifiers>,
31}
32
33impl Default for DiffState {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl DiffState {
40 pub const fn new() -> Self {
42 Self {
43 cursor_x: 0,
44 cursor_y: 0,
45 fg: None,
46 bg: None,
47 modifiers: None,
48 }
49 }
50
51 pub const fn reset(&mut self) {
53 self.fg = None;
54 self.bg = None;
55 self.modifiers = None;
56 self.cursor_x = u16::MAX;
58 self.cursor_y = u16::MAX;
59 }
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct DiffResult {
65 pub cells_changed: usize,
67 pub cursor_moves: usize,
69 pub color_changes: usize,
71 pub modifier_changes: usize,
73}
74
75pub fn render_diff(
99 current: &Buffer,
100 next: &Buffer,
101 dirty_rects: &[Rect],
102 output: &mut Vec<u8>,
103 state: &mut DiffState,
104) -> DiffResult {
105 debug_assert_eq!(current.width(), next.width());
106 debug_assert_eq!(current.height(), next.height());
107
108 let mut result = DiffResult::default();
109 let width = current.width();
110 let height = current.height();
111
112 let full_rect = Rect::from_size(width, height);
114 let rects: &[Rect] = if dirty_rects.is_empty() {
115 std::slice::from_ref(&full_rect)
116 } else {
117 dirty_rects
118 };
119
120 for rect in rects {
121 diff_rect(current, next, *rect, output, state, &mut result);
122 }
123
124 result
125}
126
127fn diff_rect(
129 current: &Buffer,
130 next: &Buffer,
131 rect: Rect,
132 output: &mut Vec<u8>,
133 state: &mut DiffState,
134 result: &mut DiffResult,
135) {
136 let width = current.width();
137
138 let x_end = (rect.x + rect.width).min(width);
140 let y_end = (rect.y + rect.height).min(current.height());
141
142 for y in rect.y..y_end {
143 for x in rect.x..x_end {
144 let idx = (y as usize) * (width as usize) + (x as usize);
145 let current_cell = ¤t.cells()[idx];
146 let next_cell = &next.cells()[idx];
147
148 if current_cell == next_cell {
150 continue;
151 }
152
153 if next_cell.is_wide_continuation() {
155 continue;
156 }
157
158 result.cells_changed += 1;
159
160 if state.cursor_y != y || state.cursor_x != x {
162 emit_cursor_move(output, x, y);
163 state.cursor_x = x;
164 state.cursor_y = y;
165 result.cursor_moves += 1;
166 }
167
168 let next_mods = next_cell.modifiers();
172 let current_mods = state.modifiers.unwrap_or(Modifiers::empty());
173 let removed_mods = current_mods.difference(next_mods);
174
175 if !removed_mods.is_empty() {
176 output.extend_from_slice(b"\x1b[0m");
177 state.fg = None;
178 state.bg = None;
179 state.modifiers = None;
180 }
181
182 if state.fg != Some(next_cell.fg()) {
184 emit_fg_color(output, next_cell.fg());
185 state.fg = Some(next_cell.fg());
186 result.color_changes += 1;
187 }
188
189 if state.bg != Some(next_cell.bg()) {
190 emit_bg_color(output, next_cell.bg());
191 state.bg = Some(next_cell.bg());
192 result.color_changes += 1;
193 }
194
195 if state.modifiers != Some(next_mods) {
197 emit_modifiers(output, next_mods, state.modifiers);
200 state.modifiers = Some(next_mods);
201 result.modifier_changes += 1;
202 }
203
204 emit_grapheme(output, next_cell, next);
206
207 let advance = u16::from(next_cell.display_width().max(1));
209 state.cursor_x += advance;
210 }
211 }
212}
213
214#[inline]
220fn emit_cursor_move(output: &mut Vec<u8>, x: u16, y: u16) {
221 let row = y + 1;
223 let col = x + 1;
224
225 if row == 1 && col == 1 {
226 output.extend_from_slice(b"\x1b[H");
227 } else if col == 1 {
228 let _ = write!(output, "\x1b[{row}H");
230 } else {
231 let _ = write!(output, "\x1b[{row};{col}H");
232 }
233}
234
235#[inline]
237fn emit_fg_color(output: &mut Vec<u8>, color: Rgb) {
238 let _ = write!(output, "\x1b[38;2;{};{};{}m", color.r, color.g, color.b);
239}
240
241#[inline]
243fn emit_bg_color(output: &mut Vec<u8>, color: Rgb) {
244 let _ = write!(output, "\x1b[48;2;{};{};{}m", color.r, color.g, color.b);
245}
246
247fn emit_modifiers(output: &mut Vec<u8>, new: Modifiers, old: Option<Modifiers>) {
252 let old = old.unwrap_or(Modifiers::empty());
253
254 let removed = old.difference(new);
256 if removed.is_empty() {
257 let added = new.difference(old);
259 emit_modifier_set(output, added);
260 } else {
261 output.extend_from_slice(b"\x1b[0m");
263 emit_modifier_set(output, new);
266 }
267}
268
269fn emit_modifier_set(output: &mut Vec<u8>, modifiers: Modifiers) {
271 if modifiers.contains(Modifiers::BOLD) {
272 output.extend_from_slice(b"\x1b[1m");
273 }
274 if modifiers.contains(Modifiers::DIM) {
275 output.extend_from_slice(b"\x1b[2m");
276 }
277 if modifiers.contains(Modifiers::ITALIC) {
278 output.extend_from_slice(b"\x1b[3m");
279 }
280 if modifiers.contains(Modifiers::UNDERLINE) {
281 output.extend_from_slice(b"\x1b[4m");
282 }
283 if modifiers.contains(Modifiers::BLINK) {
284 output.extend_from_slice(b"\x1b[5m");
285 }
286 if modifiers.contains(Modifiers::REVERSED) {
287 output.extend_from_slice(b"\x1b[7m");
288 }
289 if modifiers.contains(Modifiers::HIDDEN) {
290 output.extend_from_slice(b"\x1b[8m");
291 }
292 if modifiers.contains(Modifiers::STRIKETHROUGH) {
293 output.extend_from_slice(b"\x1b[9m");
294 }
295}
296
297#[inline]
299fn emit_grapheme(output: &mut Vec<u8>, cell: &Cell, buffer: &Buffer) {
300 if cell.flags().contains(CellFlags::OVERFLOW) {
301 if let Some(grapheme) = cell.overflow_index().and_then(|idx| buffer.get_overflow(idx)) {
303 output.extend_from_slice(grapheme.as_bytes());
304 return;
305 }
306 output.extend_from_slice("�".as_bytes());
308 } else if let Some(grapheme) = cell.grapheme() {
309 output.extend_from_slice(grapheme.as_bytes());
310 } else {
311 output.push(b' ');
313 }
314}
315
316pub fn render_full_diff(
320 current: &Buffer,
321 next: &Buffer,
322 output: &mut Vec<u8>,
323 state: &mut DiffState,
324) -> DiffResult {
325 render_diff(current, next, &[], output, state)
326}
327
328pub fn render_full(buffer: &Buffer, output: &mut Vec<u8>) {
332 let width = buffer.width();
333 let height = buffer.height();
334
335 output.extend_from_slice(b"\x1b[?25l");
337
338 output.extend_from_slice(b"\x1b[H");
340
341 let mut last_fg: Option<Rgb> = None;
342 let mut last_bg: Option<Rgb> = None;
343 let mut last_mods: Option<Modifiers> = None;
344
345 for y in 0..height {
346 if y > 0 {
347 output.extend_from_slice(b"\r\n");
349 }
350
351 for x in 0..width {
352 let idx = (y as usize) * (width as usize) + (x as usize);
353 let cell = &buffer.cells()[idx];
354
355 if cell.is_wide_continuation() {
357 continue;
358 }
359
360 if last_fg != Some(cell.fg()) {
362 emit_fg_color(output, cell.fg());
363 last_fg = Some(cell.fg());
364 }
365 if last_bg != Some(cell.bg()) {
366 emit_bg_color(output, cell.bg());
367 last_bg = Some(cell.bg());
368 }
369 if last_mods != Some(cell.modifiers()) {
370 emit_modifiers(output, cell.modifiers(), last_mods);
371 last_mods = Some(cell.modifiers());
372 }
373
374 emit_grapheme(output, cell, buffer);
375 }
376 }
377
378 output.extend_from_slice(b"\x1b[0m\x1b[?25h");
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_diff_identical_buffers() {
388 let a = Buffer::new(10, 5);
389 let b = Buffer::new(10, 5);
390 let mut output = Vec::new();
391 let mut state = DiffState::new();
392
393 let result = render_full_diff(&a, &b, &mut output, &mut state);
394
395 assert_eq!(result.cells_changed, 0);
396 assert!(output.is_empty());
397 }
398
399 #[test]
400 fn test_diff_single_cell_change() {
401 let a = Buffer::new(10, 5);
402 let mut b = Buffer::new(10, 5);
403
404 b.set(5, 2, Cell::new('X'));
405
406 let mut output = Vec::new();
407 let mut state = DiffState::new();
408
409 let result = render_full_diff(&a, &b, &mut output, &mut state);
410
411 assert_eq!(result.cells_changed, 1);
412 assert!(!output.is_empty());
413 let output_str = String::from_utf8_lossy(&output);
415 assert!(output_str.contains('X'));
416 }
417
418 #[test]
419 fn test_diff_adjacent_cells_no_cursor_move() {
420 let a = Buffer::new(10, 5);
421 let mut b = Buffer::new(10, 5);
422
423 b.set(0, 0, Cell::new('A'));
425 b.set(1, 0, Cell::new('B'));
426 b.set(2, 0, Cell::new('C'));
427
428 let mut output = Vec::new();
429 let mut state = DiffState::new();
430
431 let result = render_full_diff(&a, &b, &mut output, &mut state);
432
433 assert_eq!(result.cells_changed, 3);
434 assert_eq!(result.cursor_moves, 0);
436 }
437
438 #[test]
439 fn test_diff_color_tracking() {
440 let a = Buffer::new(10, 5);
441 let mut b = Buffer::new(10, 5);
442
443 let red = Rgb::new(255, 0, 0);
444 b.set(0, 0, Cell::new('A').with_fg(red));
446 b.set(1, 0, Cell::new('B').with_fg(red)); let mut output = Vec::new();
449 let mut state = DiffState::new();
450
451 let result = render_full_diff(&a, &b, &mut output, &mut state);
452
453 assert_eq!(result.color_changes, 2);
455 }
456
457 #[test]
458 fn test_diff_dirty_rect() {
459 let a = Buffer::new(20, 10);
460 let mut b = Buffer::new(20, 10);
461
462 b.set(0, 0, Cell::new('X'));
464
465 b.set(10, 5, Cell::new('Y'));
467
468 let mut output = Vec::new();
469 let mut state = DiffState::new();
470
471 let dirty = vec![Rect::new(8, 4, 5, 3)];
473 let result = render_diff(&a, &b, &dirty, &mut output, &mut state);
474
475 assert_eq!(result.cells_changed, 1);
477 }
478
479 #[test]
480 fn test_cursor_move_optimization() {
481 let mut output = Vec::new();
482
483 emit_cursor_move(&mut output, 0, 0);
485 assert_eq!(&output, b"\x1b[H");
486
487 output.clear();
488
489 emit_cursor_move(&mut output, 0, 5);
491 assert_eq!(&output, b"\x1b[6H"); output.clear();
494
495 emit_cursor_move(&mut output, 10, 5);
497 assert_eq!(&output, b"\x1b[6;11H"); }
499
500 #[test]
501 fn test_render_full() {
502 let mut buffer = Buffer::new(3, 2);
503 buffer.set(0, 0, Cell::new('A'));
504 buffer.set(1, 0, Cell::new('B'));
505 buffer.set(2, 0, Cell::new('C'));
506
507 let mut output = Vec::new();
508 render_full(&buffer, &mut output);
509
510 let output_str = String::from_utf8_lossy(&output);
511 assert!(output_str.starts_with("\x1b[?25l\x1b[H"));
513 assert!(output_str.contains('A'));
515 assert!(output_str.contains('B'));
516 assert!(output_str.contains('C'));
517 assert!(output_str.ends_with("\x1b[0m\x1b[?25h"));
519 }
520}