1use std::collections::HashMap;
12
13use rich::{ColorSystem, Segment, Style};
14
15use crate::frame::{Frame, FxBuild, Rgb, StyleId, StyleRemap, StyleTable};
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub struct Rect {
20 pub x: u16,
21 pub y: u16,
22 pub width: u16,
23 pub height: u16,
24}
25
26impl Rect {
27 pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
28 Rect {
29 x,
30 y,
31 width,
32 height,
33 }
34 }
35
36 pub fn right(&self) -> u16 {
37 self.x.saturating_add(self.width)
38 }
39
40 pub fn bottom(&self) -> u16 {
41 self.y.saturating_add(self.height)
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.width == 0 || self.height == 0
46 }
47
48 pub fn contains(&self, column: u16, row: u16) -> bool {
49 column >= self.x && column < self.right() && row >= self.y && row < self.bottom()
50 }
51
52 #[must_use]
54 pub fn intersection(&self, other: Rect) -> Rect {
55 let x = self.x.max(other.x);
56 let y = self.y.max(other.y);
57 let right = self.right().min(other.right());
58 let bottom = self.bottom().min(other.bottom());
59 Rect::new(x, y, right.saturating_sub(x), bottom.saturating_sub(y))
60 }
61
62 #[must_use]
64 pub fn inner(&self, n: u16) -> Rect {
65 Rect::new(
66 self.x.saturating_add(n),
67 self.y.saturating_add(n),
68 self.width.saturating_sub(n * 2),
69 self.height.saturating_sub(n * 2),
70 )
71 }
72
73 #[must_use]
83 pub fn inset(&self, top: u16, right: u16, bottom: u16, left: u16) -> Rect {
84 let width = self.width.saturating_sub(left.saturating_add(right));
85 let height = self.height.saturating_sub(top.saturating_add(bottom));
86 Rect::new(
87 self.x.saturating_add(left.min(self.width)),
88 self.y.saturating_add(top.min(self.height)),
89 width,
90 height,
91 )
92 }
93
94 #[must_use]
105 pub fn centered(&self, width: u16, height: u16) -> Rect {
106 let (width, height) = (width.min(self.width), height.min(self.height));
107 Rect::new(
108 self.x + (self.width - width) / 2,
109 self.y + (self.height - height) / 2,
110 width,
111 height,
112 )
113 }
114
115 #[must_use]
126 pub fn offset(&self, dx: i32, dy: i32) -> Rect {
127 let moved = |at: u16, by: i32, size: u16| {
128 (i32::from(at) + by).clamp(0, i32::from(u16::MAX - size)) as u16
129 };
130 Rect::new(
131 moved(self.x, dx, self.width),
132 moved(self.y, dy, self.height),
133 self.width,
134 self.height,
135 )
136 }
137
138 #[must_use]
149 pub fn union(&self, other: Rect) -> Rect {
150 if other.is_empty() {
151 return *self;
152 }
153 if self.is_empty() {
154 return other;
155 }
156 let (x, y) = (self.x.min(other.x), self.y.min(other.y));
157 let right = self.right().max(other.right());
158 let bottom = self.bottom().max(other.bottom());
159 Rect::new(x, y, right - x, bottom - y)
160 }
161
162 pub fn intersects(&self, other: Rect) -> bool {
172 !self.intersection(other).is_empty()
173 }
174
175 pub fn rows(self) -> impl DoubleEndedIterator<Item = Rect> + ExactSizeIterator {
184 (self.y..self.bottom()).map(move |y| Rect::new(self.x, y, self.width, 1))
185 }
186
187 pub fn columns(self) -> impl DoubleEndedIterator<Item = Rect> + ExactSizeIterator {
196 (self.x..self.right()).map(move |x| Rect::new(x, self.y, 1, self.height))
197 }
198}
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
221pub struct Cell {
222 bytes: [u8; 4],
224 len: u8,
226 pub width: u8,
227 pub style: StyleId,
228}
229
230const LONG: u8 = u8::MAX;
232
233struct Graphemes {
237 texts: Vec<&'static str>,
238 index: HashMap<&'static str, u32>,
239}
240
241fn graphemes() -> &'static std::sync::RwLock<Graphemes> {
242 static TABLE: std::sync::OnceLock<std::sync::RwLock<Graphemes>> = std::sync::OnceLock::new();
243 TABLE.get_or_init(|| {
244 std::sync::RwLock::new(Graphemes {
245 texts: Vec::new(),
246 index: HashMap::new(),
247 })
248 })
249}
250
251impl Cell {
252 pub fn new(text: &str, width: u8, style: StyleId) -> Cell {
255 let mut bytes = [0u8; 4];
256 let len = if text.len() <= 4 {
257 bytes[..text.len()].copy_from_slice(text.as_bytes());
258 text.len() as u8
259 } else {
260 let known = graphemes()
261 .read()
262 .unwrap_or_else(|e| e.into_inner())
263 .index
264 .get(text)
265 .copied();
266 let at = known.unwrap_or_else(|| {
267 let mut table = graphemes().write().unwrap_or_else(|e| e.into_inner());
268 if let Some(&at) = table.index.get(text) {
269 return at;
270 }
271 let at = table.texts.len() as u32;
272 let text: &'static str = Box::leak(text.to_string().into_boxed_str());
273 table.texts.push(text);
274 table.index.insert(text, at);
275 at
276 });
277 bytes = at.to_le_bytes();
278 LONG
279 };
280 Cell {
281 bytes,
282 len,
283 width,
284 style,
285 }
286 }
287
288 #[inline]
290 fn char(c: char, width: u8, style: StyleId) -> Cell {
291 let mut bytes = [0u8; 4];
292 let len = c.encode_utf8(&mut bytes).len() as u8;
293 Cell {
294 bytes,
295 len,
296 width,
297 style,
298 }
299 }
300
301 fn blank() -> Cell {
302 Cell::char(' ', 1, StyleId::NONE)
303 }
304
305 #[inline]
307 pub(crate) fn from_char(c: char, width: u8, style: StyleId) -> Cell {
308 Cell::char(c, width, style)
309 }
310
311 #[inline]
314 pub(crate) fn single_char(&self) -> Option<char> {
315 let [b0, b1, b2, b3] = self.bytes.map(u32::from);
316 let code = match self.len {
317 1 => b0,
318 2 if b0 & 0xe0 == 0xc0 => (b0 & 0x1f) << 6 | (b1 & 0x3f),
319 3 if b0 & 0xf0 == 0xe0 => (b0 & 0x0f) << 12 | (b1 & 0x3f) << 6 | (b2 & 0x3f),
320 4 if b0 & 0xf8 == 0xf0 => {
321 (b0 & 0x07) << 18 | (b1 & 0x3f) << 12 | (b2 & 0x3f) << 6 | (b3 & 0x3f)
322 }
323 _ => return None,
324 };
325 char::from_u32(code)
326 }
327
328 pub fn text(&self) -> &str {
330 if self.len == LONG {
331 let at = u32::from_le_bytes(self.bytes) as usize;
332 let table = graphemes().read().unwrap_or_else(|e| e.into_inner());
333 return table.texts[at];
334 }
335 std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or(" ")
337 }
338
339 pub fn is_continuation(&self) -> bool {
340 self.width == 0
341 }
342
343 #[inline]
345 pub(crate) fn is_empty_text(&self) -> bool {
346 self.len == 0
347 }
348
349 #[inline]
350 fn set(&mut self, text: &str, width: u8, style: StyleId) {
351 *self = Cell::new(text, width, style);
352 }
353
354 #[inline]
356 fn restyled(self, width: u8, style: StyleId) -> Cell {
357 Cell {
358 width,
359 style,
360 ..self
361 }
362 }
363}
364
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
371pub struct TextRun<'a> {
372 pub text: &'a str,
373 pub style: StyleId,
374}
375
376impl<'a> TextRun<'a> {
377 pub fn new(text: &'a str, style: StyleId) -> TextRun<'a> {
378 TextRun { text, style }
379 }
380}
381
382#[derive(Clone, Debug)]
390pub struct Screen {
391 width: u16,
392 height: u16,
393 cells: Vec<Cell>,
394 styles: StyleTable,
395 layer: Option<(Style, StyleId)>,
397 layered: HashMap<(u32, u32, bool), StyleId, FxBuild>,
400 scrolled: Vec<(Rect, u16)>,
404 scratch_ids: Vec<StyleId>,
407 scratch_spans: Vec<(usize, usize, usize)>,
408}
409
410impl Screen {
411 pub fn new(width: u16, height: u16) -> Screen {
412 Screen {
413 width,
414 height,
415 cells: vec![Cell::blank(); width as usize * height as usize],
416 styles: StyleTable::new(),
417 layer: None,
418 layered: HashMap::default(),
419 scrolled: Vec::new(),
420 scratch_ids: Vec::new(),
421 scratch_spans: Vec::new(),
422 }
423 }
424
425 pub fn scrolled(&self) -> &[(Rect, u16)] {
429 &self.scrolled
430 }
431
432 pub fn forget_scrolls(&mut self) {
434 self.scrolled.clear();
435 }
436
437 fn row(&self, y: u16) -> &[Cell] {
439 let width = self.width as usize;
440 &self.cells[y as usize * width..(y as usize + 1) * width]
441 }
442
443 pub fn area(&self) -> Rect {
444 Rect::new(0, 0, self.width, self.height)
445 }
446
447 pub fn cell(&self, x: u16, y: u16) -> &Cell {
448 &self.cells[y as usize * self.width as usize + x as usize]
449 }
450
451 pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
466 (x < self.width && y < self.height).then(|| self.cell(x, y))
467 }
468
469 fn cell_mut(&mut self, x: u16, y: u16) -> &mut Cell {
470 let width = self.width as usize;
471 &mut self.cells[y as usize * width + x as usize]
472 }
473
474 pub fn style(&self, id: StyleId) -> Option<&Style> {
475 self.styles.get(id)
476 }
477
478 pub fn style_count(&self) -> usize {
482 self.styles.len()
483 }
484
485 pub fn intern(&mut self, style: Option<&Style>) -> StyleId {
487 self.styles.intern(style)
488 }
489
490 pub fn styles_piled_up(&self) -> bool {
494 self.styles.piled_up()
495 }
496
497 pub fn compact_styles(&mut self) -> StyleRemap {
521 let mut keep = vec![false; self.styles.len()];
522 for cell in &self.cells {
523 if let Some(used) = keep.get_mut(cell.style.index()) {
524 *used = true;
525 }
526 }
527 let remap = self.styles.retain(&keep);
528 for cell in &mut self.cells {
529 cell.style = remap.get_or_gone(cell.style);
530 }
531 self.layer = None;
532 self.layered.clear();
533 remap
534 }
535
536 pub fn put_rgb(&mut self, x: u16, y: u16, ch: char, fg: Option<Rgb>, bg: Option<Rgb>) {
563 if x >= self.width || y >= self.height {
564 return;
565 }
566 let id = self.styles.intern_rgb(fg, bg);
567 self.put_cell(x, y, ch, id);
568 }
569
570 pub fn put(&mut self, x: u16, y: u16, ch: char, style: StyleId) {
587 if x >= self.width
588 || y >= self.height
589 || self.styles.get(style).is_none() && style != StyleId::NONE
590 {
591 return;
592 }
593 self.put_cell(x, y, ch, style);
594 }
595
596 #[inline]
597 fn put_cell(&mut self, x: u16, y: u16, ch: char, style: StyleId) {
598 let ch = narrow(ch);
599 let at = y as usize * self.width as usize + x as usize;
600 let width = self.cells[at].width;
601 if width == 0 && x > 0 {
602 let lead = &mut self.cells[at - 1];
604 *lead = Cell::char(' ', 1, lead.style);
605 } else if width >= 2 && x + 1 < self.width {
606 let next = &mut self.cells[at + 1];
607 *next = Cell::char(' ', 1, next.style);
608 }
609 self.cells[at] = Cell::char(ch, 1, style);
610 }
611
612 fn split_edges(&mut self, rect: Rect) {
616 let rect = rect.intersection(self.area());
617 if rect.is_empty() {
618 return;
619 }
620 for y in rect.y..rect.bottom() {
621 if rect.x > 0 && self.cell(rect.x, y).is_continuation() {
622 let style = self.cell(rect.x - 1, y).style;
623 self.cell_mut(rect.x - 1, y).set(" ", 1, style);
624 }
625 if rect.right() < self.width && self.cell(rect.right(), y).is_continuation() {
626 let style = self.cell(rect.right(), y).style;
627 self.cell_mut(rect.right(), y).set(" ", 1, style);
628 }
629 }
630 }
631
632 pub fn clear(&mut self, rect: Rect) {
634 let rect = rect.intersection(self.area());
635 self.split_edges(rect);
636 for y in rect.y..rect.bottom() {
637 for x in rect.x..rect.right() {
638 *self.cell_mut(x, y) = Cell::blank();
639 }
640 }
641 }
642
643 pub fn write_lines(&mut self, rect: Rect, lines: &[Vec<Segment>]) {
648 let rect = rect.intersection(self.area());
649 if rect.is_empty() {
650 return;
651 }
652 self.split_edges(rect);
653 if !self.write_simple_lines(rect, lines) {
654 self.write_frame_lines(rect, lines);
655 }
656 }
657
658 fn write_frame_lines(&mut self, rect: Rect, lines: &[Vec<Segment>]) {
661 let mut segments = Vec::new();
662 for line in lines.iter().take(rect.height as usize) {
663 let start = segments.len();
664 segments.extend(line.iter().cloned());
665 super::sanitize_line(&mut segments[start..]);
666 segments.push(Segment::line());
667 }
668 let frame = Frame::from_segments(&segments);
669 let mut ids: Vec<Option<StyleId>> = vec![None; frame.styles().len() + 1];
671 for row in 0..rect.height {
672 let cells = if (row as usize) < frame.height() {
673 frame.cells(row as usize)
674 } else {
675 Vec::new()
676 };
677 let y = rect.y + row;
678 let mut x = rect.x;
679 for cell in &cells {
680 if x >= rect.right() {
681 break;
682 }
683 if cell.is_continuation() {
684 continue;
685 }
686 let id = *ids[cell.style.index()].get_or_insert_with(|| {
687 let style = frame.styles().get(cell.style).filter(|s| !s.is_null());
689 self.styles.intern(style)
690 });
691 if cell.width >= 2 && x + 1 >= rect.right() {
692 self.cell_mut(x, y).set(" ", 1, id);
694 x += 1;
695 continue;
696 }
697 self.cell_mut(x, y).set(cell.text, cell.width.max(1), id);
698 for extra in 1..cell.width {
699 self.cell_mut(x + extra as u16, y).set("", 0, id);
700 }
701 x += cell.width.max(1) as u16;
702 }
703 while x < rect.right() {
704 *self.cell_mut(x, y) = Cell::blank();
705 x += 1;
706 }
707 }
708 }
709
710 fn write_simple_lines(&mut self, rect: Rect, lines: &[Vec<Segment>]) -> bool {
720 let lines = &lines[..lines.len().min(rect.height as usize)];
721 let direct = lines.iter().flatten().all(|segment| {
722 segment.control
723 || segment
724 .style
725 .as_ref()
726 .is_none_or(|s| s.meta_ref().is_none())
727 && !segment.text.contains('\n')
728 });
729 if !direct {
730 return false;
731 }
732 let mut spans = std::mem::take(&mut self.scratch_spans);
733 let mut ids = std::mem::take(&mut self.scratch_ids);
734 for row in 0..rect.height {
735 let line = lines.get(row as usize).map_or(&[][..], Vec::as_slice);
736 ids.clear();
737 for segment in line {
738 let style = segment.style.as_ref().filter(|s| !s.is_null());
740 ids.push(if segment.control {
741 StyleId::NONE
742 } else {
743 self.styles.intern(style)
744 });
745 }
746 let pieces = line
747 .iter()
748 .zip(&ids)
749 .filter(|(segment, _)| !segment.control)
750 .map(|(segment, &id)| (segment.text.as_str(), id));
751 self.write_row(rect, rect.y + row, pieces, &mut spans);
752 }
753 self.scratch_spans = spans;
754 self.scratch_ids = ids;
755 true
756 }
757
758 pub fn write_runs<'t, L: AsRef<[TextRun<'t>]>>(&mut self, rect: Rect, lines: &[L]) {
785 let rect = rect.intersection(self.area());
786 if rect.is_empty() {
787 return;
788 }
789 let lines = &lines[..lines.len().min(rect.height as usize)];
790 let mut checked = None;
793 let framed = lines.iter().flat_map(AsRef::as_ref).any(|run| {
794 run.text.contains('\n')
795 || checked != Some(run.style) && {
796 checked = Some(run.style);
797 self.styles
798 .get(run.style)
799 .is_some_and(|s| s.meta_ref().is_some())
800 }
801 });
802 if framed {
803 let segments: Vec<Vec<Segment>> = lines
804 .iter()
805 .map(|line| {
806 line.as_ref()
807 .iter()
808 .map(|run| Segment::new(run.text, self.style(run.style).cloned()))
809 .collect()
810 })
811 .collect();
812 self.write_lines(rect, &segments);
813 return;
814 }
815 self.split_edges(rect);
816 let mut spans = std::mem::take(&mut self.scratch_spans);
817 let mut ids = std::mem::take(&mut self.scratch_ids);
818 for row in 0..rect.height {
819 let line = lines.get(row as usize).map_or(&[][..], AsRef::as_ref);
820 ids.clear();
821 ids.extend(line.iter().map(|run| match self.styles.get(run.style) {
823 Some(style) if style.is_null() => StyleId::NONE,
824 _ => run.style,
825 }));
826 let pieces = line.iter().zip(&ids).map(|(run, &id)| (run.text, id));
827 self.write_row(rect, rect.y + row, pieces, &mut spans);
828 }
829 self.scratch_spans = spans;
830 self.scratch_ids = ids;
831 }
832
833 pub(crate) fn write_cell_grid(
842 &mut self,
843 rect: Rect,
844 width: usize,
845 rows: usize,
846 cells: &[Option<Cell>],
847 ids: &[StyleId],
848 ) -> bool {
849 let rect = rect.intersection(self.area());
850 if rect.is_empty() {
851 return true;
852 }
853 let rows = rows.min(rect.height as usize);
854 let cells = &cells[..rows * width];
855 for line in cells.chunks_exact(width.max(1)) {
856 let mut wide = false;
857 for (at, cell) in line.iter().enumerate() {
858 let Some(cell) = cell else {
859 if wide {
860 return false;
861 }
862 continue;
863 };
864 if cell.len == 0 {
865 if !wide {
867 return false;
868 }
869 wide = false;
870 continue;
871 }
872 if wide {
873 return false;
874 }
875 if cell.len == 1 {
876 if !(b' '..=b'~').contains(&cell.bytes[0]) || cell.width != 1 {
877 return false;
878 }
879 continue;
880 }
881 let Some(c) = cell.single_char() else {
882 return false;
883 };
884 if matches!(c, '\u{2500}'..='\u{259f}' | '\u{2800}'..='\u{28ff}') && cell.width == 1
886 {
887 continue;
888 }
889 let w = char_width(c);
890 if super::control_picture(c).is_some()
891 || !matches!(w, 1 | 2)
892 || c == '\u{200d}'
893 || c == '\u{fe0f}'
894 || usize::from(cell.width) != w
895 {
896 return false;
897 }
898 wide = w == 2;
899 if wide && at + 1 == line.len() {
900 return false;
901 }
902 }
903 if wide {
904 return false;
905 }
906 }
907 self.split_edges(rect);
908 let shown = width.min(rect.width as usize);
909 for row in 0..rect.height {
910 let y = rect.y + row;
911 let mut x = 0usize;
912 if (row as usize) < rows {
913 let line = &cells[row as usize * width..row as usize * width + shown];
914 while x < shown {
915 let at = y as usize * self.width as usize + rect.x as usize + x;
916 match line[x] {
917 None => {
918 self.cells[at] = Cell::blank();
919 x += 1;
920 }
921 Some(cell) => {
922 let id = ids[cell.style.index()];
923 if cell.width == 2 {
924 if x + 1 >= shown {
925 self.cells[at] = Cell::char(' ', 1, id);
927 x += 1;
928 break;
929 }
930 self.cells[at] = cell.restyled(2, id);
931 self.cells[at + 1] = Cell::new("", 0, id);
932 x += 2;
933 } else {
934 self.cells[at] = cell.restyled(1, id);
935 x += 1;
936 }
937 }
938 }
939 }
940 }
941 for gx in x..rect.width as usize {
942 let at = y as usize * self.width as usize + rect.x as usize + gx;
943 self.cells[at] = Cell::blank();
944 }
945 }
946 true
947 }
948
949 fn write_row<'t>(
954 &mut self,
955 rect: Rect,
956 y: u16,
957 pieces: impl Iterator<Item = (&'t str, StyleId)>,
958 spans: &mut Vec<(usize, usize, usize)>,
959 ) {
960 let mut x = rect.x;
961 let mut last: Option<u16> = None;
965 let mut lead = String::new();
966 let mut cut = false;
967 for (text, id) in pieces {
968 let (mut simple, mut controls) = (true, false);
970 for c in text.chars() {
971 if (' '..='~').contains(&c) {
972 continue;
973 }
974 if super::control_picture(c).is_some() {
975 controls = true;
976 } else if !matches!(char_width(c), 1 | 2) || c == '\u{200d}' || c == '\u{fe0f}' {
977 simple = false;
978 }
979 }
980 if simple && !controls {
981 let mut chars = text.chars();
983 if !lead.is_empty()
984 && !cut
985 && let Some(c) = chars.next()
986 {
987 lead.push(c);
988 let piece = std::mem::take(&mut lead);
989 self.put_grapheme(
990 rect,
991 &mut x,
992 y,
993 &piece,
994 char_width(c),
995 id,
996 &mut last,
997 &mut cut,
998 );
999 }
1000 for c in chars {
1001 if cut {
1002 break;
1003 }
1004 if x >= rect.right() {
1005 cut = true;
1006 break;
1007 }
1008 let width = char_width(c) as u16;
1009 if width == 2 && x + 1 >= rect.right() {
1010 self.cell_mut(x, y).set(" ", 1, id);
1011 x += 1;
1012 cut = true;
1013 continue;
1014 }
1015 *self.cell_mut(x, y) = Cell::char(c, width as u8, id);
1016 if width == 2 {
1017 self.cell_mut(x + 1, y).set("", 0, id);
1018 }
1019 last = Some(x);
1020 x += width;
1021 }
1022 continue;
1023 }
1024 let sanitized;
1025 let text = if controls {
1026 sanitized = text
1027 .chars()
1028 .map(|c| super::control_picture(c).unwrap_or(c))
1029 .collect::<String>();
1030 sanitized.as_str()
1031 } else {
1032 text
1033 };
1034 spans.clear();
1035 spans.extend(rich::cells::split_graphemes(text).0);
1036 for &(from, to, width) in spans.iter() {
1037 let piece = &text[from..to];
1038 if width == 0 {
1039 if cut {
1040 continue;
1041 }
1042 match last {
1043 Some(at) => {
1044 let cell = *self.cell(at, y);
1045 let joined = format!("{}{piece}", cell.text());
1046 *self.cell_mut(at, y) = Cell::new(&joined, cell.width, cell.style);
1047 }
1048 None => lead.push_str(piece),
1049 }
1050 continue;
1051 }
1052 if !lead.is_empty() && !cut {
1053 let piece = std::mem::take(&mut lead) + piece;
1054 self.put_grapheme(rect, &mut x, y, &piece, width, id, &mut last, &mut cut);
1055 } else {
1056 self.put_grapheme(rect, &mut x, y, piece, width, id, &mut last, &mut cut);
1057 }
1058 }
1059 }
1060 while x < rect.right() {
1061 *self.cell_mut(x, y) = Cell::blank();
1062 x += 1;
1063 }
1064 }
1065
1066 #[allow(clippy::too_many_arguments)]
1070 fn put_grapheme(
1071 &mut self,
1072 rect: Rect,
1073 x: &mut u16,
1074 y: u16,
1075 piece: &str,
1076 width: usize,
1077 id: StyleId,
1078 last: &mut Option<u16>,
1079 cut: &mut bool,
1080 ) {
1081 if *cut {
1082 return;
1083 }
1084 if *x >= rect.right() {
1085 *cut = true;
1086 return;
1087 }
1088 let width = width as u16;
1089 if width >= 2 && *x + 1 >= rect.right() {
1090 self.cell_mut(*x, y).set(" ", 1, id);
1092 *x += 1;
1093 *cut = true;
1094 return;
1095 }
1096 *self.cell_mut(*x, y) = Cell::new(piece, width as u8, id);
1097 for extra in 1..width {
1098 self.cell_mut(*x + extra, y).set("", 0, id);
1099 }
1100 *last = Some(*x);
1101 *x += width;
1102 }
1103
1104 pub fn underlay(&mut self, x: u16, y: u16, style: &Style) {
1107 self.lay(x, y, style, false);
1108 }
1109
1110 pub fn restyle(&mut self, x: u16, y: u16, style: &Style) {
1112 self.lay(x, y, style, true);
1113 }
1114
1115 fn lay(&mut self, x: u16, y: u16, style: &Style, over: bool) {
1119 if x >= self.width || y >= self.height {
1120 return;
1121 }
1122 let layer = match &self.layer {
1123 Some((last, id)) if last == style => *id,
1124 _ => {
1125 let id = self.styles.intern(Some(style));
1126 self.layer = Some((style.clone(), id));
1127 id
1128 }
1129 };
1130 let own = self.cell(x, y).style;
1131 let key = (layer.index() as u32, own.index() as u32, over);
1132 let id = match self.layered.get(&key) {
1133 Some(&id) => id,
1134 None => {
1135 let combined = match self.styles.get(own) {
1136 Some(own) if over => own.combine(style),
1137 Some(own) => style.combine(own),
1138 None => style.clone(),
1139 };
1140 let id = self.styles.intern(Some(&combined));
1141 self.layered.insert(key, id);
1142 id
1143 }
1144 };
1145 self.cell_mut(x, y).style = id;
1146 }
1147
1148 pub fn blit(&mut self, source: &Screen, from: Rect, x: u16, y: u16) {
1152 let from = from.intersection(source.area());
1153 self.split_edges(Rect::new(x, y, from.width, from.height));
1154 let mut ids: Vec<Option<StyleId>> = vec![None; source.styles.len()];
1156 for row in 0..from.height {
1157 let ty = y.saturating_add(row);
1158 if ty >= self.height {
1159 break;
1160 }
1161 for column in 0..from.width {
1162 let tx = x.saturating_add(column);
1163 if tx >= self.width {
1164 break;
1165 }
1166 let cell = *source.cell(from.x + column, from.y + row);
1167 let cut = cell.is_continuation() && column == 0
1168 || cell.width >= 2 && (column + 1 >= from.width || tx + 1 >= self.width);
1169 let styles = &mut self.styles;
1170 let id = match ids.get_mut(cell.style.index()) {
1171 Some(slot) => {
1172 *slot.get_or_insert_with(|| styles.intern(source.style(cell.style)))
1173 }
1174 None => styles.intern(source.style(cell.style)),
1175 };
1176 if cut {
1177 self.cell_mut(tx, ty).set(" ", 1, id);
1178 } else {
1179 *self.cell_mut(tx, ty) = cell.restyled(cell.width, id);
1180 }
1181 }
1182 }
1183 }
1184
1185 pub fn scroll_up(&mut self, rect: Rect, rows: u16) {
1188 let rect = rect.intersection(self.area());
1189 if rect.is_empty() {
1190 return;
1191 }
1192 let rows = rows.min(rect.height);
1193 if self.scrolled.len() >= 8 {
1194 self.scrolled.remove(0);
1195 }
1196 self.scrolled.push((rect, rows));
1197 self.split_edges(rect);
1198 let width = self.width as usize;
1199 for y in rect.y..rect.bottom() - rows {
1200 let (from, to) = ((y + rows) as usize * width, y as usize * width);
1201 let (a, b) = (from + rect.x as usize, from + rect.right() as usize);
1202 self.cells.copy_within(a..b, to + rect.x as usize);
1203 }
1204 self.clear(Rect::new(rect.x, rect.bottom() - rows, rect.width, rows));
1205 }
1206
1207 pub fn lines(&self) -> Vec<Vec<Segment>> {
1212 (0..self.height)
1213 .map(|y| {
1214 let mut line: Vec<Segment> = Vec::new();
1215 let mut run = String::new();
1216 let mut style = None;
1217 for x in 0..self.width {
1218 let cell = self.cell(x, y);
1219 if cell.is_continuation() {
1220 continue;
1221 }
1222 if style != Some(cell.style) && !run.is_empty() {
1223 let id = style.expect("a run has a style");
1224 line.push(Segment::new(
1225 std::mem::take(&mut run),
1226 self.style(id).cloned(),
1227 ));
1228 }
1229 style = Some(cell.style);
1230 run.push_str(cell.text());
1231 }
1232 if let Some(id) = style.filter(|_| !run.is_empty()) {
1233 line.push(Segment::new(run, self.style(id).cloned()));
1234 }
1235 line
1236 })
1237 .collect()
1238 }
1239
1240 pub fn to_svg(&self, options: &crate::frame::SvgOptions<'_>) -> String {
1257 let mut segments = Vec::new();
1258 for (y, line) in self.lines().into_iter().enumerate() {
1259 if y > 0 {
1260 segments.push(Segment::line());
1261 }
1262 segments.extend(line);
1263 }
1264 let mut options = options.clone();
1265 options.width.get_or_insert(self.width as usize);
1266 Frame::from_segments(&segments).to_svg(&options)
1267 }
1268
1269 pub fn plain(&self) -> Vec<String> {
1271 (0..self.height)
1272 .map(|y| {
1273 (0..self.width)
1274 .map(|x| self.cell(x, y))
1275 .filter(|c| !c.is_continuation())
1276 .map(|c| c.text())
1277 .collect()
1278 })
1279 .collect()
1280 }
1281}
1282
1283#[inline]
1288pub(crate) fn char_width(c: char) -> usize {
1289 rich::cells::char_cell_width(c)
1290}
1291
1292#[inline]
1295fn narrow(ch: char) -> char {
1296 if (' '..='~').contains(&ch)
1298 || ('\u{2500}'..='\u{259f}').contains(&ch)
1299 || ('\u{2800}'..='\u{28ff}').contains(&ch)
1300 {
1301 return ch;
1302 }
1303 if let Some(picture) = super::control_picture(ch) {
1304 return picture;
1305 }
1306 let mut buffer = [0u8; 4];
1307 if rich::cells::cell_len(ch.encode_utf8(&mut buffer)) == 1 {
1308 ch
1309 } else {
1310 ' '
1311 }
1312}
1313
1314struct Sent {
1317 width: u16,
1318 height: u16,
1319 cells: Vec<Cell>,
1320}
1321
1322impl Sent {
1323 fn row(&self, y: u16) -> &[Cell] {
1324 let width = self.width as usize;
1325 &self.cells[y as usize * width..(y as usize + 1) * width]
1326 }
1327
1328 fn shift(&mut self, top: u16, bottom: u16, by: i32) {
1331 let width = self.width as usize;
1332 let rows = (bottom - top) as i32;
1333 let n = by.unsigned_abs() as u16;
1334 if by > 0 {
1335 for y in top..bottom - n {
1336 let from = (y + n) as usize * width;
1337 self.cells
1338 .copy_within(from..from + width, y as usize * width);
1339 }
1340 } else {
1341 for y in (top + n..bottom).rev() {
1342 let from = (y - n) as usize * width;
1343 self.cells
1344 .copy_within(from..from + width, y as usize * width);
1345 }
1346 }
1347 let opened = if by > 0 {
1348 bottom - n..bottom
1349 } else {
1350 top..top + n
1351 };
1352 debug_assert!(rows > by.abs());
1353 for y in opened {
1354 let row = y as usize * width;
1355 self.cells[row..row + width].fill(Cell::blank());
1356 }
1357 }
1358}
1359
1360const SCROLL_WINDOW: u16 = 32;
1363
1364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1367struct Shift {
1368 top: u16,
1369 bottom: u16,
1370 by: i32,
1371 gain: u16,
1373}
1374
1375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1391pub enum PaintStrategy {
1392 Cells,
1395 Rows,
1398 Full,
1400 Scroll,
1405 #[default]
1410 Best,
1411}
1412
1413impl PaintStrategy {
1414 pub const ALL: [PaintStrategy; 5] = [
1416 PaintStrategy::Cells,
1417 PaintStrategy::Rows,
1418 PaintStrategy::Full,
1419 PaintStrategy::Scroll,
1420 PaintStrategy::Best,
1421 ];
1422
1423 pub fn name(self) -> &'static str {
1425 match self {
1426 PaintStrategy::Cells => "cells",
1427 PaintStrategy::Rows => "rows",
1428 PaintStrategy::Full => "full",
1429 PaintStrategy::Scroll => "scroll",
1430 PaintStrategy::Best => "best",
1431 }
1432 }
1433
1434 pub fn from_env() -> Option<PaintStrategy> {
1436 std::env::var("RICH_PAINT_STRATEGY")
1437 .ok()?
1438 .trim()
1439 .parse()
1440 .ok()
1441 }
1442}
1443
1444impl std::fmt::Display for PaintStrategy {
1445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1446 f.write_str(self.name())
1447 }
1448}
1449
1450impl std::str::FromStr for PaintStrategy {
1451 type Err = String;
1452
1453 fn from_str(name: &str) -> Result<PaintStrategy, String> {
1455 PaintStrategy::ALL
1456 .into_iter()
1457 .find(|s| s.name().eq_ignore_ascii_case(name))
1458 .ok_or_else(|| format!("no paint strategy {name:?}: cells, rows, full, scroll or best"))
1459 }
1460}
1461
1462#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1482pub struct PaintStats {
1483 pub compared: usize,
1485 pub written: usize,
1487 pub elided: usize,
1489 pub move_bytes: usize,
1491 pub style_bytes: usize,
1493 pub text_bytes: usize,
1495 pub other_bytes: usize,
1497 pub strategy: PaintStrategy,
1500}
1501
1502impl Default for PaintStats {
1503 fn default() -> PaintStats {
1506 PaintStats {
1507 compared: 0,
1508 written: 0,
1509 elided: 0,
1510 move_bytes: 0,
1511 style_bytes: 0,
1512 text_bytes: 0,
1513 other_bytes: 0,
1514 strategy: PaintStrategy::Cells,
1515 }
1516 }
1517}
1518
1519impl PaintStats {
1520 pub fn bytes(&self) -> usize {
1522 self.move_bytes + self.style_bytes + self.text_bytes + self.other_bytes
1523 }
1524
1525 pub fn add(&mut self, other: &PaintStats) {
1527 self.compared += other.compared;
1528 self.written += other.written;
1529 self.elided += other.elided;
1530 self.move_bytes += other.move_bytes;
1531 self.style_bytes += other.style_bytes;
1532 self.text_bytes += other.text_bytes;
1533 self.other_bytes += other.other_bytes;
1534 self.strategy = other.strategy;
1535 }
1536}
1537
1538pub struct Painter {
1545 previous: Option<Sent>,
1546 system: Option<ColorSystem>,
1547 codes: Vec<Option<Sgr>>,
1549 scratch: Sgr,
1551 rgb_codes: RgbCodes,
1553 changes: SgrChanges,
1557 inline: bool,
1560 cursor: Option<(u16, u16)>,
1563 height: u16,
1565 force: bool,
1567 written: Vec<Rect>,
1569 stats: PaintStats,
1571 strategy: PaintStrategy,
1573 erase: bool,
1575 scroll: bool,
1577 fixed: Vec<Rect>,
1580 found: Option<Shift>,
1582}
1583
1584#[derive(Default)]
1586struct Pen {
1587 at: Option<(u16, u16)>,
1588 style: Option<StyleId>,
1589 link: Option<String>,
1590 held: Held,
1592 rgb: Option<(Option<Rgb>, Option<Rgb>)>,
1595}
1596
1597#[derive(Clone, Copy, Default)]
1600enum Held {
1601 #[default]
1603 Unknown,
1604 Reset,
1606 Style(StyleId),
1608}
1609
1610static NO_SGR: Sgr = Sgr {
1612 full: String::new(),
1613 fg: String::new(),
1614 bg: String::new(),
1615 rest: String::new(),
1616};
1617
1618fn held_codes(codes: &[Option<Sgr>], held: Held) -> Option<&Sgr> {
1620 match held {
1621 Held::Unknown => None,
1622 Held::Reset => Some(&NO_SGR),
1623 Held::Style(id) => codes.get(id.index()).and_then(Option::as_ref),
1624 }
1625}
1626
1627#[derive(Clone, Debug, Default, PartialEq, Eq)]
1630struct Sgr {
1631 full: String,
1633 fg: String,
1635 bg: String,
1637 rest: String,
1639}
1640
1641impl Sgr {
1642 fn parse(codes: &str) -> Sgr {
1643 let mut sgr = Sgr {
1644 full: codes.to_string(),
1645 ..Sgr::default()
1646 };
1647 let tokens: Vec<&str> = codes.split(';').filter(|t| !t.is_empty()).collect();
1648 let mut i = 0;
1649 while i < tokens.len() {
1650 let token = tokens[i];
1651 let take = match (token, tokens.get(i + 1).copied()) {
1653 ("38" | "48" | "58", Some("5")) => 3,
1654 ("38" | "48" | "58", Some("2")) => 5,
1655 _ => 1,
1656 };
1657 let end = (i + take).min(tokens.len());
1658 let group = tokens[i..end].join(";");
1659 let n: u16 = token.parse().unwrap_or(0);
1660 let (slot, colour) = match n {
1661 30..=39 | 90..=97 => (&mut sgr.fg, true),
1662 40..=49 | 100..=107 => (&mut sgr.bg, true),
1663 _ => (&mut sgr.rest, false),
1664 };
1665 if colour {
1666 slot.clear();
1668 }
1669 if !slot.is_empty() {
1670 slot.push(';');
1671 }
1672 slot.push_str(&group);
1673 i = end;
1674 }
1675 sgr
1676 }
1677
1678 fn set_rgb(&mut self, fg: Option<Rgb>, bg: Option<Rgb>) {
1680 self.fg.clear();
1681 self.bg.clear();
1682 self.rest.clear();
1683 self.full.clear();
1684 if let Some(fg) = fg {
1685 rgb_code(&mut self.fg, "38;2;", fg);
1686 }
1687 if let Some(bg) = bg {
1688 rgb_code(&mut self.bg, "48;2;", bg);
1689 }
1690 self.full.push_str(&self.fg);
1691 if !self.fg.is_empty() && !self.bg.is_empty() {
1692 self.full.push(';');
1693 }
1694 self.full.push_str(&self.bg);
1695 }
1696
1697 fn write_from(&self, from: Option<&Sgr>, out: &mut String) {
1701 let full_len = 4 + if self.full.is_empty() {
1702 0
1703 } else {
1704 1 + self.full.len()
1705 };
1706 if let Some(from) = from {
1707 if from.full == self.full {
1708 return;
1709 }
1710 if from.rest == self.rest {
1711 let part = |now: &str, was: &str, default: usize| match (now == was, now.is_empty())
1712 {
1713 (true, _) => 0,
1714 (false, true) => default,
1715 (false, false) => now.len(),
1716 };
1717 let fg = part(&self.fg, &from.fg, 2);
1718 let bg = part(&self.bg, &from.bg, 2);
1719 let delta = 3 + fg + bg + usize::from(fg > 0 && bg > 0);
1720 if delta < full_len {
1721 out.push_str("\x1b[");
1722 if fg > 0 {
1723 out.push_str(if self.fg.is_empty() { "39" } else { &self.fg });
1724 }
1725 if fg > 0 && bg > 0 {
1726 out.push(';');
1727 }
1728 if bg > 0 {
1729 out.push_str(if self.bg.is_empty() { "49" } else { &self.bg });
1730 }
1731 out.push('m');
1732 return;
1733 }
1734 }
1735 }
1736 out.push_str("\x1b[0");
1737 if !self.full.is_empty() {
1738 out.push(';');
1739 out.push_str(&self.full);
1740 }
1741 out.push('m');
1742 }
1743}
1744
1745impl Painter {
1746 pub fn new(system: Option<ColorSystem>) -> Painter {
1751 Painter {
1752 previous: None,
1753 system,
1754 codes: Vec::new(),
1755 scratch: Sgr::default(),
1756 rgb_codes: RgbCodes::default(),
1757 changes: SgrChanges::default(),
1758 inline: false,
1759 cursor: None,
1760 height: 0,
1761 force: false,
1762 written: Vec::new(),
1763 stats: PaintStats::default(),
1764 strategy: PaintStrategy::Cells,
1768 erase: true,
1769 scroll: !cfg!(windows),
1771 fixed: Vec::new(),
1772 found: None,
1773 }
1774 }
1775
1776 pub fn set_scroll(&mut self, on: bool) {
1781 self.scroll = on;
1782 }
1783
1784 pub fn set_fixed(&mut self, rects: &[Rect]) {
1787 self.fixed.clear();
1788 self.fixed.extend_from_slice(rects);
1789 }
1790
1791 pub fn set_erase(&mut self, on: bool) {
1796 self.erase = on;
1797 }
1798
1799 pub fn set_strategy(&mut self, strategy: PaintStrategy) {
1801 self.strategy = strategy;
1802 }
1803
1804 pub fn strategy(&self) -> PaintStrategy {
1806 self.strategy
1807 }
1808
1809 pub fn stats(&self) -> PaintStats {
1812 self.stats
1813 }
1814
1815 #[must_use]
1819 pub fn inline(mut self) -> Painter {
1820 self.inline = true;
1821 self
1822 }
1823
1824 pub fn set_color_system(&mut self, system: Option<ColorSystem>) {
1825 if system != self.system {
1826 self.system = system;
1827 self.codes.clear();
1828 self.changes.clear();
1829 }
1830 }
1831
1832 pub fn move_to(&mut self, x: u16, y: u16) -> String {
1834 let out = self.goto(self.cursor, x, y);
1835 if self.inline {
1836 self.cursor = Some((x, y));
1837 }
1838 out
1839 }
1840
1841 pub fn finish(&mut self) -> String {
1845 if !self.inline || self.cursor.is_none() {
1846 return String::new();
1847 }
1848 let out = self.move_to(0, self.height.saturating_sub(1));
1849 self.cursor = None;
1850 format!("{out}\x1b[0m\r\n")
1851 }
1852
1853 fn goto(&self, from: Option<(u16, u16)>, x: u16, y: u16) -> String {
1854 let mut out = String::new();
1855 self.goto_into(from, x, y, u16::MAX, &mut out);
1856 out
1857 }
1858
1859 fn goto_into(&self, from: Option<(u16, u16)>, x: u16, y: u16, width: u16, out: &mut String) {
1865 if self.inline {
1866 let (_, from_y) = from.unwrap_or((0, 0));
1867 if y < from_y {
1868 csi(out, from_y - y, 'A');
1869 } else if y > from_y {
1870 csi(out, y - from_y, 'B');
1871 }
1872 out.push('\r');
1875 if x > 0 {
1876 csi(out, x, 'C');
1877 }
1878 return;
1879 }
1880 let cup = if x == 0 && y == 0 {
1881 3
1882 } else {
1883 3 + digits(y + 1) + if x > 0 { 1 + digits(x + 1) } else { 0 }
1884 };
1885 let Some((fx, fy)) = from else {
1886 return push_cup(out, x, y);
1887 };
1888 let step = |n: u16| if n == 1 { 3 } else { 3 + digits(n) };
1889 let vertical = match y.cmp(&fy) {
1890 std::cmp::Ordering::Equal => 0,
1891 std::cmp::Ordering::Greater => step(y - fy),
1892 std::cmp::Ordering::Less => step(fy - y),
1893 };
1894 let wrapping = fx >= width;
1895 let along = if wrapping || x == fx {
1898 usize::MAX
1899 } else if x > fx {
1900 step(x - fx)
1901 } else {
1902 step(fx - x)
1903 };
1904 let along = if x == fx && !wrapping { 0 } else { along };
1905 let from_edge = 1 + if x > 0 { step(x) } else { 0 };
1906 let crlf = x == 0 && y == fy + 1;
1907 let relative = vertical.saturating_add(along.min(from_edge));
1908 if crlf && 2 < cup && 2 <= relative {
1909 out.push_str("\r\n");
1910 return;
1911 }
1912 if relative >= cup {
1913 return push_cup(out, x, y);
1914 }
1915 if along > from_edge {
1916 out.push('\r');
1917 }
1918 match y.cmp(&fy) {
1919 std::cmp::Ordering::Greater => csi(out, y - fy, 'B'),
1920 std::cmp::Ordering::Less => csi(out, fy - y, 'A'),
1921 std::cmp::Ordering::Equal => {}
1922 }
1923 if along <= from_edge {
1924 if x > fx {
1925 csi(out, x - fx, 'C');
1926 } else if x < fx {
1927 csi(out, fx - x, 'D');
1928 }
1929 } else if x > 0 {
1930 csi(out, x, 'C');
1931 }
1932 }
1933
1934 fn reserve(&mut self, height: u16) -> String {
1938 let mut out = String::from("\x1b[0m");
1939 if let Some((_, y)) = self.cursor
1940 && y > 0
1941 {
1942 out.push_str(&format!("\x1b[{y}A"));
1943 }
1944 out.push('\r');
1945 if height > 1 {
1946 out.push_str(&"\n".repeat(height as usize - 1));
1947 out.push_str(&format!("\x1b[{}A", height - 1));
1948 }
1949 out.push_str("\x1b[J");
1950 self.cursor = Some((0, 0));
1951 self.height = height;
1952 out
1953 }
1954
1955 pub fn invalidate(&mut self) {
1957 self.previous = None;
1958 }
1959
1960 pub fn remap_styles(&mut self, remap: &StyleRemap) {
1965 if let Some(previous) = &mut self.previous {
1966 for cell in &mut previous.cells {
1967 cell.style = remap.get_or_gone(cell.style);
1968 }
1969 }
1970 let mut codes = Vec::with_capacity(self.codes.len());
1971 for (old, code) in self.codes.drain(..).enumerate() {
1972 let Some(code) = code else { continue };
1973 if let Some(new) = remap.get(StyleId::from_index(old)) {
1974 if codes.len() <= new.index() {
1975 codes.resize(new.index() + 1, None);
1976 }
1977 codes[new.index()] = Some(code);
1978 }
1979 }
1980 self.codes = codes;
1981 self.rgb_codes.clear();
1982 self.changes.clear();
1983 }
1984
1985 pub fn compact(&mut self, screen: &mut Screen) -> bool {
2012 if !screen.styles_piled_up() {
2013 return false;
2014 }
2015 let remap = screen.compact_styles();
2016 self.remap_styles(&remap);
2017 true
2018 }
2019
2020 pub fn paint(&mut self, screen: &Screen, damage: &[Rect]) -> String {
2024 let mut out = String::new();
2025 self.paint_into(screen, damage, &mut out);
2026 out
2027 }
2028
2029 pub fn paint_into(&mut self, screen: &Screen, damage: &[Rect], out: &mut String) {
2032 self.written.clear();
2033 self.stats = PaintStats::default();
2034 let base = out.len();
2035 let first = !matches!(&self.previous, Some(p) if p.width == screen.width && p.height == screen.height);
2036 let mut rects: Vec<Rect> = if first {
2037 if self.inline {
2038 out.push_str(&self.reserve(screen.height));
2039 } else {
2040 out.push_str("\x1b[0m\x1b[H\x1b[2J");
2041 }
2042 self.previous = None;
2043 self.codes.clear();
2044 self.rgb_codes.clear();
2045 self.changes.clear();
2046 vec![screen.area()]
2047 } else {
2048 let wide = u16::from(!self.force);
2052 damage
2053 .iter()
2054 .map(|r| {
2055 let x = r.x.saturating_sub(wide);
2056 let right = r.right().saturating_add(wide);
2057 Rect::new(x, r.y, right - x, r.height).intersection(screen.area())
2058 })
2059 .filter(|r| !r.is_empty())
2060 .collect()
2061 };
2062 let mut pen = Pen {
2063 at: self.cursor,
2064 ..Pen::default()
2065 };
2066 let mut strategy = match self.strategy {
2067 _ if first || self.force => PaintStrategy::Cells,
2068 PaintStrategy::Best => self.choose(screen, &rects),
2069 other => other,
2070 };
2071 if strategy == PaintStrategy::Scroll {
2072 let found = match self.found.take() {
2073 Some(shift) => Some(shift),
2074 None => self.find_shift(screen, &rects),
2075 };
2076 match found {
2077 Some(shift) => {
2078 let before = out.len();
2079 self.scroll_by(shift, screen, &mut pen, out);
2080 self.stats.other_bytes += out.len() - before;
2081 rects.push(Rect::new(
2082 0,
2083 shift.top,
2084 screen.width,
2085 shift.bottom - shift.top,
2086 ));
2087 }
2088 None => strategy = PaintStrategy::Cells,
2089 }
2090 }
2091 let forced = self.force;
2092 if strategy == PaintStrategy::Full {
2093 rects = vec![screen.area()];
2094 self.force = true;
2095 }
2096 for (y, spans) in row_spans(&rects, screen.height) {
2097 if strategy == PaintStrategy::Rows {
2098 self.paint_row(screen, y, &mut pen, out);
2099 continue;
2100 }
2101 for (start, end) in spans {
2102 if !self.force
2105 && let Some(previous) = &self.previous
2106 {
2107 let width = screen.width as usize;
2108 let row = y as usize * width;
2109 let span = row + start as usize..row + end as usize;
2110 if previous.cells[span.clone()] == screen.cells[span] {
2111 self.stats.compared += usize::from(end - start);
2112 continue;
2113 }
2114 }
2115 self.paint_span(screen, y, start, end, &mut pen, out);
2116 }
2117 }
2118 self.force = forced;
2119 let before = out.len();
2120 if pen.link.is_some() {
2121 out.push_str("\x1b]8;;\x1b\\");
2122 }
2123 if pen.rgb.is_some()
2124 || held_codes(&self.codes, pen.held).is_some_and(|c| !c.full.is_empty())
2125 {
2126 out.push_str("\x1b[0m");
2127 }
2128 self.stats.style_bytes += out.len() - before;
2129 let stats = &mut self.stats;
2130 stats.strategy = strategy;
2131 stats.elided = stats.compared.saturating_sub(stats.written);
2132 stats.other_bytes =
2133 out.len() - base - stats.move_bytes - stats.style_bytes - stats.text_bytes;
2134 if self.inline {
2135 self.cursor = pen.at.or(self.cursor);
2136 }
2137 match &mut self.previous {
2139 None => {
2140 self.previous = Some(Sent {
2141 width: screen.width,
2142 height: screen.height,
2143 cells: screen.cells.clone(),
2144 })
2145 }
2146 Some(previous) => {
2147 let width = screen.width as usize;
2148 for rect in &rects {
2149 for y in rect.y..rect.bottom() {
2150 let row = y as usize * width;
2151 let span = row + rect.x as usize..row + rect.right() as usize;
2152 previous.cells[span.clone()].copy_from_slice(&screen.cells[span]);
2153 }
2154 }
2155 }
2156 }
2157 }
2158
2159 fn choose(&mut self, screen: &Screen, rects: &[Rect]) -> PaintStrategy {
2171 self.found = self.find_shift(screen, rects);
2172 match self.found {
2173 Some(_) => PaintStrategy::Scroll,
2174 None => PaintStrategy::Cells,
2175 }
2176 }
2177
2178 fn paint_row(&mut self, screen: &Screen, y: u16, pen: &mut Pen, out: &mut String) {
2181 let Some(previous) = &self.previous else {
2182 return;
2183 };
2184 let (sent, now) = (previous.row(y), screen.row(y));
2185 let width = screen.width as usize;
2186 self.stats.compared += width;
2187 let Some(first) = (0..width).find(|&x| sent[x] != now[x]) else {
2188 return;
2189 };
2190 let last = (0..width).rfind(|&x| sent[x] != now[x]).unwrap_or(first);
2191 let end = (last + 1 + usize::from(now.get(last).is_some_and(|c| c.width >= 2))).min(width);
2193 let forced = self.force;
2194 self.force = true;
2195 let compared = self.stats.compared;
2196 self.paint_span(screen, y, first as u16, end as u16, pen, out);
2197 self.stats.compared = compared;
2199 self.force = forced;
2200 }
2201
2202 fn find_shift(&mut self, screen: &Screen, rects: &[Rect]) -> Option<Shift> {
2207 if !self.scroll || self.inline {
2208 return None;
2209 }
2210 let previous = self.previous.as_ref()?;
2211 let (top, bottom) = rects
2212 .iter()
2213 .fold((u16::MAX, 0), |(t, b), r| (t.min(r.y), b.max(r.bottom())));
2214 if bottom <= top || bottom - top < 3 {
2215 return None;
2216 }
2217 let height = screen.height;
2218 let same = |y: u16, source: u16| screen.row(y) == previous.row(source);
2221 let mut votes: Vec<(i32, u16)> = Vec::new();
2225 let mut vote = |by: i32, n: u16| match votes.iter_mut().find(|(b, _)| *b == by) {
2226 Some((_, count)) => *count += n,
2227 None => votes.push((by, n)),
2228 };
2229 for &(_, rows) in &screen.scrolled {
2230 vote(i32::from(rows), height);
2231 }
2232 let mut damaged = vec![false; height as usize];
2234 for rect in rects {
2235 for y in rect.y..rect.bottom().min(height) {
2236 damaged[y as usize] = true;
2237 }
2238 }
2239 let mut changed = 0;
2240 let mut found: Vec<u16> = Vec::new();
2241 for y in (top..bottom).filter(|&y| damaged[y as usize]) {
2242 if same(y, y) {
2243 continue;
2244 }
2245 changed += 1;
2246 found.clear();
2247 'look: for d in 1..=SCROLL_WINDOW.min(height) {
2248 for source in [y.checked_add(d), y.checked_sub(d)].into_iter().flatten() {
2249 if source < height && same(y, source) {
2250 found.push(source);
2251 if found.len() > 8 {
2254 break 'look;
2255 }
2256 }
2257 }
2258 }
2259 if found.len() <= 8 {
2260 for &source in &found {
2261 vote(i32::from(source) - i32::from(y), 1);
2262 }
2263 }
2264 if changed >= 6 {
2265 break;
2266 }
2267 }
2268 if changed < 3 || votes.is_empty() {
2269 return None;
2270 }
2271 let mut best: Option<Shift> = None;
2272 let consider = |by: i32, best: &mut Option<Shift>| {
2273 let mut run_start = None;
2275 let mut y = 0i32;
2276 while y <= height as i32 {
2277 let source = y + by;
2278 let matches = y < height as i32
2279 && (0..height as i32).contains(&source)
2280 && same(y as u16, source as u16);
2281 match (matches, run_start) {
2282 (true, None) => run_start = Some(y),
2283 (false, Some(start)) => {
2284 run_start = None;
2285 let gain = (start..y).filter(|&r| !same(r as u16, r as u16)).count() as u16;
2286 let (top, bottom) = if by > 0 {
2287 (start as u16, (y + by) as u16)
2288 } else {
2289 ((start + by) as u16, y as u16)
2290 };
2291 if best.is_none_or(|b| gain > b.gain) {
2292 *best = Some(Shift {
2293 top,
2294 bottom,
2295 by,
2296 gain,
2297 });
2298 }
2299 }
2300 _ => {}
2301 }
2302 y += 1;
2303 }
2304 };
2305 votes.sort_unstable_by_key(|&(by, n)| (std::cmp::Reverse(n), by.abs()));
2306 for &(by, _) in votes.iter().take(3) {
2307 consider(by, &mut best);
2308 }
2309 let shift = best?;
2310 if shift.gain < 2 || i32::from(shift.gain) <= shift.by.abs() {
2313 return None;
2314 }
2315 let region = Rect::new(0, shift.top, screen.width, shift.bottom - shift.top);
2317 if self.fixed.iter().any(|r| r.intersects(region)) {
2318 return None;
2319 }
2320 Some(shift)
2321 }
2322
2323 fn scroll_by(&mut self, shift: Shift, screen: &Screen, pen: &mut Pen, out: &mut String) {
2328 out.push_str("\x1b[0m");
2329 pen.held = Held::Reset;
2330 pen.rgb = None;
2331 pen.style = None;
2332 let whole = shift.top == 0 && shift.bottom == screen.height;
2333 if !whole {
2334 out.push_str("\x1b[");
2335 push_u16(out, shift.top + 1);
2336 out.push(';');
2337 push_u16(out, shift.bottom);
2338 out.push('r');
2339 }
2340 let n = shift.by.unsigned_abs() as u16;
2341 csi(out, n, if shift.by > 0 { 'S' } else { 'T' });
2342 if !whole {
2343 out.push_str("\x1b[r");
2344 }
2345 pen.at = None;
2347 if let Some(previous) = &mut self.previous {
2348 previous.shift(shift.top, shift.bottom, shift.by);
2349 }
2350 }
2351
2352 pub fn written(&self) -> &[Rect] {
2356 &self.written
2357 }
2358
2359 pub fn repaint(&mut self, screen: &Screen, rects: &[Rect]) -> String {
2363 self.force = true;
2364 let out = self.paint(screen, rects);
2365 self.force = false;
2366 out
2367 }
2368
2369 fn paint_span(
2370 &mut self,
2371 screen: &Screen,
2372 y: u16,
2373 start: u16,
2374 end: u16,
2375 pen: &mut Pen,
2376 out: &mut String,
2377 ) {
2378 let mut x = start;
2379 if x > 0 && screen.cell(x, y).is_continuation() {
2382 x -= 1;
2383 }
2384 while x < end {
2385 if !self.force
2387 && let Some(previous) = &self.previous
2388 {
2389 let row = y as usize * screen.width as usize;
2390 let (from, to) = (row + x as usize, row + end as usize);
2391 let same = previous.cells[from..to]
2392 .iter()
2393 .zip(&screen.cells[from..to])
2394 .take_while(|(sent, now)| sent == now)
2395 .count();
2396 self.stats.compared += same;
2397 x += same as u16;
2398 if x >= end {
2399 break;
2400 }
2401 }
2402 let mut x0 = x;
2403 if screen.cell(x0, y).is_continuation() && x0 > 0 {
2404 x0 -= 1;
2405 }
2406 if self.erase && !self.inline && self.plain_blank(screen, x0, y) {
2410 if let Some((fx, fy)) = pen.at
2413 && fy == y
2414 && fx < x0
2415 && (fx..x0).all(|gx| self.plain_blank(screen, gx, y))
2416 {
2417 self.stats.compared += usize::from(x0 - fx);
2418 x = fx;
2419 x0 = fx;
2420 }
2421 let mut run = 1;
2422 while x0 + run < end && self.plain_blank(screen, x0 + run, y) {
2423 run += 1;
2424 }
2425 let to_edge = x0 + run == screen.width;
2426 let erase = if to_edge { 3 } else { 3 + digits(run) };
2427 if usize::from(run) > erase {
2428 self.move_pen(screen, x0, y, pen, out);
2429 let before = out.len();
2430 self.set_pen(screen, StyleId::NONE, pen, out);
2431 self.stats.style_bytes += out.len() - before;
2432 if to_edge {
2433 out.push_str("\x1b[K");
2434 } else {
2435 csi(out, run, 'X');
2436 }
2437 self.stats.compared += usize::from(x0 + run - x);
2438 self.stats.written += usize::from(run);
2439 self.note_written(x0, y, x0 + run);
2440 x = x0 + run;
2441 continue;
2442 }
2443 }
2444 self.move_pen(screen, x0, y, pen, out);
2445 let cell = screen.cell(x0, y);
2446 let before = out.len();
2447 self.set_pen(screen, cell.style, pen, out);
2448 self.stats.style_bytes += out.len() - before;
2449 let several = match cell.single_char() {
2453 Some(c) => {
2454 out.push(c);
2455 self.stats.text_bytes += c.len_utf8();
2456 c == '\u{200d}' || c == '\u{fe0f}'
2457 }
2458 None => {
2459 let text = cell.text();
2460 out.push_str(text);
2461 self.stats.text_bytes += text.len();
2462 cell.len == LONG || text.contains(['\u{200d}', '\u{fe0f}'])
2463 }
2464 };
2465 let next = x0 + cell.width.max(1) as u16;
2466 let columns = usize::from(next - x0);
2467 self.stats.compared += usize::from(next - x);
2468 self.stats.written += columns;
2469 self.note_written(x0, y, next);
2470 pen.at = (!several).then_some((next, y));
2471 x = next;
2472 }
2473 }
2474
2475 fn note_written(&mut self, x: u16, y: u16, next: u16) {
2477 match self.written.last_mut() {
2478 Some(run) if run.y == y && run.right() == x => run.width = next - run.x,
2479 _ => self.written.push(Rect::new(x, y, next - x, 1)),
2480 }
2481 }
2482
2483 fn move_pen(&mut self, screen: &Screen, x: u16, y: u16, pen: &mut Pen, out: &mut String) {
2487 if pen.at == Some((x, y)) {
2488 return;
2489 }
2490 let before = out.len();
2491 self.goto_into(pen.at, x, y, screen.width, out);
2492 let jump = out.len() - before;
2493 if let Some((fx, fy)) = pen.at.filter(|&(fx, fy)| {
2497 !self.inline && fy == y && fx < x && x - fx <= 8 && jump > usize::from(x - fx)
2498 }) {
2499 let mut cost = 0;
2500 let mut style = pen.style;
2501 for gx in fx..x {
2502 let cell = screen.cell(gx, fy);
2503 if cell.width != 1 || cell.len == LONG || cell.len == 0 {
2504 cost = usize::MAX;
2505 break;
2506 }
2507 cost += cell.len as usize;
2508 if style != Some(cell.style) {
2509 style = Some(cell.style);
2510 cost += match screen.styles.rgb(cell.style) {
2512 Some(_) => 40,
2513 None if cell.style == StyleId::NONE => 4,
2514 None => 4 + self.sgr(screen, cell.style).full.len(),
2515 };
2516 }
2517 if cost >= jump {
2518 break;
2519 }
2520 }
2521 if style != pen.style && cost < jump {
2522 cost += 8;
2524 }
2525 if cost < jump {
2526 out.truncate(before);
2527 for gx in fx..x {
2528 let cell = *screen.cell(gx, y);
2529 self.set_pen(screen, cell.style, pen, out);
2530 out.push_str(cell.text());
2531 }
2532 }
2533 }
2534 self.stats.move_bytes += out.len() - before;
2535 pen.at = Some((x, y));
2536 }
2537
2538 fn plain_blank(&mut self, screen: &Screen, x: u16, y: u16) -> bool {
2541 let cell = screen.cell(x, y);
2542 if cell.len != 1 || cell.bytes[0] != b' ' || cell.width != 1 {
2543 return false;
2544 }
2545 if cell.style == StyleId::NONE {
2546 return true;
2547 }
2548 let Some(style) = screen.style(cell.style) else {
2549 return true;
2550 };
2551 style.link().is_none() && self.sgr(screen, cell.style).full.is_empty()
2554 }
2555
2556 fn sgr(&mut self, screen: &Screen, style: StyleId) -> &Sgr {
2558 if self.codes.len() <= style.index() {
2559 self.codes.resize(style.index() + 1, None);
2560 }
2561 let system = self.system;
2562 self.codes[style.index()].get_or_insert_with(|| match (screen.style(style), system) {
2563 (Some(s), Some(system)) => Sgr::parse(&s.ansi_codes(system)),
2564 _ => Sgr::default(),
2565 })
2566 }
2567
2568 fn set_pen(&mut self, screen: &Screen, style: StyleId, pen: &mut Pen, out: &mut String) {
2569 if pen.style == Some(style) {
2570 return;
2571 }
2572 if pen.link.is_none()
2575 && let (Some(pair), Some(ColorSystem::Truecolor), Some(old)) =
2576 (screen.styles.rgb(style), self.system, pen.rgb)
2577 {
2578 if old != pair {
2579 rgb_delta(old, pair, self.rgb_codes.get(style, pair), out);
2580 }
2581 pen.rgb = Some(pair);
2582 pen.style = Some(style);
2583 return;
2584 }
2585 let resolved = screen.style(style);
2586 let link = resolved
2589 .and_then(|s| s.link())
2590 .filter(|url| !url.chars().any(|c| super::control_picture(c).is_some()));
2591 if link != pen.link.as_deref() {
2592 match link {
2593 Some(url) => {
2594 out.push_str("\x1b]8;;");
2595 out.push_str(url);
2596 out.push_str("\x1b\\");
2597 }
2598 None => out.push_str("\x1b]8;;\x1b\\"),
2599 }
2600 pen.link = link.map(str::to_string);
2601 }
2602 if let (Some(pair), Some(ColorSystem::Truecolor)) = (screen.styles.rgb(style), self.system)
2603 {
2604 match pen.rgb {
2608 Some(old) if old == pair => {}
2609 Some(old) => rgb_delta(old, pair, self.rgb_codes.get(style, pair), out),
2610 None => {
2611 self.scratch.set_rgb(pair.0, pair.1);
2612 self.scratch
2613 .write_from(held_codes(&self.codes, pen.held), out);
2614 }
2615 }
2616 pen.rgb = Some(pair);
2617 pen.style = Some(style);
2618 return;
2619 }
2620 if let (None, Held::Style(held)) = (pen.rgb, pen.held) {
2621 if let Some(change) = self.changes.get(held, style) {
2623 out.push_str(change);
2624 pen.held = Held::Style(style);
2625 pen.style = Some(style);
2626 return;
2627 }
2628 self.sgr(screen, style);
2629 let codes = self.codes[style.index()].as_ref().expect("cached above");
2630 let before = out.len();
2631 codes.write_from(held_codes(&self.codes, pen.held), out);
2632 self.changes.put(held, style, &out[before..]);
2633 pen.held = Held::Style(style);
2634 pen.style = Some(style);
2635 return;
2636 }
2637 self.sgr(screen, style);
2638 let codes = self.codes[style.index()].as_ref().expect("cached above");
2639 let from = match pen.rgb.take() {
2640 Some((fg, bg)) => {
2641 self.scratch.set_rgb(fg, bg);
2643 Some(&self.scratch)
2644 }
2645 None => held_codes(&self.codes, pen.held),
2646 };
2647 codes.write_from(from, out);
2650 pen.held = Held::Style(style);
2651 pen.style = Some(style);
2652 }
2653}
2654
2655fn rgb_delta(
2659 old: (Option<Rgb>, Option<Rgb>),
2660 new: (Option<Rgb>, Option<Rgb>),
2661 sgr: RgbSgr<'_>,
2662 out: &mut String,
2663) {
2664 let fg = old.0 != new.0;
2665 let bg = old.1 != new.1;
2666 if fg && bg && new.0.is_some() && new.1.is_some() {
2667 out.push_str(sgr.whole);
2668 return;
2669 }
2670 out.push_str("\x1b[");
2671 let mut first = true;
2672 for (changed, now, code, default) in [(fg, new.0, sgr.fg, "39"), (bg, new.1, sgr.bg, "49")] {
2673 if !changed {
2674 continue;
2675 }
2676 if !first {
2677 out.push(';');
2678 }
2679 first = false;
2680 out.push_str(if now.is_some() { code } else { default });
2681 }
2682 out.push('m');
2683}
2684
2685fn csi(out: &mut String, n: u16, c: char) {
2687 out.push_str("\x1b[");
2688 if n != 1 {
2689 push_u16(out, n);
2690 }
2691 out.push(c);
2692}
2693
2694fn push_cup(out: &mut String, x: u16, y: u16) {
2697 out.push_str("\x1b[");
2698 if x > 0 || y > 0 {
2699 push_u16(out, y + 1);
2700 if x > 0 {
2701 out.push(';');
2702 push_u16(out, x + 1);
2703 }
2704 }
2705 out.push('H');
2706}
2707
2708fn digits(n: u16) -> usize {
2710 match n {
2711 0..=9 => 1,
2712 10..=99 => 2,
2713 100..=999 => 3,
2714 1000..=9999 => 4,
2715 _ => 5,
2716 }
2717}
2718
2719fn push_u16(out: &mut String, n: u16) {
2721 let mut buffer = [0u8; 5];
2722 let mut at = buffer.len();
2723 let mut n = n;
2724 loop {
2725 at -= 1;
2726 buffer[at] = b'0' + (n % 10) as u8;
2727 n /= 10;
2728 if n == 0 {
2729 break;
2730 }
2731 }
2732 for &b in &buffer[at..] {
2733 out.push(char::from(b));
2734 }
2735}
2736
2737fn rgb_code(out: &mut String, lead: &str, rgb: Rgb) {
2739 out.push_str(lead);
2740 for (i, part) in rgb.into_iter().enumerate() {
2741 if i > 0 {
2742 out.push(';');
2743 }
2744 push_u8(out, part);
2745 }
2746}
2747
2748fn push_u8(out: &mut String, n: u8) {
2750 if n >= 100 {
2751 out.push(char::from(b'0' + n / 100));
2752 }
2753 if n >= 10 {
2754 out.push(char::from(b'0' + n / 10 % 10));
2755 }
2756 out.push(char::from(b'0' + n % 10));
2757}
2758
2759#[derive(Default)]
2764struct SgrChanges {
2765 text: String,
2766 at: HashMap<(u32, u32), (u32, u32), FxBuild>,
2767}
2768
2769impl SgrChanges {
2770 const MAX: usize = 1 << 16;
2771
2772 fn clear(&mut self) {
2773 self.text.clear();
2774 self.at.clear();
2775 }
2776
2777 fn get(&self, from: StyleId, to: StyleId) -> Option<&str> {
2778 let &(start, end) = self.at.get(&(from.index() as u32, to.index() as u32))?;
2779 self.text.get(start as usize..end as usize)
2780 }
2781
2782 fn put(&mut self, from: StyleId, to: StyleId, change: &str) {
2783 if self.text.len() + change.len() > SgrChanges::MAX {
2784 self.clear();
2785 }
2786 let start = self.text.len() as u32;
2787 self.text.push_str(change);
2788 let key = (from.index() as u32, to.index() as u32);
2789 self.at.insert(key, (start, self.text.len() as u32));
2790 }
2791}
2792
2793#[derive(Clone, Copy)]
2797struct RgbAt {
2798 at: u32,
2799 fg: u8,
2800 bg: u8,
2801}
2802
2803#[derive(Default)]
2809struct RgbCodes {
2810 text: String,
2811 at: Vec<Option<RgbAt>>,
2812}
2813
2814struct RgbSgr<'a> {
2817 whole: &'a str,
2818 fg: &'a str,
2819 bg: &'a str,
2820}
2821
2822impl RgbCodes {
2823 fn clear(&mut self) {
2824 self.text.clear();
2825 self.at.clear();
2826 }
2827
2828 fn get(&mut self, id: StyleId, pair: (Option<Rgb>, Option<Rgb>)) -> RgbSgr<'_> {
2830 if self.at.len() <= id.index() {
2831 self.at.resize(id.index() + 1, None);
2832 }
2833 let at = match self.at[id.index()] {
2834 Some(at) => at,
2835 None => {
2836 let start = self.text.len();
2837 self.text.push_str("\x1b[");
2838 if let Some(fg) = pair.0 {
2839 rgb_code(&mut self.text, "38;2;", fg);
2840 }
2841 let middle = self.text.len();
2842 if pair.0.is_some() && pair.1.is_some() {
2843 self.text.push(';');
2844 }
2845 let second = self.text.len();
2846 if let Some(bg) = pair.1 {
2847 rgb_code(&mut self.text, "48;2;", bg);
2848 }
2849 let end = self.text.len();
2850 self.text.push('m');
2851 let at = RgbAt {
2852 at: start as u32,
2853 fg: (middle - start - 2) as u8,
2854 bg: (end - second) as u8,
2855 };
2856 self.at[id.index()] = Some(at);
2857 at
2858 }
2859 };
2860 let start = at.at as usize;
2861 let fg = start + 2..start + 2 + at.fg as usize;
2862 let joint = usize::from(at.fg > 0 && at.bg > 0);
2863 let bg = fg.end + joint..fg.end + joint + at.bg as usize;
2864 RgbSgr {
2865 whole: &self.text[start..bg.end + 1],
2866 fg: &self.text[fg],
2867 bg: &self.text[bg],
2868 }
2869 }
2870}
2871
2872fn row_spans(rects: &[Rect], height: u16) -> Vec<(u16, Vec<(u16, u16)>)> {
2874 let mut rows: Vec<Vec<(u16, u16)>> = vec![Vec::new(); height as usize];
2875 for rect in rects {
2876 for y in rect.y..rect.bottom().min(height) {
2877 rows[y as usize].push((rect.x, rect.right()));
2878 }
2879 }
2880 rows.into_iter()
2881 .enumerate()
2882 .filter(|(_, spans)| !spans.is_empty())
2883 .map(|(y, mut spans)| {
2884 spans.sort_unstable();
2885 let mut merged: Vec<(u16, u16)> = Vec::new();
2886 for (a, b) in spans {
2887 match merged.last_mut() {
2888 Some(last) if a <= last.1 => last.1 = last.1.max(b),
2889 _ => merged.push((a, b)),
2890 }
2891 }
2892 (y as u16, merged)
2893 })
2894 .collect()
2895}
2896
2897#[cfg(test)]
2898mod tests {
2899 use super::*;
2900
2901 fn line(markup: &str) -> Vec<Segment> {
2902 let console = rich::Console::builder()
2903 .width(80)
2904 .color_system(None)
2905 .build();
2906 let text = rich::Text::from_markup(markup).unwrap();
2907 console
2908 .render_lines(&text, &console.options(), false)
2909 .remove(0)
2910 }
2911
2912 #[test]
2913 fn lines_are_written_into_their_rectangle_only() {
2914 let mut screen = Screen::new(10, 3);
2915 screen.write_lines(
2916 Rect::new(0, 0, 10, 3),
2917 &[line("xxxxxxxxxx"), line("xxxxxxxxxx")],
2918 );
2919 screen.write_lines(Rect::new(2, 1, 4, 2), &[line("abcdefgh")]);
2920 assert_eq!(screen.plain(), ["xxxxxxxxxx", "xxabcdxxxx", " "]);
2921 }
2922
2923 #[test]
2924 fn wide_graphemes_take_two_cells_and_never_cross_the_edge() {
2925 let mut screen = Screen::new(5, 1);
2926 screen.write_lines(Rect::new(0, 0, 5, 1), &[line("日本語")]);
2927 assert_eq!(screen.plain(), ["日本 "]);
2928 assert!(screen.cell(1, 0).is_continuation());
2929 assert_eq!(screen.cell(4, 0).text(), " ");
2930 }
2931
2932 #[test]
2933 fn simple_lines_write_as_a_frame_would() {
2934 let pieces = [
2935 "ab",
2936 "日本",
2937 "●x",
2938 "⣿⠁",
2939 " ",
2940 "é",
2941 "a\u{301}",
2942 "\x1b",
2943 "─┐",
2944 "\u{301}x",
2945 "👨\u{200d}👩\u{200d}👧",
2946 "❤\u{fe0f}",
2947 "\u{200d}",
2948 "e\u{301}\u{302}",
2949 ];
2950 let styles = [
2951 None,
2952 Style::parse("bold red").ok(),
2953 Style::parse("on blue").ok(),
2954 ];
2955 let mut n = 7usize;
2956 for case in 0..1500 {
2957 let lines: Vec<Vec<Segment>> = (0..3)
2958 .map(|_| {
2959 (0..4)
2960 .map(|_| {
2961 n = n.wrapping_mul(1103515245).wrapping_add(12345) >> 3;
2962 let text = pieces[n % pieces.len()];
2963 Segment::new(text, styles[n / 9 % 3].clone())
2964 })
2965 .collect()
2966 })
2967 .collect();
2968 let rect = Rect::new((case % 3) as u16, 1, 5 + (case % 4) as u16, 3);
2969 let mut fast = Screen::new(12, 5);
2970 fast.write_lines(Rect::new(0, 0, 12, 5), &[line("日本語日本語")]);
2971 let mut slow = fast.clone();
2972 fast.write_lines(rect, &lines);
2973 slow.split_edges(rect.intersection(slow.area()));
2974 slow.write_frame_lines(rect.intersection(slow.area()), &lines);
2975 assert_eq!(fast.lines(), slow.lines(), "{lines:?} in {rect:?}");
2976 let widths = |s: &Screen| (0..12).map(|x| s.cell(x, 1).width).collect::<Vec<_>>();
2977 assert_eq!(widths(&fast), widths(&slow), "{lines:?}");
2978 }
2979 }
2980
2981 #[test]
2982 fn controls_in_text_become_visible() {
2983 let mut screen = Screen::new(6, 1);
2984 let evil = vec![Segment::new("a\x1b[2Jb", None)];
2985 screen.write_lines(Rect::new(0, 0, 6, 1), &[evil]);
2986 assert_eq!(screen.plain(), ["a␛[2Jb"]);
2987 }
2988
2989 #[test]
2990 fn the_painter_writes_only_changed_cells_inside_the_damage() {
2991 let mut screen = Screen::new(20, 2);
2992 let mut painter = Painter::new(Some(ColorSystem::Truecolor));
2993 screen.write_lines(screen.area(), &[line("tick 1"), line("[bold red]alert[/]")]);
2994 let first = painter.paint(&screen, &[]);
2995 assert!(first.contains("\x1b[2J") && first.contains("alert"));
2996 screen.write_lines(Rect::new(0, 0, 20, 1), &[line("tick 2")]);
2997 let second = painter.paint(&screen, &[Rect::new(0, 0, 20, 1)]);
2998 assert_eq!(second, "\x1b[1;6H\x1b[0m2");
3000 assert_eq!(painter.paint(&screen, &[]), "");
3002 }
3003
3004 #[test]
3005 fn styles_and_links_carry_across_a_run() {
3006 let mut screen = Screen::new(12, 1);
3007 let mut painter = Painter::new(Some(ColorSystem::Truecolor));
3008 painter.paint(&screen, &[]);
3009 screen.write_lines(
3010 screen.area(),
3011 &[line("[link=https://x.y]docs[/link] [b]ok[/b]")],
3012 );
3013 let out = painter.paint(&screen, &[screen.area()]);
3014 assert!(
3015 out.starts_with("\x1b[H\x1b]8;;https://x.y\x1b\\"),
3016 "{out:?}"
3017 );
3018 assert_eq!(out.matches("\x1b]8;;https://x.y").count(), 1, "{out:?}");
3020 assert!(
3021 out.contains("\x1b]8;;\x1b\\"),
3022 "the link is closed: {out:?}"
3023 );
3024 assert!(out.contains("\x1b[0;1mok"), "{out:?}");
3025 }
3026
3027 #[test]
3028 fn a_wide_grapheme_cut_by_the_destination_edge_becomes_a_space() {
3029 let mut source = Screen::new(4, 1);
3030 source.write_lines(source.area(), &[line("ab日")]);
3031 let mut narrow = Screen::new(3, 1);
3032 narrow.blit(&source, source.area(), 0, 0);
3033 assert_eq!(narrow.plain(), ["ab "]);
3034 assert_eq!(narrow.cell(2, 0).width, 1);
3035 }
3036
3037 #[test]
3038 fn the_painter_reports_the_runs_it_wrote() {
3039 let mut screen = Screen::new(10, 2);
3040 let mut painter = Painter::new(None);
3041 screen.write_lines(screen.area(), &[line("abc"), line("xyz")]);
3042 painter.paint(&screen, &[]);
3043 screen.write_lines(Rect::new(0, 1, 10, 1), &[line("xYz")]);
3044 painter.paint(&screen, &[Rect::new(0, 0, 10, 2)]);
3045 assert_eq!(painter.written(), [Rect::new(1, 1, 1, 1)]);
3046 }
3047
3048 #[test]
3049 fn a_wide_grapheme_replaced_by_narrow_text_clears_its_second_column() {
3050 let mut screen = Screen::new(4, 1);
3051 let mut painter = Painter::new(None);
3052 screen.write_lines(screen.area(), &[line("日x")]);
3053 painter.paint(&screen, &[]);
3054 screen.write_lines(screen.area(), &[line("abx")]);
3055 let out = painter.paint(&screen, &[screen.area()]);
3056 assert_eq!(out, "\x1b[H\x1b[0mab");
3057 }
3058
3059 struct Noise(u64);
3061
3062 impl Noise {
3063 fn rgb(&mut self) -> [u8; 3] {
3064 self.0 = self
3065 .0
3066 .wrapping_mul(6364136223846793005)
3067 .wrapping_add(1442695040888963407);
3068 let [r, g, b, _] = ((self.0 >> 33) as u32).to_le_bytes();
3069 [r, g, b]
3070 }
3071 }
3072
3073 fn noise_frame(screen: &mut Screen, noise: &mut Noise, segments: bool) {
3076 let area = screen.area();
3077 if segments {
3078 let lines: Vec<Vec<Segment>> = (0..area.height)
3079 .map(|_| {
3080 (0..area.width)
3081 .map(|_| {
3082 let [r, g, b] = noise.rgb();
3083 let [r2, g2, b2] = noise.rgb();
3084 let style = Style::from_color(
3085 Some(rich::Color::from_rgb(r, g, b)),
3086 Some(rich::Color::from_rgb(r2, g2, b2)),
3087 );
3088 Segment::new("▀", Some(style))
3089 })
3090 .collect()
3091 })
3092 .collect();
3093 screen.write_lines(area, &lines);
3094 } else {
3095 for y in 0..area.height {
3096 for x in 0..area.width {
3097 let (fg, bg) = (noise.rgb(), noise.rgb());
3098 screen.put_rgb(x, y, '▀', Some(fg), Some(bg));
3099 }
3100 }
3101 }
3102 }
3103
3104 #[test]
3105 fn the_style_table_stays_bounded_over_a_thousand_random_frames() {
3106 for segments in [false, true] {
3107 let mut screen = Screen::new(40, 12);
3108 let mut painter = Painter::new(Some(ColorSystem::Truecolor));
3109 let mut noise = Noise(1);
3110 let mut most = 0;
3111 for _ in 0..1000 {
3112 noise_frame(&mut screen, &mut noise, segments);
3113 painter.paint(&screen, &[screen.area()]);
3114 painter.compact(&mut screen);
3115 most = most.max(screen.style_count());
3116 }
3117 assert!(most <= 2 * 1024 + 1, "{most} styles ({segments})");
3120 assert!(painter.codes.len() <= most, "{}", painter.codes.len());
3121 }
3122 }
3123
3124 #[test]
3125 fn compacting_changes_no_byte_painted() {
3126 let mut kept = (
3129 Screen::new(30, 6),
3130 Painter::new(Some(ColorSystem::Truecolor)),
3131 );
3132 let mut compacted = (
3133 Screen::new(30, 6),
3134 Painter::new(Some(ColorSystem::Truecolor)),
3135 );
3136 let mut noise = Noise(9);
3137 for frame in 0..60 {
3138 let mut lines = vec![
3139 line("[bold]title[/] [link=https://x.y]docs[/link]"),
3140 line(&format!("[red]{frame}[/] [on blue] [/]")),
3141 ];
3142 if frame % 3 == 0 {
3143 lines.push(line("[italic green]every third[/]"));
3144 }
3145 let mut cells = Vec::new();
3146 for x in 0..30 {
3147 cells.push((x, noise.rgb(), noise.rgb()));
3148 }
3149 let underlay = Style::parse("on #202020").unwrap();
3150 let mut bytes = Vec::new();
3151 for (screen, painter) in [&mut kept, &mut compacted] {
3152 screen.write_lines(Rect::new(0, 0, 30, 3), &lines);
3153 for &(x, fg, bg) in &cells[..(frame % 30)] {
3155 screen.put_rgb(x, 4, '▀', Some(fg), Some(bg));
3156 }
3157 for x in 0..30 {
3158 screen.underlay(x, 1, &underlay);
3159 }
3160 bytes.push(painter.paint(screen, &[Rect::new(0, 0, 30, 5)]));
3161 }
3162 let remap = compacted.0.compact_styles();
3163 compacted.1.remap_styles(&remap);
3164 assert_eq!(bytes[0], bytes[1], "frame {frame}");
3165 assert_eq!(kept.0.plain(), compacted.0.plain());
3166 }
3167 assert!(compacted.0.style_count() < kept.0.style_count());
3168 }
3169
3170 #[test]
3171 fn a_cell_sent_in_a_dropped_style_reads_as_changed() {
3172 let mut screen = Screen::new(2, 1);
3173 let mut painter = Painter::new(Some(ColorSystem::Truecolor));
3174 screen.put_rgb(0, 0, 'a', Some([1, 2, 3]), None);
3175 painter.paint(&screen, &[]);
3176 screen.put_rgb(0, 0, 'a', Some([4, 5, 6]), None);
3178 let remap = screen.compact_styles();
3179 painter.remap_styles(&remap);
3180 let out = painter.paint(&screen, &[screen.area()]);
3181 assert_eq!(out, "\x1b[H\x1b[0;38;2;4;5;6ma\x1b[0m");
3182 }
3183
3184 #[test]
3185 fn pixel_cells_write_what_their_style_would() {
3186 let mut noise = Noise(3);
3187 for system in [
3188 ColorSystem::Truecolor,
3189 ColorSystem::EightBit,
3190 ColorSystem::Standard,
3191 ] {
3192 for case in 0..200 {
3193 let fg = (case % 3 != 0).then(|| noise.rgb());
3194 let bg = (case % 5 != 0).then(|| noise.rgb());
3195 let mut fast = Screen::new(1, 1);
3196 fast.put_rgb(0, 0, '▄', fg, bg);
3197 let mut slow = Screen::new(1, 1);
3198 let style = Style::from_color(
3199 fg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
3200 bg.map(|[r, g, b]| rich::Color::from_rgb(r, g, b)),
3201 );
3202 slow.write_lines(slow.area(), &[vec![Segment::new("▄", Some(style))]]);
3203 let paint = |screen: &Screen| Painter::new(Some(system)).paint(screen, &[]);
3204 assert_eq!(paint(&fast), paint(&slow), "{fg:?} {bg:?} {system:?}");
3205 }
3206 }
3207 }
3208
3209 #[test]
3210 fn a_pixel_cell_over_half_a_wide_grapheme_clears_the_other_half() {
3211 let mut screen = Screen::new(5, 1);
3212 screen.write_lines(screen.area(), &[line("日本x")]);
3213 screen.put_rgb(1, 0, '▀', Some([255, 0, 0]), None);
3214 screen.put_rgb(2, 0, '▀', None, Some([0, 0, 255]));
3215 assert_eq!(screen.plain(), [" ▀▀ x"]);
3216 assert!((0..5).all(|x| screen.cell(x, 0).width == 1));
3217 screen.put_rgb(0, 0, '日', None, None);
3219 screen.put_rgb(1, 0, '\u{301}', None, None);
3220 screen.put_rgb(2, 0, '\x1b', None, None);
3221 assert_eq!(screen.plain(), [" ␛ x"]);
3222 screen.put_rgb(5, 0, 'z', None, None);
3224 screen.put(0, 1, 'z', StyleId::NONE);
3225 assert_eq!(screen.plain(), [" ␛ x"]);
3226 }
3227
3228 #[test]
3229 fn the_pixel_glyphs_are_one_cell_wide() {
3230 let ranges = [
3231 (' ', '~'),
3232 ('\u{2500}', '\u{259f}'),
3233 ('\u{2800}', '\u{28ff}'),
3234 ];
3235 for (first, last) in ranges {
3236 for c in first..=last {
3237 assert_eq!(rich::cells::cell_len(&c.to_string()), 1, "{c:?}");
3238 }
3239 }
3240 }
3241
3242 #[test]
3243 fn layering_a_style_over_many_cells_combines_each_pair_once() {
3244 let mut screen = Screen::new(8, 1);
3245 screen.write_lines(screen.area(), &[line("[red]abcd[/][blue]efgh[/]")]);
3246 let before = screen.style_count();
3247 let under = Style::parse("on black").unwrap();
3248 for x in 0..8 {
3249 screen.underlay(x, 0, &under);
3250 }
3251 assert_eq!(screen.style_count(), before + 3);
3253 let style_of = |screen: &Screen, x| screen.style(screen.cell(x, 0).style).cloned();
3254 let parse = |s| Style::parse(s).ok();
3255 assert_eq!(
3256 (style_of(&screen, 0), style_of(&screen, 7)),
3257 (parse("red on black"), parse("blue on black"))
3258 );
3259 let over = Style::parse("bold").unwrap();
3260 screen.restyle(0, 0, &over);
3261 assert_eq!(style_of(&screen, 0), parse("bold red on black"));
3262 screen.compact_styles();
3263 screen.underlay(1, 0, &under);
3264 assert_eq!(style_of(&screen, 1), parse("red on black"));
3265 }
3266
3267 #[test]
3268 fn a_blit_translates_each_style_once() {
3269 let mut source = Screen::new(6, 1);
3270 source.write_lines(source.area(), &[line("[bold]ab[/][red]cd[/]ef")]);
3271 let mut target = Screen::new(6, 1);
3272 target.blit(&source, source.area(), 0, 0);
3273 assert_eq!(target.lines(), source.lines());
3274 assert_eq!(target.style_count(), source.style_count());
3275 }
3276}