1use crate::history::History;
6use crate::style::Style;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum CellWidth {
16 Continuation,
20 #[default]
22 Single,
23 Wide,
27}
28
29impl CellWidth {
30 pub const fn columns(self) -> usize {
33 match self {
34 CellWidth::Continuation => 0,
35 CellWidth::Single => 1,
36 CellWidth::Wide => 2,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Cell {
51 pub ch: char,
52 pub style: Style,
53 pub width: CellWidth,
54}
55
56impl Default for Cell {
57 fn default() -> Self {
58 Cell {
59 ch: ' ',
60 style: Style::default(),
61 width: CellWidth::Single,
62 }
63 }
64}
65
66impl Cell {
67 pub const fn new(ch: char, style: Style) -> Self {
69 Cell {
70 ch,
71 style,
72 width: CellWidth::Single,
73 }
74 }
75
76 pub const fn wide(ch: char, style: Style) -> Self {
79 Cell {
80 ch,
81 style,
82 width: CellWidth::Wide,
83 }
84 }
85
86 pub const fn continuation(style: Style) -> Self {
90 Cell {
91 ch: ' ',
92 style,
93 width: CellWidth::Continuation,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub struct Cursor {
102 pub row: usize,
103 pub col: usize,
104}
105
106impl Cursor {
107 pub const fn new(row: usize, col: usize) -> Self {
108 Cursor { row, col }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115enum Plain {
116 Empty,
118 Bytes(Style),
121 Mixed,
123}
124
125#[derive(Debug, Clone)]
127pub struct Screen {
128 rows: usize,
129 cols: usize,
130 cells: Vec<Cell>,
146 map: Vec<usize>,
148 hist: History,
154 plain: Vec<Plain>,
160 text: Vec<u8>,
161 written: Vec<usize>,
163 blank: Vec<Cell>,
165 wrapped: Vec<bool>,
169 cursor: Cursor,
172}
173
174impl PartialEq for Screen {
179 fn eq(&self, other: &Self) -> bool {
180 if self.rows != other.rows || self.cols != other.cols || self.cursor != other.cursor {
181 return false;
182 }
183 (0..self.rows).all(|r| {
184 self.row_wrapped(r) == other.row_wrapped(r)
185 && (0..self.cols).all(|c| self.cell(r, c) == other.cell(r, c))
186 })
187 }
188}
189
190impl Eq for Screen {}
191
192impl Screen {
193 pub fn new(rows: usize, cols: usize) -> Self {
195 Screen::with_history(rows, cols, 0)
196 }
197
198 pub fn with_history(rows: usize, cols: usize, history: usize) -> Self {
202 Screen {
203 rows,
204 cols,
205 cells: vec![Cell::default(); rows * cols],
206 map: (0..rows).collect(),
207 hist: History::new(history),
208 plain: vec![Plain::Empty; if history > 0 { rows } else { 0 }],
209 text: vec![0; if history > 0 { rows * cols } else { 0 }],
210 written: vec![0; rows],
211 blank: vec![Cell::default(); rows],
212 wrapped: vec![false; rows],
213 cursor: Cursor::default(),
214 }
215 }
216
217 pub fn visible(&self) -> Screen {
221 let mut s = Screen::new(self.rows, self.cols);
222 for r in 0..self.rows {
223 let p = self.map[r];
224 let w = self.written[p];
225 s.blank[r] = self.blank[p];
226 s.written[r] = w;
227 s.wrapped[r] = self.wrapped[p];
228 let (src, dst) = (p * self.cols, r * self.cols);
229 s.cells[dst..dst + w].copy_from_slice(&self.cells[src..src + w]);
230 }
231 s.cursor = self.cursor;
232 s
233 }
234
235 pub fn history_len(&self) -> usize {
238 self.hist.len()
239 }
240
241 pub fn history_capacity(&self) -> usize {
243 self.hist.capacity()
244 }
245
246 pub fn set_history_bytes(&mut self, max: usize) {
251 self.hist.set_max_bytes(max);
252 }
253
254 pub fn history_bytes(&self) -> usize {
256 self.hist.bytes()
257 }
258
259 pub fn scrolled_lines(&self) -> u64 {
262 self.hist.pushed()
263 }
264
265 pub fn history_cell(&self, age: usize, col: usize) -> Cell {
270 if col >= self.cols {
271 return Cell::default();
272 }
273 self.hist.cell(age, col).unwrap_or_default()
274 }
275
276 pub fn history_row(&self, age: usize, out: &mut Vec<Cell>) -> bool {
280 self.hist.row(age, self.cols, out)
281 }
282
283 pub fn history_wrapped(&self, age: usize) -> bool {
286 self.hist.wrapped(age)
287 }
288
289 pub fn row_wrapped(&self, row: usize) -> bool {
292 row < self.rows && self.wrapped[self.physical(row)]
293 }
294
295 pub fn set_row_wrapped(&mut self, row: usize, wrapped: bool) {
298 if row < self.rows {
299 let p = self.physical(row);
300 self.wrapped[p] = wrapped;
301 }
302 }
303
304 pub fn clear_history(&mut self) {
306 self.hist.clear();
307 }
308
309 pub fn copy_history_from(&mut self, other: &Screen) {
312 let cols = self.cols.min(other.cols);
313 let mut row = Vec::with_capacity(other.cols);
314 for age in (0..other.history_len()).rev() {
315 other.history_row(age, &mut row);
316 row.truncate(cols);
317 self.hist
318 .push(&row, Cell::default(), other.history_wrapped(age));
319 }
320 }
321
322 pub fn rows(&self) -> usize {
323 self.rows
324 }
325
326 pub fn cols(&self) -> usize {
327 self.cols
328 }
329
330 #[inline]
332 pub fn cursor(&self) -> Cursor {
333 self.cursor
334 }
335
336 #[inline]
340 pub fn set_cursor(&mut self, cursor: Cursor) {
341 self.cursor = Cursor {
342 row: cursor.row.min(self.rows.saturating_sub(1)),
343 col: cursor.col.min(self.cols.saturating_sub(1)),
344 };
345 }
346
347 pub fn clear(&mut self) {
351 for r in 0..self.rows {
353 self.fill_row(r, Cell::default());
354 }
355 self.cursor = Cursor::default();
356 }
357
358 #[inline]
361 fn index(&self, row: usize, col: usize) -> usize {
362 debug_assert_eq!(self.cells.len(), self.rows * self.cols);
363 debug_assert!(row < self.rows && col < self.cols);
364 self.physical(row) * self.cols + col
365 }
366
367 #[inline]
369 fn physical(&self, row: usize) -> usize {
370 self.map[row]
371 }
372
373 #[inline]
376 fn materialize(&mut self, p: usize, upto: usize) {
377 let w = self.written[p];
378 if w < upto {
379 let base = p * self.cols;
380 let blank = self.blank[p];
381 self.cells[base + w..base + upto].fill(blank);
382 self.written[p] = upto;
383 if self.track(p, blank) {
384 self.text[base + w..base + upto].fill(blank.ch as u8);
385 }
386 }
387 }
388
389 #[inline]
393 fn track(&mut self, p: usize, cell: Cell) -> bool {
394 let Some(state) = self.plain.get_mut(p) else {
395 return false;
396 };
397 let byte = cell.width == CellWidth::Single && (cell.ch as u32) < 0x100;
398 *state = match *state {
399 _ if !byte => Plain::Mixed,
400 Plain::Empty => Plain::Bytes(cell.style),
401 Plain::Bytes(s) if s == cell.style => Plain::Bytes(s),
402 _ => Plain::Mixed,
403 };
404 *state != Plain::Mixed
405 }
406
407 pub fn cell(&self, row: usize, col: usize) -> Cell {
409 if row < self.rows && col < self.cols {
410 let p = self.physical(row);
411 if col < self.written[p] {
412 self.cells[self.index(row, col)]
413 } else {
414 self.blank[p]
415 }
416 } else {
417 Cell::default()
418 }
419 }
420
421 pub fn row_cells(&self, row: usize) -> (&[Cell], Cell) {
427 if row >= self.rows {
428 return (&[], Cell::default());
429 }
430 let p = self.physical(row);
431 let base = p * self.cols;
432 (&self.cells[base..base + self.written[p]], self.blank[p])
433 }
434
435 pub fn set(&mut self, row: usize, col: usize, cell: Cell) {
437 if row < self.rows && col < self.cols {
438 let p = self.physical(row);
439 self.materialize(p, col + 1);
440 let i = self.index(row, col);
441 self.cells[i] = cell;
442 if self.track(p, cell) {
443 self.text[i] = cell.ch as u8;
444 }
445 }
446 }
447
448 pub fn write_str(&mut self, row: usize, col: usize, text: &str, style: Style) {
453 for (i, ch) in text.chars().enumerate() {
454 self.set(row, col + i, Cell::new(ch, style));
455 }
456 }
457
458 pub fn copy_cells(&mut self, row: usize, col: usize, cells: &[Cell]) {
464 if row >= self.rows || col >= self.cols {
465 return;
466 }
467 let n = cells.len().min(self.cols - col);
468 let p = self.physical(row);
469 self.materialize(p, col);
470 let base = self.index(row, col);
471 self.cells[base..base + n].copy_from_slice(&cells[..n]);
472 self.written[p] = self.written[p].max(col + n);
473 if let Some(state) = self.plain.get_mut(p) {
474 *state = Plain::Mixed;
475 }
476 }
477
478 pub fn write_ascii(&mut self, row: usize, col: usize, text: &[u8], style: Style) -> usize {
486 if row >= self.rows || col >= self.cols {
487 return 0;
488 }
489 let n = text.len().min(self.cols - col);
490 let p = self.physical(row);
491 if col == 0 && n >= self.written[p] {
492 if let Some(state) = self.plain.get_mut(p) {
494 *state = Plain::Empty;
495 }
496 }
497 self.materialize(p, col);
498 let base = self.index(row, col);
499 for (cell, &b) in self.cells[base..base + n].iter_mut().zip(text) {
500 *cell = Cell::new(b as char, style);
501 }
502 self.written[p] = self.written[p].max(col + n);
503 if n > 0 && self.track(p, Cell::new(' ', style)) {
504 self.text[base..base + n].copy_from_slice(&text[..n]);
505 }
506 n
507 }
508
509 pub fn scroll_rows_up(&mut self, top: usize, bottom: usize, n: usize, fill: Cell) {
523 if top > bottom || bottom >= self.rows || n == 0 {
524 return;
525 }
526 let n = n.min(bottom - top + 1);
527 if self.hist.capacity() > 0 && top == 0 && bottom == self.rows - 1 {
528 for r in 0..n {
531 let p = self.map[r];
532 let (from, to) = (p * self.cols, p * self.cols + self.written[p]);
533 let wrapped = self.wrapped[p];
534 match self.plain[p] {
535 Plain::Bytes(style) => {
536 self.hist
537 .push_ascii(&self.text[from..to], style, self.blank[p], wrapped)
538 }
539 Plain::Empty => {
540 self.hist
541 .push_ascii(&[], Style::default(), self.blank[p], wrapped)
542 }
543 Plain::Mixed => self
544 .hist
545 .push(&self.cells[from..to], self.blank[p], wrapped),
546 }
547 }
548 }
549 self.map[top..=bottom].rotate_left(n);
550 for r in bottom + 1 - n..=bottom {
551 self.fill_row(r, fill);
552 }
553 }
554
555 #[inline]
558 fn fill_row(&mut self, row: usize, fill: Cell) {
559 let p = self.physical(row);
560 self.written[p] = 0;
561 self.blank[p] = fill;
562 self.wrapped[p] = false;
563 if let Some(state) = self.plain.get_mut(p) {
564 *state = Plain::Empty;
565 }
566 }
567
568 pub fn scroll_rows_down(&mut self, top: usize, bottom: usize, n: usize, fill: Cell) {
572 if top > bottom || bottom >= self.rows || n == 0 {
573 return;
574 }
575 let n = n.min(bottom - top + 1);
576 self.map[top..=bottom].rotate_right(n);
577 for r in top..top + n {
578 self.fill_row(r, fill);
579 }
580 }
581
582 pub fn diff(&self, next: &Screen) -> Vec<u8> {
590 if self.rows != next.rows || self.cols != next.cols {
591 return next.render_full();
592 }
593 if self == next {
594 return Vec::new();
595 }
596 let mut out = String::new();
597 emit_changes(Some(self), next, &mut out);
598 out.into_bytes()
599 }
600
601 pub fn render_full(&self) -> Vec<u8> {
605 let mut out = String::from("\x1b[2J");
606 emit_changes(None, self, &mut out);
607 out.into_bytes()
608 }
609}
610
611fn emit_changes(prev: Option<&Screen>, next: &Screen, out: &mut String) {
619 use std::fmt::Write;
620 let mut style = Style::default();
621 let mut at: Option<(usize, usize)> = None;
625 for row in 0..next.rows {
626 let last_content = (0..next.cols)
629 .rev()
630 .find(|&c| next.cell(row, c) != Cell::default())
631 .map_or(0, |c| c + 1);
632
633 for col in 0..last_content {
635 let target = next.cell(row, col);
636 if target.width == CellWidth::Continuation {
641 continue;
642 }
643 let same = match prev {
644 Some(p) => p.cell(row, col) == target,
645 None => target == Cell::default(),
646 };
647 if same {
648 continue;
649 }
650 if at != Some((row, col)) {
651 write!(out, "\x1b[{};{}H", row + 1, col + 1).unwrap();
652 }
653 out.push_str(&style.transition_to(&target.style));
654 style = target.style;
655 out.push(target.ch);
656 let advance = target.width.columns();
663 at = if col + advance < next.cols {
664 Some((row, col + advance))
665 } else {
666 None
667 };
668 }
669
670 if last_content < next.cols {
675 let needs_erase = match prev {
676 Some(p) => (last_content..next.cols).any(|c| p.cell(row, c) != Cell::default()),
677 None => false,
678 };
679 if needs_erase {
680 out.push_str(&style.transition_to(&Style::default()));
683 style = Style::default();
684 if at != Some((row, last_content)) {
685 write!(out, "\x1b[{};{}H", row + 1, last_content + 1).unwrap();
686 }
687 out.push_str("\x1b[K");
688 at = None;
691 }
692 }
693 }
694 out.push_str(&style.transition_to(&Style::default()));
695 let Cursor { row, col } = next.cursor;
696 write!(out, "\x1b[{};{}H", row + 1, col + 1).unwrap();
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn row_cells_agrees_with_cell_before_and_after_scrolling() {
705 use crate::Color;
706 let mut s = Screen::with_history(4, 10, 8);
707 let red = Style {
708 fg: Color::Indexed(1),
709 ..Style::default()
710 };
711 s.write_str(0, 0, "abc", Style::default());
712 s.write_str(1, 2, "de", red);
713 s.copy_cells(
714 3,
715 0,
716 &[Cell::wide('\u{4e2d}', red), Cell::continuation(red)],
717 );
718 let check = |s: &Screen| {
719 for r in 0..s.rows() {
720 let (written, fill) = s.row_cells(r);
721 assert!(written.len() <= s.cols());
722 for c in 0..s.cols() {
723 let want = s.cell(r, c);
724 let got = written.get(c).copied().unwrap_or(fill);
725 assert_eq!(got, want, "row {r} col {c}");
726 }
727 }
728 };
729 check(&s);
730 s.write_str(2, 0, "0123456789", red);
733 check(&s);
734 s.scroll_rows_up(0, 3, 1, Cell::new('~', red));
735 check(&s);
736 let (written, fill) = s.row_cells(3);
737 assert!(written.is_empty(), "a scrolled-in row is lazily blank");
738 assert_eq!(fill, Cell::new('~', red));
739 assert_eq!(s.row_cells(4), (&[][..], Cell::default()));
740 }
741 use crate::style::Style;
742
743 fn numbered(rows: usize, cols: usize) -> Screen {
745 let mut s = Screen::new(rows, cols);
746 for r in 0..rows {
747 for c in 0..cols {
748 let ch = char::from_u32(0x4E00 + (r * 64 + c) as u32).unwrap();
749 s.set(r, c, Cell::new(ch, Style::default()));
750 }
751 }
752 s
753 }
754
755 fn reference_up(s: &mut Screen, top: usize, bottom: usize, n: usize, fill: Cell) {
757 let n = n.min(bottom - top + 1);
758 for r in top..=bottom {
759 for c in 0..s.cols() {
760 let cell = if r + n <= bottom {
761 s.cell(r + n, c)
762 } else {
763 fill
764 };
765 s.set(r, c, cell);
766 }
767 }
768 }
769
770 fn reference_down(s: &mut Screen, top: usize, bottom: usize, n: usize, fill: Cell) {
771 let n = n.min(bottom - top + 1);
772 for r in (top..=bottom).rev() {
773 for c in 0..s.cols() {
774 let cell = if r >= top + n { s.cell(r - n, c) } else { fill };
775 s.set(r, c, cell);
776 }
777 }
778 }
779
780 #[test]
781 fn region_scrolls_match_the_per_cell_reference_everywhere() {
782 let fill = Cell::new(' ', Style::default());
783 for (rows, cols) in [(1, 1), (5, 3), (8, 7)] {
784 for top in 0..rows {
785 for bottom in top..rows {
786 for n in 0..=(bottom - top + 2) {
787 let (mut fast, mut slow) = (numbered(rows, cols), numbered(rows, cols));
788 fast.scroll_rows_up(top, bottom, n, fill);
789 if n > 0 {
790 reference_up(&mut slow, top, bottom, n, fill);
791 }
792 assert_eq!(fast, slow, "up {rows}x{cols} {top}..={bottom} by {n}");
793
794 let (mut fast, mut slow) = (numbered(rows, cols), numbered(rows, cols));
795 fast.scroll_rows_down(top, bottom, n, fill);
796 if n > 0 {
797 reference_down(&mut slow, top, bottom, n, fill);
798 }
799 assert_eq!(fast, slow, "down {rows}x{cols} {top}..={bottom} by {n}");
800 }
801 }
802 }
803 }
804 }
805
806 #[test]
809 fn a_fully_scrolled_screen_equals_one_built_that_way() {
810 let fill = Cell::new(' ', Style::default());
811 let mut s = numbered(4, 3);
812 for _ in 0..7 {
814 s.scroll_rows_up(0, 3, 1, fill);
815 }
816 let blank = Screen::new(4, 3);
817 assert_eq!(s, blank, "everything scrolled off: a blank screen");
818
819 let mut s = numbered(4, 3);
820 s.scroll_rows_up(0, 3, 2, fill);
821 let mut want = Screen::new(4, 3);
822 for r in 0..2 {
823 for c in 0..3 {
824 want.set(r, c, numbered(4, 3).cell(r + 2, c));
825 }
826 }
827 assert_eq!(s, want, "two rows up, two blank rows in");
828 assert_eq!(s.render_full(), want.render_full(), "same bytes");
829
830 s.set(0, 0, Cell::new('A', Style::default()));
832 assert_eq!(s.cell(0, 0).ch, 'A');
833 s.clear();
834 assert_eq!(s, Screen::new(4, 3));
835 }
836
837 #[test]
841 fn mixed_scrolls_and_writes_match_the_reference_over_many_steps() {
842 let mut seed = 0x9E37_79B9_7F4A_7C15u64;
843 let mut next = |n: usize| {
844 seed ^= seed << 13;
845 seed ^= seed >> 7;
846 seed ^= seed << 17;
847 (seed % n as u64) as usize
848 };
849 for (rows, cols) in [(1, 3), (6, 5), (24, 9)] {
850 let (mut fast, mut slow) = (numbered(rows, cols), numbered(rows, cols));
851 for step in 0..2_000 {
852 let fill = Cell::new(char::from(b'a' + (step % 26) as u8), Style::default());
853 let (top, bottom) = if next(3) == 0 {
854 (0, rows - 1)
855 } else {
856 let a = next(rows);
857 (a, a + next(rows - a))
858 };
859 let n = 1 + next(3);
860 match next(4) {
861 0 => {
862 fast.scroll_rows_up(top, bottom, n, fill);
863 reference_up(&mut slow, top, bottom, n, fill);
864 }
865 1 => {
866 fast.scroll_rows_down(top, bottom, n, fill);
867 reference_down(&mut slow, top, bottom, n, fill);
868 }
869 _ => {
870 let (r, c) = (next(rows), next(cols));
871 fast.set(r, c, fill);
872 slow.set(r, c, fill);
873 }
874 }
875 assert_eq!(fast, slow, "{rows}x{cols}, step {step}");
876 }
877 }
878 }
879
880 #[test]
881 fn write_ascii_matches_copy_cells_and_clips() {
882 let style = Style {
883 bold: true,
884 ..Style::default()
885 };
886 let (mut a, mut b) = (numbered(3, 8), numbered(3, 8));
887 let fill = Cell::new(' ', style);
889 a.scroll_rows_up(0, 2, 1, fill);
890 b.scroll_rows_up(0, 2, 1, fill);
891 let text = b"hello, world";
892 assert_eq!(a.write_ascii(2, 3, text, style), 5, "clipped at the edge");
893 let cells: Vec<Cell> = text.iter().map(|&c| Cell::new(c as char, style)).collect();
894 b.copy_cells(2, 3, &cells);
895 assert_eq!(a, b);
896 assert_eq!(a.cell(2, 0), fill, "the gap before the text keeps the fill");
897 assert_eq!(a.write_ascii(3, 0, b"x", style), 0);
898 assert_eq!(a.write_ascii(0, 8, b"x", style), 0);
899 }
900
901 #[test]
904 fn a_wrap_flag_follows_its_row_into_history_and_clears_on_reuse() {
905 let fill = Cell::default();
906 let mut s = Screen::with_history(3, 5, 10);
907 s.write_str(0, 0, "hello", Style::default());
908 s.set_row_wrapped(0, true);
909 assert!(s.row_wrapped(0) && !s.row_wrapped(1));
910 assert!(s.visible().row_wrapped(0), "a snapshot keeps it");
911 let mut other = Screen::with_history(3, 5, 10);
912 other.write_str(0, 0, "hello", Style::default());
913 assert_ne!(s, other, "the flag is part of what a screen holds");
914 s.scroll_rows_up(0, 2, 1, fill);
917 assert!(s.history_wrapped(0));
918 assert!(!s.row_wrapped(2), "reused row cleared");
919 let mut copy = Screen::with_history(3, 5, 10);
920 copy.copy_history_from(&s);
921 assert!(copy.history_wrapped(0), "a resize's history copy keeps it");
922 s.set_row_wrapped(1, true);
923 s.clear();
924 assert!(!s.row_wrapped(1));
925 s.set_row_wrapped(9, true); assert!(!s.row_wrapped(9));
927 }
928
929 #[test]
933 fn history_keeps_what_scrolls_off_the_top() {
934 let fill = Cell::new(' ', Style::default());
935 let line = |k: usize| -> Vec<Cell> {
936 format!("line {k:03}")
937 .chars()
938 .map(|c| Cell::new(c, Style::default()))
939 .collect()
940 };
941 let (mut plain, mut hist) = (Screen::new(4, 10), Screen::with_history(4, 10, 5));
942 for k in 0..12 {
943 for s in [&mut plain, &mut hist] {
944 s.scroll_rows_up(0, 3, 1, fill);
945 s.copy_cells(3, 0, &line(k));
946 }
947 assert_eq!(
948 hist, plain,
949 "the visible screen is unchanged by history, step {k}"
950 );
951 }
952 assert_eq!(hist.history_len(), 5);
955 assert_eq!(hist.scrolled_lines(), 12);
956 let text = |age| -> String { (0..8).map(|c| hist.history_cell(age, c).ch).collect() };
957 assert_eq!(text(0), "line 007");
958 assert_eq!(text(4), "line 003");
959 assert_eq!(hist.history_cell(5, 0), Cell::default(), "past the ring");
960 hist.scroll_rows_up(1, 3, 1, fill);
962 assert_eq!(hist.history_len(), 5);
963 assert_eq!(hist.scrolled_lines(), 12);
964 let v = hist.visible();
966 assert_eq!(v, hist);
967 assert_eq!(v.history_len(), 0);
968 let mut wide = Screen::with_history(3, 6, 10);
970 wide.copy_history_from(&hist);
971 assert_eq!(wide.history_len(), 5);
972 let t: String = (0..6).map(|c| wide.history_cell(0, c).ch).collect();
973 assert_eq!(t, "line 0");
974 hist.clear_history();
975 assert_eq!(hist.history_len(), 0);
976 for k in 0..3 {
978 hist.scroll_rows_up(0, 3, 1, fill);
979 hist.copy_cells(3, 0, &line(100 + k));
980 }
981 assert_eq!(hist.history_len(), 3);
982 }
983
984 #[test]
988 fn history_rows_come_back_exactly_as_they_left() {
989 let (rows, cols, keep) = (5, 12, 400);
990 let mut seed = 0x2545_f491_4f6c_dd1du64;
991 let mut next = move |n: u64| {
992 seed ^= seed << 13;
993 seed ^= seed >> 7;
994 seed ^= seed << 17;
995 seed % n
996 };
997 let styles = [
998 Style::default(),
999 Style {
1000 fg: crate::style::Color::Indexed(9),
1001 bold: true,
1002 ..Style::default()
1003 },
1004 Style {
1005 bg: crate::style::Color::Rgb(10, 20, 30),
1006 italic: true,
1007 strike: true,
1008 ..Style::default()
1009 },
1010 ];
1011 let chars = ['a', 'Z', ' ', 'é', '─', '😀'];
1012 let mut s = Screen::with_history(rows, cols, keep);
1013 let mut left: Vec<Vec<Cell>> = Vec::new();
1014 for step in 0..3000 {
1015 let row = next(rows as u64) as usize;
1016 let col = next(cols as u64) as usize;
1017 let style = styles[if next(4) == 0 { next(3) as usize } else { 0 }];
1019 match next(10) {
1020 0 => s.set(row, col, Cell::wide('漢', style)),
1021 1 => s.set(row, col, Cell::continuation(style)),
1022 2..=4 => s.set(row, col, Cell::new(chars[next(6) as usize], style)),
1023 _ => {
1024 let text = &b"ls -la \xe9x"[..next(10) as usize];
1025 s.write_ascii(row, if next(2) == 0 { 0 } else { col }, text, style);
1026 }
1027 }
1028 if next(3) == 0 {
1029 let n = 1 + next(3) as usize;
1030 for r in 0..n.min(rows) {
1031 left.push((0..cols).map(|c| s.cell(r, c)).collect());
1032 }
1033 s.scroll_rows_up(0, rows - 1, n, Cell::new(' ', styles[step % 3]));
1034 }
1035 }
1036 assert_eq!(s.scrolled_lines(), left.len() as u64);
1037 assert_eq!(s.history_len(), keep.min(left.len()));
1038 let mut out = Vec::new();
1039 for age in 0..s.history_len() {
1040 assert!(s.history_row(age, &mut out));
1041 let want = &left[left.len() - 1 - age];
1042 assert_eq!(&out, want, "age {age}");
1043 for (c, cell) in want.iter().enumerate() {
1044 assert_eq!(s.history_cell(age, c), *cell, "age {age} col {c}");
1045 }
1046 }
1047 assert!(!s.history_row(s.history_len(), &mut out));
1048 }
1049
1050 #[test]
1051 fn an_out_of_range_region_scroll_changes_nothing() {
1052 let fill = Cell::new('x', Style::default());
1053 let mut s = numbered(4, 4);
1054 let before = s.clone();
1055 s.scroll_rows_up(3, 1, 1, fill);
1056 s.scroll_rows_up(0, 4, 1, fill);
1057 s.scroll_rows_down(2, 9, 1, fill);
1058 assert_eq!(s, before);
1059 }
1060
1061 fn filled_row(rows: usize, cols: usize, row: usize, ch: char) -> Screen {
1064 let mut s = Screen::new(rows, cols);
1065 for c in 0..cols {
1066 s.set(row, c, Cell::new(ch, Style::default()));
1067 }
1068 s
1069 }
1070
1071 fn contains_el(bytes: &[u8]) -> bool {
1072 let s = std::str::from_utf8(bytes).unwrap_or("");
1073 s.contains("\x1b[K")
1074 }
1075
1076 #[test]
1079 fn clear_resets_all_cells_and_cursor_without_realloc() {
1080 let mut s = Screen::new(4, 6);
1081 let dirty_style = Style {
1083 bold: true,
1084 ..Style::default()
1085 };
1086 for r in 0..4 {
1087 for c in 0..6 {
1088 s.set(r, c, Cell::new('X', dirty_style));
1089 }
1090 }
1091 s.set_cursor(Cursor::new(3, 5));
1093
1094 s.clear();
1095
1096 assert_eq!(s.rows(), 4);
1098 assert_eq!(s.cols(), 6);
1099 assert_eq!(s.cursor(), Cursor::new(0, 0));
1101 let blank = Cell::default();
1103 for r in 0..4 {
1104 for c in 0..6 {
1105 assert_eq!(
1106 s.cell(r, c),
1107 blank,
1108 "cell ({r},{c}) was not reset by clear()"
1109 );
1110 }
1111 }
1112 }
1113
1114 #[test]
1117 fn diff_emits_el_when_row_tail_clears_to_blank() {
1118 let prev = filled_row(2, 8, 0, 'X');
1121 let mut next = Screen::new(2, 8);
1122 for c in 0..3 {
1123 next.set(0, c, Cell::new('X', Style::default()));
1124 }
1125 let diff = prev.diff(&next);
1126 assert!(
1127 contains_el(&diff),
1128 "expected \\x1b[K in diff when tail blanks out"
1129 );
1130 }
1131
1132 #[test]
1133 fn diff_no_el_when_tail_was_already_blank_in_prev() {
1134 let mut prev = Screen::new(2, 8);
1137 let mut next = Screen::new(2, 8);
1138 for c in 0..3 {
1139 prev.set(0, c, Cell::new('A', Style::default()));
1140 next.set(0, c, Cell::new('B', Style::default()));
1141 }
1142 let diff = prev.diff(&next);
1143 assert!(
1144 !contains_el(&diff),
1145 "unexpected \\x1b[K when tail was already blank"
1146 );
1147 }
1148
1149 #[test]
1150 fn render_full_never_emits_el() {
1151 let s = filled_row(2, 8, 0, 'X');
1155 let bytes = s.render_full();
1156 assert!(!contains_el(&bytes), "render_full must not emit \\x1b[K");
1157 }
1158
1159 #[test]
1160 fn diff_el_visual_correctness_via_round_trip() {
1161 let prev = filled_row(1, 6, 0, 'Z');
1165 let mut next = Screen::new(1, 6);
1166 next.set(0, 0, Cell::new('A', Style::default()));
1167 assert!(contains_el(&prev.diff(&next)));
1169 let empty = next.diff(&next);
1172 assert!(empty.is_empty(), "diff of identical screens must be empty");
1173 }
1174
1175 #[test]
1176 fn diff_entire_row_blank_in_next_uses_el() {
1177 let prev = filled_row(3, 5, 1, 'Y');
1179 let next = Screen::new(3, 5);
1180 let diff = prev.diff(&next);
1181 assert!(
1182 contains_el(&diff),
1183 "entire-row blank transition should use EL"
1184 );
1185 }
1186
1187 #[test]
1190 fn cell_constructors_set_width() {
1191 let st = Style::default();
1192 assert_eq!(Cell::default().width, CellWidth::Single);
1193 assert_eq!(Cell::new('a', st).width, CellWidth::Single);
1194 assert_eq!(Cell::wide('世', st).width, CellWidth::Wide);
1195 let cont = Cell::continuation(st);
1196 assert_eq!(cont.width, CellWidth::Continuation);
1197 let cols: Vec<usize> = [CellWidth::Continuation, CellWidth::Single, CellWidth::Wide]
1198 .iter()
1199 .map(|w| w.columns())
1200 .collect();
1201 assert_eq!(cols, [0, 1, 2]);
1202 assert_eq!(cont.ch, ' ');
1203 }
1204
1205 #[test]
1206 fn wide_lead_emits_glyph_once_no_continuation_char() {
1207 let prev = Screen::new(1, 4);
1211 let mut next = Screen::new(1, 4);
1212 next.set(0, 0, Cell::wide('世', Style::default()));
1213 next.set(0, 1, Cell::continuation(Style::default()));
1214 let diff = prev.diff(&next);
1215 let s = std::str::from_utf8(&diff).unwrap();
1216 assert_eq!(s.matches('世').count(), 1, "glyph must appear exactly once");
1217 assert!(
1218 !s.contains(' '),
1219 "continuation column must emit no space: {s:?}"
1220 );
1221 assert_eq!(diff, "\x1b[1;1H世\x1b[1;1H".as_bytes());
1223 }
1224
1225 #[test]
1226 fn wide_lead_advances_cursor_by_two() {
1227 let prev = Screen::new(1, 6);
1232 let mut next = Screen::new(1, 6);
1233 next.set(0, 0, Cell::wide('世', Style::default()));
1234 next.set(0, 1, Cell::continuation(Style::default()));
1235 next.set(0, 2, Cell::new('X', Style::default()));
1236 let diff = prev.diff(&next);
1237 assert_eq!(diff, "\x1b[1;1H世X\x1b[1;1H".as_bytes());
1238 }
1239
1240 #[test]
1241 fn ascii_diff_is_byte_identical_regression() {
1242 let mut old = Screen::new(2, 10);
1244 old.write_str(0, 0, "hello", Style::default());
1245 let mut new = old.clone();
1246 new.set(0, 1, Cell::new('a', Style::default()));
1247 assert_eq!(old.diff(&new), b"\x1b[1;2Ha\x1b[1;1H");
1248 }
1249
1250 #[test]
1251 fn set_cursor_clamps_onto_the_grid() {
1252 let mut s = Screen::new(4, 6);
1253 s.set_cursor(Cursor::new(2, 3));
1254 assert_eq!(s.cursor(), Cursor::new(2, 3));
1255 s.set_cursor(Cursor::new(99, 99));
1256 assert_eq!(s.cursor(), Cursor::new(3, 5));
1257 let mut empty = Screen::new(0, 0);
1259 empty.set_cursor(Cursor::new(5, 5));
1260 assert_eq!(empty.cursor(), Cursor::new(0, 0));
1261 }
1262
1263 #[test]
1264 fn copy_cells_matches_repeated_set() {
1265 let st = Style::default();
1266 let run = [
1267 Cell::new('a', st),
1268 Cell::wide('世', st),
1269 Cell::continuation(st),
1270 Cell::new('b', st),
1271 ];
1272 let mut via_copy = Screen::new(2, 8);
1273 via_copy.copy_cells(1, 2, &run);
1274 let mut via_set = Screen::new(2, 8);
1275 for (i, cell) in run.iter().enumerate() {
1276 via_set.set(1, 2 + i, *cell);
1277 }
1278 assert_eq!(via_copy, via_set);
1279 }
1280
1281 #[test]
1282 fn copy_cells_clips_at_edge_and_ignores_out_of_range() {
1283 let st = Style::default();
1284 let run = [Cell::new('x', st); 5];
1285 let mut s = Screen::new(2, 4);
1286 s.copy_cells(0, 2, &run);
1288 assert_eq!(s.cell(0, 2).ch, 'x');
1289 assert_eq!(s.cell(0, 3).ch, 'x');
1290 assert_eq!(s.cell(0, 1), Cell::default());
1291 s.copy_cells(9, 0, &run);
1293 s.copy_cells(0, 9, &run);
1294 }
1295}