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[2J");
340
341 output.extend_from_slice(b"\x1b[H");
343
344 let mut last_fg: Option<Rgb> = None;
345 let mut last_bg: Option<Rgb> = None;
346 let mut last_mods: Option<Modifiers> = None;
347
348 for y in 0..height {
349 if y > 0 {
350 output.extend_from_slice(b"\r\n");
352 }
353
354 for x in 0..width {
355 let idx = (y as usize) * (width as usize) + (x as usize);
356 let cell = &buffer.cells()[idx];
357
358 if cell.is_wide_continuation() {
360 continue;
361 }
362
363 if last_fg != Some(cell.fg()) {
365 emit_fg_color(output, cell.fg());
366 last_fg = Some(cell.fg());
367 }
368 if last_bg != Some(cell.bg()) {
369 emit_bg_color(output, cell.bg());
370 last_bg = Some(cell.bg());
371 }
372 if last_mods != Some(cell.modifiers()) {
373 emit_modifiers(output, cell.modifiers(), last_mods);
374 last_mods = Some(cell.modifiers());
375 }
376
377 emit_grapheme(output, cell, buffer);
378 }
379 }
380
381 output.extend_from_slice(b"\x1b[0m\x1b[?25h");
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_diff_identical_buffers() {
391 let a = Buffer::new(10, 5);
392 let b = Buffer::new(10, 5);
393 let mut output = Vec::new();
394 let mut state = DiffState::new();
395
396 let result = render_full_diff(&a, &b, &mut output, &mut state);
397
398 assert_eq!(result.cells_changed, 0);
399 assert!(output.is_empty());
400 }
401
402 #[test]
403 fn test_diff_single_cell_change() {
404 let a = Buffer::new(10, 5);
405 let mut b = Buffer::new(10, 5);
406
407 b.set(5, 2, Cell::new('X'));
408
409 let mut output = Vec::new();
410 let mut state = DiffState::new();
411
412 let result = render_full_diff(&a, &b, &mut output, &mut state);
413
414 assert_eq!(result.cells_changed, 1);
415 assert!(!output.is_empty());
416 let output_str = String::from_utf8_lossy(&output);
418 assert!(output_str.contains('X'));
419 }
420
421 #[test]
422 fn test_diff_adjacent_cells_no_cursor_move() {
423 let a = Buffer::new(10, 5);
424 let mut b = Buffer::new(10, 5);
425
426 b.set(0, 0, Cell::new('A'));
428 b.set(1, 0, Cell::new('B'));
429 b.set(2, 0, Cell::new('C'));
430
431 let mut output = Vec::new();
432 let mut state = DiffState::new();
433
434 let result = render_full_diff(&a, &b, &mut output, &mut state);
435
436 assert_eq!(result.cells_changed, 3);
437 assert_eq!(result.cursor_moves, 0);
439 }
440
441 #[test]
442 fn test_diff_color_tracking() {
443 let a = Buffer::new(10, 5);
444 let mut b = Buffer::new(10, 5);
445
446 let red = Rgb::new(255, 0, 0);
447 b.set(0, 0, Cell::new('A').with_fg(red));
449 b.set(1, 0, Cell::new('B').with_fg(red)); let mut output = Vec::new();
452 let mut state = DiffState::new();
453
454 let result = render_full_diff(&a, &b, &mut output, &mut state);
455
456 assert_eq!(result.color_changes, 2);
458 }
459
460 #[test]
461 fn test_diff_dirty_rect() {
462 let a = Buffer::new(20, 10);
463 let mut b = Buffer::new(20, 10);
464
465 b.set(0, 0, Cell::new('X'));
467
468 b.set(10, 5, Cell::new('Y'));
470
471 let mut output = Vec::new();
472 let mut state = DiffState::new();
473
474 let dirty = vec![Rect::new(8, 4, 5, 3)];
476 let result = render_diff(&a, &b, &dirty, &mut output, &mut state);
477
478 assert_eq!(result.cells_changed, 1);
480 }
481
482 #[test]
483 fn test_cursor_move_optimization() {
484 let mut output = Vec::new();
485
486 emit_cursor_move(&mut output, 0, 0);
488 assert_eq!(&output, b"\x1b[H");
489
490 output.clear();
491
492 emit_cursor_move(&mut output, 0, 5);
494 assert_eq!(&output, b"\x1b[6H"); output.clear();
497
498 emit_cursor_move(&mut output, 10, 5);
500 assert_eq!(&output, b"\x1b[6;11H"); }
502
503 #[test]
504 fn test_render_full() {
505 let mut buffer = Buffer::new(3, 2);
506 buffer.set(0, 0, Cell::new('A'));
507 buffer.set(1, 0, Cell::new('B'));
508 buffer.set(2, 0, Cell::new('C'));
509
510 let mut output = Vec::new();
511 render_full(&buffer, &mut output);
512
513 let output_str = String::from_utf8_lossy(&output);
514 assert!(output_str.starts_with("\x1b[?25l\x1b[2J\x1b[H"));
516 assert!(output_str.contains('A'));
518 assert!(output_str.contains('B'));
519 assert!(output_str.contains('C'));
520 assert!(output_str.ends_with("\x1b[0m\x1b[?25h"));
522 }
523}