1use crate::style::Style;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum CellWidth {
15 Continuation,
19 #[default]
21 Single,
22 Wide,
26}
27
28impl CellWidth {
29 pub const fn columns(self) -> usize {
32 match self {
33 CellWidth::Continuation => 0,
34 CellWidth::Single => 1,
35 CellWidth::Wide => 2,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Cell {
50 pub ch: char,
51 pub style: Style,
52 pub width: CellWidth,
53}
54
55impl Default for Cell {
56 fn default() -> Self {
57 Cell {
58 ch: ' ',
59 style: Style::default(),
60 width: CellWidth::Single,
61 }
62 }
63}
64
65impl Cell {
66 pub const fn new(ch: char, style: Style) -> Self {
68 Cell {
69 ch,
70 style,
71 width: CellWidth::Single,
72 }
73 }
74
75 pub const fn wide(ch: char, style: Style) -> Self {
78 Cell {
79 ch,
80 style,
81 width: CellWidth::Wide,
82 }
83 }
84
85 pub const fn continuation(style: Style) -> Self {
89 Cell {
90 ch: ' ',
91 style,
92 width: CellWidth::Continuation,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub struct Cursor {
101 pub row: usize,
102 pub col: usize,
103}
104
105impl Cursor {
106 pub const fn new(row: usize, col: usize) -> Self {
107 Cursor { row, col }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct Screen {
114 rows: usize,
115 cols: usize,
116 cells: Vec<Cell>,
124 origin: usize,
126 cursor: Cursor,
129}
130
131impl PartialEq for Screen {
136 fn eq(&self, other: &Self) -> bool {
137 if self.rows != other.rows || self.cols != other.cols || self.cursor != other.cursor {
138 return false;
139 }
140 if self.origin == other.origin {
141 return self.cells == other.cells;
142 }
143 (0..self.rows).all(|r| {
144 let (a, b) = (self.row_slice(r), other.row_slice(r));
145 a == b
146 })
147 }
148}
149
150impl Eq for Screen {}
151
152impl Screen {
153 pub fn new(rows: usize, cols: usize) -> Self {
155 Screen {
156 rows,
157 cols,
158 cells: vec![Cell::default(); rows * cols],
159 origin: 0,
160 cursor: Cursor::default(),
161 }
162 }
163
164 pub fn rows(&self) -> usize {
165 self.rows
166 }
167
168 pub fn cols(&self) -> usize {
169 self.cols
170 }
171
172 #[inline]
174 pub fn cursor(&self) -> Cursor {
175 self.cursor
176 }
177
178 #[inline]
182 pub fn set_cursor(&mut self, cursor: Cursor) {
183 self.cursor = Cursor {
184 row: cursor.row.min(self.rows.saturating_sub(1)),
185 col: cursor.col.min(self.cols.saturating_sub(1)),
186 };
187 }
188
189 pub fn clear(&mut self) {
193 self.cells.fill(Cell::default());
194 self.origin = 0;
195 self.cursor = Cursor::default();
196 }
197
198 #[inline]
201 fn index(&self, row: usize, col: usize) -> usize {
202 debug_assert_eq!(self.cells.len(), self.rows * self.cols);
203 debug_assert!(row < self.rows && col < self.cols);
204 self.physical(row) * self.cols + col
205 }
206
207 #[inline]
209 fn physical(&self, row: usize) -> usize {
210 debug_assert!(self.rows > 0);
211 let r = self.origin + row;
212 if r >= self.rows {
213 r - self.rows
214 } else {
215 r
216 }
217 }
218
219 #[inline]
221 fn row_slice(&self, row: usize) -> &[Cell] {
222 let start = self.physical(row) * self.cols;
223 &self.cells[start..start + self.cols]
224 }
225
226 pub fn cell(&self, row: usize, col: usize) -> Cell {
228 if row < self.rows && col < self.cols {
229 self.cells[self.index(row, col)]
230 } else {
231 Cell::default()
232 }
233 }
234
235 pub fn set(&mut self, row: usize, col: usize, cell: Cell) {
237 if row < self.rows && col < self.cols {
238 let i = self.index(row, col);
239 self.cells[i] = cell;
240 }
241 }
242
243 pub fn write_str(&mut self, row: usize, col: usize, text: &str, style: Style) {
248 for (i, ch) in text.chars().enumerate() {
249 self.set(row, col + i, Cell::new(ch, style));
250 }
251 }
252
253 pub fn copy_cells(&mut self, row: usize, col: usize, cells: &[Cell]) {
259 if row >= self.rows || col >= self.cols {
260 return;
261 }
262 let n = cells.len().min(self.cols - col);
263 let base = self.index(row, col);
264 self.cells[base..base + n].copy_from_slice(&cells[..n]);
265 }
266
267 pub fn scroll_rows_up(&mut self, top: usize, bottom: usize, n: usize, fill: Cell) {
281 if top > bottom || bottom >= self.rows || n == 0 {
282 return;
283 }
284 let n = n.min(bottom - top + 1);
285 if top == 0 && bottom == self.rows - 1 {
286 self.origin = self.physical(n);
287 for r in self.rows - n..self.rows {
288 self.fill_row(r, fill);
289 }
290 return;
291 }
292 if n <= bottom - top {
294 for r in top..=bottom - n {
295 self.copy_row(r + n, r);
296 }
297 }
298 for r in bottom + 1 - n..=bottom {
299 self.fill_row(r, fill);
300 }
301 }
302
303 #[inline]
305 fn copy_row(&mut self, from: usize, to: usize) {
306 let (src, dst) = (self.index(from, 0), self.index(to, 0));
307 self.cells.copy_within(src..src + self.cols, dst);
308 }
309
310 #[inline]
312 fn fill_row(&mut self, row: usize, fill: Cell) {
313 let start = self.index(row, 0);
314 self.cells[start..start + self.cols].fill(fill);
315 }
316
317 pub fn scroll_rows_down(&mut self, top: usize, bottom: usize, n: usize, fill: Cell) {
321 if top > bottom || bottom >= self.rows || n == 0 {
322 return;
323 }
324 let n = n.min(bottom - top + 1);
325 if top == 0 && bottom == self.rows - 1 {
326 self.origin = self.physical(self.rows - n);
327 for r in 0..n {
328 self.fill_row(r, fill);
329 }
330 return;
331 }
332 for r in (top + n..=bottom).rev() {
333 self.copy_row(r - n, r);
334 }
335 for r in top..top + n {
336 self.fill_row(r, fill);
337 }
338 }
339
340 pub fn diff(&self, next: &Screen) -> Vec<u8> {
348 if self.rows != next.rows || self.cols != next.cols {
349 return next.render_full();
350 }
351 if self == next {
352 return Vec::new();
353 }
354 let mut out = String::new();
355 emit_changes(Some(self), next, &mut out);
356 out.into_bytes()
357 }
358
359 pub fn render_full(&self) -> Vec<u8> {
363 let mut out = String::from("\x1b[2J");
364 emit_changes(None, self, &mut out);
365 out.into_bytes()
366 }
367}
368
369fn emit_changes(prev: Option<&Screen>, next: &Screen, out: &mut String) {
377 use std::fmt::Write;
378 let mut style = Style::default();
379 let mut at: Option<(usize, usize)> = None;
383 for row in 0..next.rows {
384 let last_content = (0..next.cols)
387 .rev()
388 .find(|&c| next.cell(row, c) != Cell::default())
389 .map_or(0, |c| c + 1);
390
391 for col in 0..last_content {
393 let target = next.cell(row, col);
394 if target.width == CellWidth::Continuation {
399 continue;
400 }
401 let same = match prev {
402 Some(p) => p.cell(row, col) == target,
403 None => target == Cell::default(),
404 };
405 if same {
406 continue;
407 }
408 if at != Some((row, col)) {
409 write!(out, "\x1b[{};{}H", row + 1, col + 1).unwrap();
410 }
411 out.push_str(&style.transition_to(&target.style));
412 style = target.style;
413 out.push(target.ch);
414 let advance = target.width.columns();
421 at = if col + advance < next.cols {
422 Some((row, col + advance))
423 } else {
424 None
425 };
426 }
427
428 if last_content < next.cols {
433 let needs_erase = match prev {
434 Some(p) => (last_content..next.cols).any(|c| p.cell(row, c) != Cell::default()),
435 None => false,
436 };
437 if needs_erase {
438 out.push_str(&style.transition_to(&Style::default()));
441 style = Style::default();
442 if at != Some((row, last_content)) {
443 write!(out, "\x1b[{};{}H", row + 1, last_content + 1).unwrap();
444 }
445 out.push_str("\x1b[K");
446 at = None;
449 }
450 }
451 }
452 out.push_str(&style.transition_to(&Style::default()));
453 let Cursor { row, col } = next.cursor;
454 write!(out, "\x1b[{};{}H", row + 1, col + 1).unwrap();
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::style::Style;
461
462 fn numbered(rows: usize, cols: usize) -> Screen {
464 let mut s = Screen::new(rows, cols);
465 for r in 0..rows {
466 for c in 0..cols {
467 let ch = char::from_u32(0x4E00 + (r * 64 + c) as u32).unwrap();
468 s.set(r, c, Cell::new(ch, Style::default()));
469 }
470 }
471 s
472 }
473
474 fn reference_up(s: &mut Screen, top: usize, bottom: usize, n: usize, fill: Cell) {
476 let n = n.min(bottom - top + 1);
477 for r in top..=bottom {
478 for c in 0..s.cols() {
479 let cell = if r + n <= bottom {
480 s.cell(r + n, c)
481 } else {
482 fill
483 };
484 s.set(r, c, cell);
485 }
486 }
487 }
488
489 fn reference_down(s: &mut Screen, top: usize, bottom: usize, n: usize, fill: Cell) {
490 let n = n.min(bottom - top + 1);
491 for r in (top..=bottom).rev() {
492 for c in 0..s.cols() {
493 let cell = if r >= top + n { s.cell(r - n, c) } else { fill };
494 s.set(r, c, cell);
495 }
496 }
497 }
498
499 #[test]
500 fn region_scrolls_match_the_per_cell_reference_everywhere() {
501 let fill = Cell::new(' ', Style::default());
502 for (rows, cols) in [(1, 1), (5, 3), (8, 7)] {
503 for top in 0..rows {
504 for bottom in top..rows {
505 for n in 0..=(bottom - top + 2) {
506 let (mut fast, mut slow) = (numbered(rows, cols), numbered(rows, cols));
507 fast.scroll_rows_up(top, bottom, n, fill);
508 if n > 0 {
509 reference_up(&mut slow, top, bottom, n, fill);
510 }
511 assert_eq!(fast, slow, "up {rows}x{cols} {top}..={bottom} by {n}");
512
513 let (mut fast, mut slow) = (numbered(rows, cols), numbered(rows, cols));
514 fast.scroll_rows_down(top, bottom, n, fill);
515 if n > 0 {
516 reference_down(&mut slow, top, bottom, n, fill);
517 }
518 assert_eq!(fast, slow, "down {rows}x{cols} {top}..={bottom} by {n}");
519 }
520 }
521 }
522 }
523 }
524
525 #[test]
528 fn the_row_ring_is_invisible_to_readers() {
529 let fill = Cell::new(' ', Style::default());
530 let mut s = numbered(4, 3);
531 for _ in 0..7 {
533 s.scroll_rows_up(0, 3, 1, fill);
534 }
535 let blank = Screen::new(4, 3);
536 assert_eq!(s, blank, "everything scrolled off: a blank screen");
537
538 let mut s = numbered(4, 3);
539 s.scroll_rows_up(0, 3, 2, fill);
540 let mut want = Screen::new(4, 3);
541 for r in 0..2 {
542 for c in 0..3 {
543 want.set(r, c, numbered(4, 3).cell(r + 2, c));
544 }
545 }
546 assert_eq!(s, want, "two rows up, two blank rows in");
547 assert_eq!(s.render_full(), want.render_full(), "same bytes");
548
549 s.set(0, 0, Cell::new('A', Style::default()));
551 assert_eq!(s.cell(0, 0).ch, 'A');
552 s.clear();
553 assert_eq!(s, Screen::new(4, 3));
554 }
555
556 #[test]
557 fn an_out_of_range_region_scroll_changes_nothing() {
558 let fill = Cell::new('x', Style::default());
559 let mut s = numbered(4, 4);
560 let before = s.clone();
561 s.scroll_rows_up(3, 1, 1, fill);
562 s.scroll_rows_up(0, 4, 1, fill);
563 s.scroll_rows_down(2, 9, 1, fill);
564 assert_eq!(s, before);
565 }
566
567 fn filled_row(rows: usize, cols: usize, row: usize, ch: char) -> Screen {
570 let mut s = Screen::new(rows, cols);
571 for c in 0..cols {
572 s.set(row, c, Cell::new(ch, Style::default()));
573 }
574 s
575 }
576
577 fn contains_el(bytes: &[u8]) -> bool {
578 let s = std::str::from_utf8(bytes).unwrap_or("");
579 s.contains("\x1b[K")
580 }
581
582 #[test]
585 fn clear_resets_all_cells_and_cursor_without_realloc() {
586 let mut s = Screen::new(4, 6);
587 let dirty_style = Style {
589 bold: true,
590 ..Style::default()
591 };
592 for r in 0..4 {
593 for c in 0..6 {
594 s.set(r, c, Cell::new('X', dirty_style));
595 }
596 }
597 s.set_cursor(Cursor::new(3, 5));
599
600 s.clear();
601
602 assert_eq!(s.rows(), 4);
604 assert_eq!(s.cols(), 6);
605 assert_eq!(s.cursor(), Cursor::new(0, 0));
607 let blank = Cell::default();
609 for r in 0..4 {
610 for c in 0..6 {
611 assert_eq!(
612 s.cell(r, c),
613 blank,
614 "cell ({r},{c}) was not reset by clear()"
615 );
616 }
617 }
618 }
619
620 #[test]
623 fn diff_emits_el_when_row_tail_clears_to_blank() {
624 let prev = filled_row(2, 8, 0, 'X');
627 let mut next = Screen::new(2, 8);
628 for c in 0..3 {
629 next.set(0, c, Cell::new('X', Style::default()));
630 }
631 let diff = prev.diff(&next);
632 assert!(
633 contains_el(&diff),
634 "expected \\x1b[K in diff when tail blanks out"
635 );
636 }
637
638 #[test]
639 fn diff_no_el_when_tail_was_already_blank_in_prev() {
640 let mut prev = Screen::new(2, 8);
643 let mut next = Screen::new(2, 8);
644 for c in 0..3 {
645 prev.set(0, c, Cell::new('A', Style::default()));
646 next.set(0, c, Cell::new('B', Style::default()));
647 }
648 let diff = prev.diff(&next);
649 assert!(
650 !contains_el(&diff),
651 "unexpected \\x1b[K when tail was already blank"
652 );
653 }
654
655 #[test]
656 fn render_full_never_emits_el() {
657 let s = filled_row(2, 8, 0, 'X');
661 let bytes = s.render_full();
662 assert!(!contains_el(&bytes), "render_full must not emit \\x1b[K");
663 }
664
665 #[test]
666 fn diff_el_visual_correctness_via_round_trip() {
667 let prev = filled_row(1, 6, 0, 'Z');
671 let mut next = Screen::new(1, 6);
672 next.set(0, 0, Cell::new('A', Style::default()));
673 assert!(contains_el(&prev.diff(&next)));
675 let empty = next.diff(&next);
678 assert!(empty.is_empty(), "diff of identical screens must be empty");
679 }
680
681 #[test]
682 fn diff_entire_row_blank_in_next_uses_el() {
683 let prev = filled_row(3, 5, 1, 'Y');
685 let next = Screen::new(3, 5);
686 let diff = prev.diff(&next);
687 assert!(
688 contains_el(&diff),
689 "entire-row blank transition should use EL"
690 );
691 }
692
693 #[test]
696 fn cell_constructors_set_width() {
697 let st = Style::default();
698 assert_eq!(Cell::default().width, CellWidth::Single);
699 assert_eq!(Cell::new('a', st).width, CellWidth::Single);
700 assert_eq!(Cell::wide('世', st).width, CellWidth::Wide);
701 let cont = Cell::continuation(st);
702 assert_eq!(cont.width, CellWidth::Continuation);
703 let cols: Vec<usize> = [CellWidth::Continuation, CellWidth::Single, CellWidth::Wide]
704 .iter()
705 .map(|w| w.columns())
706 .collect();
707 assert_eq!(cols, [0, 1, 2]);
708 assert_eq!(cont.ch, ' ');
709 }
710
711 #[test]
712 fn wide_lead_emits_glyph_once_no_continuation_char() {
713 let prev = Screen::new(1, 4);
717 let mut next = Screen::new(1, 4);
718 next.set(0, 0, Cell::wide('世', Style::default()));
719 next.set(0, 1, Cell::continuation(Style::default()));
720 let diff = prev.diff(&next);
721 let s = std::str::from_utf8(&diff).unwrap();
722 assert_eq!(s.matches('世').count(), 1, "glyph must appear exactly once");
723 assert!(
724 !s.contains(' '),
725 "continuation column must emit no space: {s:?}"
726 );
727 assert_eq!(diff, "\x1b[1;1H世\x1b[1;1H".as_bytes());
729 }
730
731 #[test]
732 fn wide_lead_advances_cursor_by_two() {
733 let prev = Screen::new(1, 6);
738 let mut next = Screen::new(1, 6);
739 next.set(0, 0, Cell::wide('世', Style::default()));
740 next.set(0, 1, Cell::continuation(Style::default()));
741 next.set(0, 2, Cell::new('X', Style::default()));
742 let diff = prev.diff(&next);
743 assert_eq!(diff, "\x1b[1;1H世X\x1b[1;1H".as_bytes());
744 }
745
746 #[test]
747 fn ascii_diff_is_byte_identical_regression() {
748 let mut old = Screen::new(2, 10);
750 old.write_str(0, 0, "hello", Style::default());
751 let mut new = old.clone();
752 new.set(0, 1, Cell::new('a', Style::default()));
753 assert_eq!(old.diff(&new), b"\x1b[1;2Ha\x1b[1;1H");
754 }
755
756 #[test]
757 fn set_cursor_clamps_onto_the_grid() {
758 let mut s = Screen::new(4, 6);
759 s.set_cursor(Cursor::new(2, 3));
760 assert_eq!(s.cursor(), Cursor::new(2, 3));
761 s.set_cursor(Cursor::new(99, 99));
762 assert_eq!(s.cursor(), Cursor::new(3, 5));
763 let mut empty = Screen::new(0, 0);
765 empty.set_cursor(Cursor::new(5, 5));
766 assert_eq!(empty.cursor(), Cursor::new(0, 0));
767 }
768
769 #[test]
770 fn copy_cells_matches_repeated_set() {
771 let st = Style::default();
772 let run = [
773 Cell::new('a', st),
774 Cell::wide('世', st),
775 Cell::continuation(st),
776 Cell::new('b', st),
777 ];
778 let mut via_copy = Screen::new(2, 8);
779 via_copy.copy_cells(1, 2, &run);
780 let mut via_set = Screen::new(2, 8);
781 for (i, cell) in run.iter().enumerate() {
782 via_set.set(1, 2 + i, *cell);
783 }
784 assert_eq!(via_copy, via_set);
785 }
786
787 #[test]
788 fn copy_cells_clips_at_edge_and_ignores_out_of_range() {
789 let st = Style::default();
790 let run = [Cell::new('x', st); 5];
791 let mut s = Screen::new(2, 4);
792 s.copy_cells(0, 2, &run);
794 assert_eq!(s.cell(0, 2).ch, 'x');
795 assert_eq!(s.cell(0, 3).ch, 'x');
796 assert_eq!(s.cell(0, 1), Cell::default());
797 s.copy_cells(9, 0, &run);
799 s.copy_cells(0, 9, &run);
800 }
801}