1use std::collections::VecDeque;
20
21use tear_types::pane_snapshot::{CellAttrs, Color, ansi_256_color, default_ansi_palette};
22use tear_types::graphics::{Graphic, GraphicProtocol, GRAPHIC_PAYLOAD_MAX};
23use tear_types::host_role::{HostRole, TearCaps};
24use tear_types::modes::{
25 AltScreen, AutoWrap, BracketedPaste, CursorKeys, CursorVisible, FocusReporting, ModeSet,
26 MouseSgr, MouseTracking, SyncOutput,
27};
28use unicode_width::UnicodeWidthChar;
29use vte::{Params, Parser, Perform};
30
31pub use tear_types::pane_snapshot::{Cell, PaneSnapshot};
32
33pub const DEFAULT_SCROLLBACK_ROWS: usize = usize::MAX;
47
48pub struct PaneGrid {
53 parser: Parser,
54 pub(crate) state: GridState,
55 apc: ApcScanner,
67}
68
69#[derive(Debug, Default)]
77struct ApcScanner {
78 state: ApcState,
79 buf: Vec<u8>,
80 cut: bool,
83}
84
85#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
86enum ApcState {
87 #[default]
89 Idle,
90 Escape,
92 Inside,
94 InsideEscape,
96}
97
98impl ApcScanner {
99 fn split(&mut self, bytes: &[u8]) -> (Vec<u8>, Vec<(Vec<u8>, bool)>) {
114 let mut passthrough = Vec::with_capacity(bytes.len());
115 let mut done = Vec::new();
116 for &b in bytes {
117 match self.state {
118 ApcState::Idle => {
119 if b == 0x1b {
120 self.state = ApcState::Escape;
121 } else {
122 passthrough.push(b);
123 }
124 }
125 ApcState::Escape => {
126 if b == b'_' {
127 self.state = ApcState::Inside;
129 self.buf.clear();
130 self.cut = false;
131 } else {
132 passthrough.push(0x1b);
136 if b == 0x1b {
137 self.state = ApcState::Escape;
138 } else {
139 passthrough.push(b);
140 self.state = ApcState::Idle;
141 }
142 }
143 }
144 ApcState::Inside => match b {
145 0x1b => self.state = ApcState::InsideEscape,
146 0x07 => {
148 done.push((std::mem::take(&mut self.buf), self.cut));
149 self.state = ApcState::Idle;
150 }
151 _ => {
152 if self.buf.len() < GRAPHIC_PAYLOAD_MAX {
153 self.buf.push(b);
154 } else {
155 self.cut = true;
156 }
157 }
158 },
159 ApcState::InsideEscape => {
160 if b == b'\\' {
161 done.push((std::mem::take(&mut self.buf), self.cut));
162 self.state = ApcState::Idle;
163 } else {
164 if self.buf.len() < GRAPHIC_PAYLOAD_MAX {
166 self.buf.push(0x1b);
167 self.buf.push(b);
168 } else {
169 self.cut = true;
170 }
171 self.state = ApcState::Inside;
172 }
173 }
174 }
175 }
176 (passthrough, done)
177 }
178}
179
180pub(crate) struct GridState {
183 rows: usize,
184 cols: usize,
185 primary: VecDeque<Vec<Cell>>,
187 alternate: Vec<Vec<Cell>>,
191 alt_active: bool,
193 scrollback: VecDeque<Vec<Cell>>,
196 scrollback_cap: usize,
197 cursor_row: usize,
199 cursor_col: usize,
200 pen_fg: Color,
202 pen_bg: Color,
203 pen_attrs: CellAttrs,
204 saved: Option<SavedCursor>,
206 wrap_pending: bool,
214 scroll_top: usize,
217 scroll_bottom: usize,
218 palette: [Color; 16],
220 insert_mode: bool,
223 cursor_visible: bool,
225 cursor_keys_mode: bool,
230 last_printed: Option<char>,
232 role: HostRole,
236 autowrap: bool,
238 focus_reporting: bool,
240 bracketed_paste: bool,
242 sync_output: bool,
244 mouse: MouseTracking,
246 mouse_sgr: bool,
248 combining: Vec<Vec<char>>,
251 graphics: Vec<Graphic>,
255 sixel_in_flight: Option<Vec<u8>>,
258 pending_response: Vec<u8>,
266 title: Option<String>,
268 pub(crate) blocks: crate::blocks::BlockExtractor,
274}
275
276#[derive(Clone, Copy)]
277struct SavedCursor {
278 row: usize,
279 col: usize,
280 fg: Color,
281 bg: Color,
282 attrs: CellAttrs,
283}
284
285impl GridState {
286 fn new(cols: usize, rows: usize, scrollback_cap: usize) -> Self {
287 Self {
288 rows,
289 cols,
290 primary: VecDeque::from(vec![vec![Cell::BLANK; cols]; rows]),
291 alternate: vec![vec![Cell::BLANK; cols]; rows],
292 alt_active: false,
293 scrollback: VecDeque::with_capacity(64.min(scrollback_cap)),
300 scrollback_cap,
301 cursor_row: 0,
302 cursor_col: 0,
303 pen_fg: Color::WHITE,
304 pen_bg: Color::BLACK,
305 pen_attrs: CellAttrs::NONE,
306 saved: None,
307 wrap_pending: false,
308 scroll_top: 0,
309 scroll_bottom: rows.saturating_sub(1),
310 palette: default_ansi_palette(),
311 insert_mode: false,
312 cursor_visible: true,
313 cursor_keys_mode: false,
314 last_printed: None,
315 role: HostRole::default(),
316 autowrap: true,
318 focus_reporting: false,
319 bracketed_paste: false,
320 sync_output: false,
321 mouse: MouseTracking::Off,
322 mouse_sgr: false,
323 combining: Vec::new(),
324 graphics: Vec::new(),
325 sixel_in_flight: None,
326 pending_response: Vec::new(),
327 title: None,
328 blocks: crate::blocks::BlockExtractor::default(),
329 }
330 }
331
332 fn active_cell_mut(&mut self, row: usize, col: usize) -> Option<&mut Cell> {
335 if self.alt_active {
336 self.alternate.get_mut(row).and_then(|r| r.get_mut(col))
337 } else {
338 self.primary.get_mut(row).and_then(|r| r.get_mut(col))
339 }
340 }
341
342 fn ingest_apc(&mut self, payload: &[u8], cut: bool) {
350 let Some((&b'G', rest)) = payload.split_first() else {
351 return;
352 };
353 let (params, data) = match rest.iter().position(|&b| b == b';') {
358 Some(i) => (&rest[..i], &rest[i + 1..]),
359 None => (rest, &[][..]),
362 };
363 self.push_graphic(
364 GraphicProtocol::Kitty,
365 String::from_utf8_lossy(params).into_owned(),
366 data.to_vec(),
367 cut,
368 );
369 }
370
371 fn push_graphic(
376 &mut self,
377 protocol: GraphicProtocol,
378 params: String,
379 mut data: Vec<u8>,
380 cut_upstream: bool,
381 ) {
382 let truncated = cut_upstream || data.len() > GRAPHIC_PAYLOAD_MAX;
387 if data.len() > GRAPHIC_PAYLOAD_MAX {
388 data.truncate(GRAPHIC_PAYLOAD_MAX);
389 }
390 self.graphics.push(Graphic {
391 protocol,
392 params,
393 data,
394 at_row: self.cursor_row,
395 at_col: self.cursor_col,
396 truncated,
397 });
398 }
399
400 fn answer(&mut self, bytes: &[u8]) {
407 if self.role.answers_queries() {
408 self.pending_response.extend_from_slice(bytes);
409 }
410 }
411
412 fn active_cell_at(&self, row: usize, col: usize) -> Option<&Cell> {
414 if self.alt_active {
415 self.alternate.get(row).and_then(|r| r.get(col))
416 } else {
417 self.primary.get(row).and_then(|r| r.get(col))
418 }
419 }
420
421 fn active_row_mut(&mut self, row: usize) -> Option<&mut Vec<Cell>> {
422 if self.alt_active {
423 self.alternate.get_mut(row)
424 } else {
425 self.primary.get_mut(row)
426 }
427 }
428
429 fn active_rows(&self) -> impl Iterator<Item = &Vec<Cell>> + '_ {
430 if self.alt_active {
431 Box::new(self.alternate.iter()) as Box<dyn Iterator<Item = &Vec<Cell>>>
432 } else {
433 Box::new(self.primary.iter())
434 }
435 }
436
437 fn blank_cell(&self) -> Cell {
438 Cell {
441 ch: ' ',
442 fg: self.pen_fg,
443 bg: self.pen_bg,
444 attrs: CellAttrs::NONE,
445 width: 1,
446 combining: 0,
447 }
448 }
449
450 fn current_cell_for_print(&self, ch: char, w: u8) -> Cell {
453 Cell {
454 ch,
455 fg: self.pen_fg,
456 bg: self.pen_bg,
457 attrs: self.pen_attrs,
458 width: w,
459 combining: 0,
462 }
463 }
464
465 fn continuation_cell(&self) -> Cell {
471 Cell {
472 ch: ' ',
473 fg: self.pen_fg,
474 bg: self.pen_bg,
475 attrs: self.pen_attrs,
476 width: 0,
477 combining: 0,
478 }
479 }
480
481
482 fn scroll_region_up(&mut self) {
483 if self.scroll_top > self.scroll_bottom {
487 return;
488 }
489 let blank = vec![self.blank_cell(); self.cols];
490 let full_region = self.scroll_top == 0 && self.scroll_bottom == self.rows.saturating_sub(1);
491 if self.alt_active {
492 if self.scroll_top < self.alternate.len() {
493 self.alternate.remove(self.scroll_top);
494 self.alternate
495 .insert(self.scroll_bottom.min(self.alternate.len()), blank);
496 }
497 } else {
498 if full_region {
499 if let Some(top) = self.primary.pop_front() {
500 if self.scrollback_cap > 0 {
501 if self.scrollback.len() >= self.scrollback_cap {
502 self.scrollback.pop_front();
503 }
504 self.scrollback.push_back(top);
505 }
506 }
507 self.primary.push_back(blank);
508 } else if self.scroll_top < self.primary.len() {
509 self.primary.remove(self.scroll_top);
510 let insert_at = (self.scroll_bottom + 1).min(self.primary.len());
511 self.primary.insert(insert_at, blank);
512 }
513 }
514 }
515
516 fn advance_cursor_after_print(&mut self, w: usize) {
523 let adv = w.max(1);
524 if self.cursor_col + adv >= self.cols {
525 self.park_at_right_margin();
526 } else {
527 self.cursor_col += adv;
528 }
529 }
530
531 fn park_at_right_margin(&mut self) {
541 self.cursor_col = self.cols.saturating_sub(1);
542 self.wrap_pending = true;
543 }
544
545 fn clear_orphans_at(&mut self, row: usize, col: usize, w: usize) {
551 if col > 0 && self.active_cell_at(row, col).is_some_and(Cell::is_continuation) {
554 if let Some(lead) = self.active_cell_mut(row, col - 1) {
555 *lead = Cell::BLANK;
556 }
557 }
558 let last = col + w.saturating_sub(1);
561 if self.active_cell_at(row, last).is_some_and(|c| c.width == 2) && last + 1 < self.cols {
562 if let Some(cont) = self.active_cell_mut(row, last + 1) {
563 *cont = Cell::BLANK;
564 }
565 }
566 }
567
568 fn combine_into_previous(&mut self, c: char) {
587 let start = if self.wrap_pending {
588 self.cols.saturating_sub(1)
589 } else if self.cursor_col > 0 {
590 self.cursor_col - 1
591 } else {
592 return;
593 };
594 let row = self.cursor_row;
595 let col = self.lead_col_at(row, start);
596 if col >= self.cols || row >= self.rows {
597 return;
598 }
599 let existing = self
602 .active_cell_at(row, col)
603 .map_or(0, |cell| cell.combining);
604 if existing == 0 {
605 let Ok(next) = u16::try_from(self.combining.len() + 1) else {
608 return;
609 };
610 self.combining.push(vec![c]);
611 if let Some(cell) = self.active_cell_mut(row, col) {
612 cell.combining = next;
613 } else {
614 self.combining.pop();
617 }
618 } else if let Some(marks) = self.combining.get_mut(existing as usize - 1) {
619 marks.push(c);
620 }
621 }
622
623 fn lead_col_at(&self, row: usize, col: usize) -> usize {
628 if col > 0 && self.active_cell_at(row, col).is_some_and(Cell::is_continuation) {
629 col - 1
630 } else {
631 col
632 }
633 }
634
635 fn put_char(&mut self, c: char, w: usize) {
637 if self.wrap_pending {
639 self.wrap_pending = false;
640 self.cursor_col = 0;
641 self.linefeed();
642 }
643 if w == 2 && self.cursor_col + 1 >= self.cols {
647 self.cursor_col = 0;
648 self.linefeed();
649 }
650 let row = self.cursor_row;
651 let col = self.cursor_col;
652 let cell = self.current_cell_for_print(c, w as u8);
653
654 if self.insert_mode {
655 let cols = self.cols;
657 let cont = self.continuation_cell();
658 if let Some(r) = self.active_row_mut(row) {
659 if col < r.len() {
660 r.insert(col, cell);
661 if w == 2 && col + 1 <= r.len() {
662 r.insert(col + 1, cont);
663 }
664 r.truncate(cols);
665 }
666 }
667 } else {
668 self.clear_orphans_at(row, col, w);
669 if let Some(slot) = self.active_cell_mut(row, col) {
670 *slot = cell;
671 }
672 if w == 2 && col + 1 < self.cols {
673 let cont = self.continuation_cell();
674 if let Some(slot) = self.active_cell_mut(row, col + 1) {
675 *slot = cont;
676 }
677 }
678 }
679 self.last_printed = Some(c);
680 self.advance_cursor_after_print(w);
681 }
682
683 fn linefeed(&mut self) {
684 if self.cursor_row == self.scroll_bottom {
685 self.scroll_region_up();
686 } else if self.cursor_row + 1 < self.rows {
687 self.cursor_row += 1;
688 }
689 }
690
691 fn carriage_return(&mut self) {
692 self.cursor_col = 0;
693 }
694
695 fn backspace(&mut self) {
696 if self.cursor_col > 0 {
697 self.cursor_col -= 1;
698 }
699 }
700
701 fn tab_forward(&mut self) {
702 let next = ((self.cursor_col / 8) + 1) * 8;
703 self.cursor_col = next.min(self.cols.saturating_sub(1));
704 }
705
706 fn cursor_move_relative(&mut self, drow: isize, dcol: isize) {
707 let r = (self.cursor_row as isize + drow).max(0) as usize;
708 let c = (self.cursor_col as isize + dcol).max(0) as usize;
709 self.cursor_row = r.min(self.rows.saturating_sub(1));
710 self.cursor_col = c.min(self.cols.saturating_sub(1));
711 }
712
713 fn cursor_set(&mut self, row: usize, col: usize) {
714 self.cursor_row = row.min(self.rows.saturating_sub(1));
715 self.cursor_col = col.min(self.cols.saturating_sub(1));
716 }
717
718 fn erase_to_end_of_line(&mut self) {
719 let row = self.cursor_row;
720 let start = self.cursor_col;
721 let blank = self.blank_cell();
722 if let Some(r) = self.active_row_mut(row) {
723 for c in r.iter_mut().skip(start) {
724 *c = blank;
725 }
726 }
727 }
728
729 fn erase_from_start_of_line(&mut self) {
730 let row = self.cursor_row;
731 let stop = self.cursor_col + 1;
732 let blank = self.blank_cell();
733 if let Some(r) = self.active_row_mut(row) {
734 let stop = stop.min(r.len());
735 for c in r.iter_mut().take(stop) {
736 *c = blank;
737 }
738 }
739 }
740
741 fn erase_line(&mut self) {
742 let row = self.cursor_row;
743 let blank = self.blank_cell();
744 if let Some(r) = self.active_row_mut(row) {
745 for c in r.iter_mut() {
746 *c = blank;
747 }
748 }
749 }
750
751 fn erase_below_cursor(&mut self) {
752 self.erase_to_end_of_line();
754 let start = self.cursor_row + 1;
755 let end = self.rows;
756 let blank = self.blank_cell();
757 for r in start..end {
758 if let Some(row) = self.active_row_mut(r) {
759 for c in row.iter_mut() {
760 *c = blank;
761 }
762 }
763 }
764 }
765
766 fn erase_above_cursor(&mut self) {
767 let stop_row = self.cursor_row;
769 let blank = self.blank_cell();
770 for r in 0..stop_row {
771 if let Some(row) = self.active_row_mut(r) {
772 for c in row.iter_mut() {
773 *c = blank;
774 }
775 }
776 }
777 self.erase_from_start_of_line();
778 }
779
780 fn erase_all(&mut self) {
781 let blank = self.blank_cell();
782 let rows = self.rows;
783 for r in 0..rows {
784 if let Some(row) = self.active_row_mut(r) {
785 for c in row.iter_mut() {
786 *c = blank;
787 }
788 }
789 }
790 }
791
792 fn save_cursor(&mut self) {
793 self.saved = Some(SavedCursor {
794 row: self.cursor_row,
795 col: self.cursor_col,
796 fg: self.pen_fg,
797 bg: self.pen_bg,
798 attrs: self.pen_attrs,
799 });
800 }
801
802 fn restore_cursor(&mut self) {
803 if let Some(s) = self.saved {
804 self.cursor_row = s.row.min(self.rows.saturating_sub(1));
805 self.cursor_col = s.col.min(self.cols.saturating_sub(1));
806 self.pen_fg = s.fg;
807 self.pen_bg = s.bg;
808 self.pen_attrs = s.attrs;
809 }
810 }
811
812 fn enter_alt_screen(&mut self, clear: bool) {
813 if !self.alt_active {
814 self.alt_active = true;
815 }
816 if clear {
817 for row in &mut self.alternate {
818 for c in row.iter_mut() {
819 *c = Cell::BLANK;
820 }
821 }
822 self.cursor_row = 0;
823 self.cursor_col = 0;
824 }
825 }
826
827 fn leave_alt_screen(&mut self) {
828 self.alt_active = false;
829 }
830
831 fn apply_sgr(&mut self, params: &Params) {
834 let items: Vec<&[u16]> = params.iter().collect();
865 if items.is_empty() {
866 self.sgr_reset();
867 return;
868 }
869 let mut idx = 0;
870 while idx < items.len() {
871 let param = items[idx];
872 let Some(&code) = param.first() else {
873 idx += 1;
874 continue;
875 };
876
877 if param.len() > 1 {
879 self.apply_sgr_subparams(param);
880 idx += 1;
881 continue;
882 }
883
884 if matches!(code, 38 | 48 | 58) {
889 let (colour, consumed) = self.parse_extended_color_params(&items[idx..]);
890 match (code, colour) {
891 (38, Some(c)) => self.pen_fg = c,
892 (48, Some(c)) => self.pen_bg = c,
893 _ => {}
894 }
895 idx += consumed;
896 continue;
897 }
898
899 self.apply_sgr_code(code);
900 idx += 1;
901 }
902 }
903
904 fn apply_sgr_subparams(&mut self, param: &[u16]) {
907 match param[0] {
908 4 => {
912 if param[1] == 0 {
913 self.pen_attrs.remove(CellAttrs::UNDERLINE);
914 } else {
915 self.pen_attrs.insert(CellAttrs::UNDERLINE);
916 }
917 }
918 code @ (38 | 48 | 58) => {
919 let colour = match param[1] {
920 5 => param.get(2).map(|&n| ansi_256_color(n, &self.palette)),
921 2 => match param.len() {
925 n if n >= 6 => {
926 Some(Color::new(param[3] as u8, param[4] as u8, param[5] as u8))
927 }
928 5 => Some(Color::new(param[2] as u8, param[3] as u8, param[4] as u8)),
929 _ => None,
930 },
931 _ => None,
932 };
933 match (code, colour) {
934 (38, Some(c)) => self.pen_fg = c,
935 (48, Some(c)) => self.pen_bg = c,
936 _ => {}
939 }
940 }
941 other => self.apply_sgr_code(other),
942 }
943 }
944
945 fn parse_extended_color_params(&self, rest: &[&[u16]]) -> (Option<Color>, usize) {
950 let first = |i: usize| rest.get(i).and_then(|p| p.first().copied());
951 match first(1) {
952 Some(5) => match first(2) {
953 Some(n) => (Some(ansi_256_color(n, &self.palette)), 3),
957 None => (None, 2),
958 },
959 Some(2) => match (first(2), first(3), first(4)) {
960 (Some(r), Some(g), Some(b)) => (Some(Color::new(r as u8, g as u8, b as u8)), 5),
961 _ => (None, rest.len().min(5)),
962 },
963 _ => (None, 1),
964 }
965 }
966
967 fn apply_sgr_code(&mut self, code: u16) {
968 {
969 let p = code;
970 match p {
971 0 => self.sgr_reset(),
972 1 => self.pen_attrs.insert(CellAttrs::BOLD),
973 2 => self.pen_attrs.insert(CellAttrs::DIM),
974 3 => self.pen_attrs.insert(CellAttrs::ITALIC),
975 4 => self.pen_attrs.insert(CellAttrs::UNDERLINE),
976 5 | 6 => self.pen_attrs.insert(CellAttrs::BLINK),
977 7 => self.pen_attrs.insert(CellAttrs::INVERSE),
978 8 => self.pen_attrs.insert(CellAttrs::HIDDEN),
979 9 => self.pen_attrs.insert(CellAttrs::STRIKETHROUGH),
980 21 | 22 => {
981 self.pen_attrs.remove(CellAttrs::BOLD);
982 self.pen_attrs.remove(CellAttrs::DIM);
983 }
984 23 => self.pen_attrs.remove(CellAttrs::ITALIC),
985 24 => self.pen_attrs.remove(CellAttrs::UNDERLINE),
986 25 => self.pen_attrs.remove(CellAttrs::BLINK),
987 27 => self.pen_attrs.remove(CellAttrs::INVERSE),
988 28 => self.pen_attrs.remove(CellAttrs::HIDDEN),
989 29 => self.pen_attrs.remove(CellAttrs::STRIKETHROUGH),
990 30..=37 => self.pen_fg = self.palette[(p - 30) as usize],
991 39 => self.pen_fg = Color::WHITE,
992 40..=47 => self.pen_bg = self.palette[(p - 40) as usize],
993 49 => self.pen_bg = Color::BLACK,
994 90..=97 => self.pen_fg = self.palette[8 + (p - 90) as usize],
995 100..=107 => self.pen_bg = self.palette[8 + (p - 100) as usize],
996 _ => {} }
998 }
999 }
1000
1001 fn sgr_reset(&mut self) {
1002 self.pen_fg = Color::WHITE;
1003 self.pen_bg = Color::BLACK;
1004 self.pen_attrs = CellAttrs::NONE;
1005 }
1006}
1007
1008impl Perform for GridState {
1009 fn print(&mut self, c: char) {
1010 self.blocks.on_print(c);
1015 let w = UnicodeWidthChar::width(c).unwrap_or(1);
1016 if w == 0 {
1017 self.combine_into_previous(c);
1022 return;
1023 }
1024 self.put_char(c, w);
1025 }
1026
1027 fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, action: char) {
1033 if action == 'q' {
1034 self.sixel_in_flight = Some(Vec::new());
1035 }
1036 }
1037
1038 fn put(&mut self, byte: u8) {
1039 if let Some(buf) = self.sixel_in_flight.as_mut() {
1040 if buf.len() < GRAPHIC_PAYLOAD_MAX {
1043 buf.push(byte);
1044 }
1045 }
1046 }
1047
1048 fn unhook(&mut self) {
1049 if let Some(data) = self.sixel_in_flight.take() {
1050 if !data.is_empty() {
1051 let cut = data.len() >= GRAPHIC_PAYLOAD_MAX;
1054 self.push_graphic(GraphicProtocol::Sixel, String::new(), data, cut);
1055 }
1056 }
1057 }
1058
1059 fn execute(&mut self, byte: u8) {
1060 self.wrap_pending = false;
1063 match byte {
1064 b'\n' => self.linefeed(),
1065 b'\r' => self.carriage_return(),
1066 b'\x08' => self.backspace(),
1067 b'\t' => self.tab_forward(),
1068 b'\x07' => {} _ => {}
1070 }
1071 }
1072
1073 fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, c: char) {
1074 if c != 'm' {
1076 self.wrap_pending = false;
1077 }
1078 let first = params
1079 .iter()
1080 .next()
1081 .and_then(|p| p.first().copied())
1082 .unwrap_or(0);
1083 let n = first.max(1) as isize;
1084 if let Some(prefix) = intermediates
1107 .first()
1108 .copied()
1109 .filter(|b| (0x3C..=0x3F).contains(b))
1110 {
1111 if prefix == b'?' && (c == 'h' || c == 'l') {
1112 let set = c == 'h';
1113 for p in params.iter() {
1114 if let Some(&code) = p.first() {
1115 self.apply_dec_mode(code, set);
1116 }
1117 }
1118 }
1119 if prefix == b'>' && c == 'c' {
1124 self.answer(TearCaps::SECONDARY_DA);
1125 }
1126 return;
1127 }
1128 match c {
1129 'n' => match first {
1132 5 => self.answer(TearCaps::STATUS_OK),
1133 6 => {
1134 let row = self.cursor_row + 1;
1139 let col = self.cursor_col + 1;
1140 let mut r = Vec::new();
1141 r.extend_from_slice(b"\x1b[");
1142 r.extend_from_slice(row.to_string().as_bytes());
1143 r.push(b';');
1144 r.extend_from_slice(col.to_string().as_bytes());
1145 r.push(b'R');
1146 self.answer(&r);
1147 }
1148 _ => {}
1149 },
1150 'c' => self.answer(TearCaps::PRIMARY_DA),
1152 'A' => self.cursor_move_relative(-n, 0),
1153 'B' => self.cursor_move_relative(n, 0),
1154 'C' => self.cursor_move_relative(0, n),
1155 'D' => self.cursor_move_relative(0, -n),
1156 'E' => {
1157 self.carriage_return();
1158 self.cursor_move_relative(n, 0);
1159 }
1160 'F' => {
1161 self.carriage_return();
1162 self.cursor_move_relative(-n, 0);
1163 }
1164 'G' => {
1165 let col = first.max(1) as usize - 1;
1166 let row = self.cursor_row;
1167 self.cursor_set(row, col);
1168 }
1169 'H' | 'f' => {
1170 let mut it = params.iter();
1171 let row = it
1172 .next()
1173 .and_then(|p| p.first().copied())
1174 .unwrap_or(1)
1175 .max(1) as usize;
1176 let col = it
1177 .next()
1178 .and_then(|p| p.first().copied())
1179 .unwrap_or(1)
1180 .max(1) as usize;
1181 self.cursor_set(row - 1, col - 1);
1182 }
1183 'J' => match first {
1184 0 => self.erase_below_cursor(),
1185 1 => self.erase_above_cursor(),
1186 2 | 3 => self.erase_all(),
1187 _ => {}
1188 },
1189 'K' => match first {
1190 0 => self.erase_to_end_of_line(),
1191 1 => self.erase_from_start_of_line(),
1192 2 => self.erase_line(),
1193 _ => {}
1194 },
1195 'L' => {
1196 let blank = vec![self.blank_cell(); self.cols];
1199 let row = self.cursor_row;
1200 for _ in 0..n {
1201 if self.alt_active {
1202 if row < self.alternate.len() && row <= self.scroll_bottom {
1203 self.alternate.insert(row, blank.clone());
1204 if self.scroll_bottom + 1 < self.alternate.len() {
1205 self.alternate.remove(self.scroll_bottom + 1);
1206 }
1207 }
1208 } else if row < self.primary.len() && row <= self.scroll_bottom {
1209 self.primary.insert(row, blank.clone());
1210 if self.scroll_bottom + 1 < self.primary.len() {
1211 self.primary.remove(self.scroll_bottom + 1);
1212 }
1213 }
1214 }
1215 }
1216 'M' => {
1217 let blank = vec![self.blank_cell(); self.cols];
1220 let row = self.cursor_row;
1221 for _ in 0..n {
1222 if self.alt_active {
1223 if row < self.alternate.len() && row <= self.scroll_bottom {
1224 self.alternate.remove(row);
1225 let insert_at = (self.scroll_bottom).min(self.alternate.len());
1226 self.alternate.insert(insert_at, blank.clone());
1227 }
1228 } else if row < self.primary.len() && row <= self.scroll_bottom {
1229 self.primary.remove(row);
1230 let insert_at = (self.scroll_bottom).min(self.primary.len());
1231 self.primary.insert(insert_at, blank.clone());
1232 }
1233 }
1234 }
1235 '@' => {
1236 let blank = self.blank_cell();
1238 let row = self.cursor_row;
1239 let col = self.cursor_col;
1240 let cols = self.cols;
1241 if let Some(r) = self.active_row_mut(row) {
1242 for _ in 0..n {
1243 if col < r.len() {
1244 r.insert(col, blank);
1245 r.truncate(cols);
1246 }
1247 }
1248 }
1249 }
1250 'P' => {
1251 let blank = self.blank_cell();
1253 let row = self.cursor_row;
1254 let col = self.cursor_col;
1255 let cols = self.cols;
1256 if let Some(r) = self.active_row_mut(row) {
1257 for _ in 0..n {
1258 if col < r.len() {
1259 r.remove(col);
1260 r.push(blank);
1261 if r.len() > cols {
1262 r.truncate(cols);
1263 }
1264 }
1265 }
1266 }
1267 }
1268 'X' => {
1269 let blank = self.blank_cell();
1271 let row = self.cursor_row;
1272 let col = self.cursor_col;
1273 let n_usize = n as usize;
1274 if let Some(r) = self.active_row_mut(row) {
1275 for i in 0..n_usize {
1276 if col + i < r.len() {
1277 r[col + i] = blank;
1278 }
1279 }
1280 }
1281 }
1282 'b' => {
1283 if let Some(c) = self.last_printed {
1285 for _ in 0..n {
1286 Perform::print(self, c);
1287 }
1288 }
1289 }
1290 'h' => {
1291 for p in params.iter() {
1293 if p.first().copied() == Some(4) {
1294 self.insert_mode = true;
1295 }
1296 }
1297 }
1298 'l' => {
1299 for p in params.iter() {
1301 if p.first().copied() == Some(4) {
1302 self.insert_mode = false;
1303 }
1304 }
1305 }
1306 'S' => {
1307 for _ in 0..n {
1308 self.scroll_region_up();
1309 }
1310 }
1311 'T' => {
1312 for _ in 0..n {
1314 let blank = vec![self.blank_cell(); self.cols];
1315 if self.alt_active {
1316 if self.scroll_top < self.alternate.len() {
1317 self.alternate.insert(self.scroll_top, blank);
1318 if self.scroll_bottom + 1 < self.alternate.len() {
1319 self.alternate.remove(self.scroll_bottom + 1);
1320 }
1321 }
1322 } else if self.scroll_top < self.primary.len() {
1323 self.primary.insert(self.scroll_top, blank);
1324 if self.scroll_bottom + 1 < self.primary.len() {
1325 self.primary.remove(self.scroll_bottom + 1);
1326 }
1327 }
1328 }
1329 }
1330 'd' => {
1331 let row = first.max(1) as usize - 1;
1332 let col = self.cursor_col;
1333 self.cursor_set(row, col);
1334 }
1335 'm' => self.apply_sgr(params),
1336 'r' => {
1337 let mut it = params.iter();
1339 let top = it
1340 .next()
1341 .and_then(|p| p.first().copied())
1342 .unwrap_or(1)
1343 .max(1) as usize
1344 - 1;
1345 let bottom = it
1346 .next()
1347 .and_then(|p| p.first().copied())
1348 .unwrap_or(self.rows as u16)
1349 .max(1) as usize
1350 - 1;
1351 self.scroll_top = top.min(self.rows.saturating_sub(1));
1352 self.scroll_bottom = bottom.min(self.rows.saturating_sub(1));
1353 self.cursor_set(0, 0);
1354 }
1355 's' => self.save_cursor(),
1356 'u' => self.restore_cursor(),
1357 _ => {}
1358 }
1359 }
1360
1361 fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
1362 let code = params.first().and_then(|p| std::str::from_utf8(p).ok());
1365 if matches!(code, Some("0") | Some("1") | Some("2")) {
1366 if let Some(t) = params.get(1).and_then(|p| std::str::from_utf8(p).ok()) {
1367 self.title = Some(t.to_owned());
1368 }
1369 return;
1370 }
1371 if matches!(code, Some("7"))
1377 && let Some(payload) = params.get(1).and_then(|p| std::str::from_utf8(p).ok())
1378 {
1379 self.blocks.set_cwd_from_osc7(payload);
1380 return;
1381 }
1382 if matches!(code, Some("133")) {
1388 let marker: String = params
1390 .iter()
1391 .skip(1)
1392 .filter_map(|p| std::str::from_utf8(p).ok())
1393 .collect::<Vec<_>>()
1394 .join(";");
1395 self.blocks.on_osc_133(&marker);
1396 }
1397 }
1398
1399 fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, byte: u8) {
1400 match byte {
1401 b'7' => self.save_cursor(),
1402 b'8' => self.restore_cursor(),
1403 b'D' => self.linefeed(),
1404 b'E' => {
1405 self.linefeed();
1406 self.carriage_return();
1407 }
1408 b'M' => {
1409 if self.cursor_row == self.scroll_top {
1411 let blank = vec![self.blank_cell(); self.cols];
1413 if self.alt_active {
1414 if self.scroll_top < self.alternate.len() {
1415 self.alternate.insert(self.scroll_top, blank);
1416 if self.scroll_bottom + 1 < self.alternate.len() {
1417 self.alternate.remove(self.scroll_bottom + 1);
1418 }
1419 }
1420 } else {
1421 self.primary.insert(self.scroll_top, blank);
1422 if self.scroll_bottom + 1 < self.primary.len() {
1423 self.primary.remove(self.scroll_bottom + 1);
1424 }
1425 }
1426 } else if self.cursor_row > 0 {
1427 self.cursor_row -= 1;
1428 }
1429 }
1430 b'c' => {
1431 self.sgr_reset();
1433 self.erase_all();
1434 self.cursor_set(0, 0);
1435 self.scroll_top = 0;
1436 self.scroll_bottom = self.rows.saturating_sub(1);
1437 self.alt_active = false;
1438 self.saved = None;
1439 self.cursor_keys_mode = false;
1440 self.cursor_visible = true;
1441 self.title = None;
1442 }
1443 _ => {}
1444 }
1445 }
1446}
1447
1448impl GridState {
1449 fn apply_dec_mode(&mut self, code: u16, set: bool) {
1450 match code {
1451 47 => {
1456 if set {
1457 self.enter_alt_screen(false);
1458 } else {
1459 self.leave_alt_screen();
1460 }
1461 }
1462 1047 => {
1463 if set {
1464 self.enter_alt_screen(true);
1465 } else {
1466 self.erase_all();
1467 self.leave_alt_screen();
1468 }
1469 }
1470 1049 => {
1471 if set {
1472 self.save_cursor();
1473 self.enter_alt_screen(true);
1474 } else {
1475 self.erase_all();
1476 self.leave_alt_screen();
1477 self.restore_cursor();
1478 }
1479 }
1480 1 => self.cursor_keys_mode = set, 25 => self.cursor_visible = set, 7 => self.autowrap = set, 1004 => self.focus_reporting = set, 2004 => self.bracketed_paste = set, 2026 => self.sync_output = set, 1000 => self.mouse = if set { MouseTracking::Click } else { MouseTracking::Off },
1491 1002 => self.mouse = if set { MouseTracking::Drag } else { MouseTracking::Off },
1492 1003 => self.mouse = if set { MouseTracking::Motion } else { MouseTracking::Off },
1493 1006 => self.mouse_sgr = set, _ => {}
1495 }
1496 }
1497}
1498
1499impl PaneGrid {
1500 #[must_use]
1530 pub(crate) fn new(cols: usize, rows: usize) -> Self {
1531 Self::with_scrollback(cols, rows, DEFAULT_SCROLLBACK_ROWS)
1532 }
1533
1534 #[must_use]
1535 pub(crate) fn with_scrollback(cols: usize, rows: usize, scrollback_cap: usize) -> Self {
1536 Self {
1537 parser: Parser::new(),
1538 state: GridState::new(cols, rows, scrollback_cap),
1539 apc: ApcScanner::default(),
1540 }
1541 }
1542
1543 pub(crate) fn feed(&mut self, bytes: &[u8]) {
1549 let (passthrough, apcs) = self.apc.split(bytes);
1552 self.parser.advance(&mut self.state, &passthrough);
1553 for (payload, cut) in apcs {
1554 self.state.ingest_apc(&payload, cut);
1555 }
1556 }
1557
1558 #[must_use]
1570 pub fn modes(&self) -> ModeSet {
1571 let s = &self.state;
1572 ModeSet {
1573 bracketed_paste: BracketedPaste::new(s.bracketed_paste),
1574 cursor_keys: CursorKeys::new(s.cursor_keys_mode),
1575 focus_reporting: FocusReporting::new(s.focus_reporting),
1576 sync_output: SyncOutput::new(s.sync_output),
1577 mouse: s.mouse,
1578 mouse_sgr: MouseSgr::new(s.mouse_sgr),
1579 cursor_visible: CursorVisible::new(s.cursor_visible),
1580 autowrap: AutoWrap::new(s.autowrap),
1581 alt_screen: AltScreen::new(s.alt_active),
1582 }
1583 }
1584
1585 pub(crate) fn set_host_role(&mut self, role: HostRole) {
1591 self.state.role = role;
1592 }
1593
1594 #[must_use]
1600 pub(crate) fn take_response(&mut self) -> Option<Vec<u8>> {
1601 if self.state.pending_response.is_empty() {
1602 None
1603 } else {
1604 Some(std::mem::take(&mut self.state.pending_response))
1605 }
1606 }
1607
1608 #[must_use]
1609 pub fn snapshot(&self) -> PaneSnapshot {
1610 let cells: Vec<Vec<Cell>> = self.state.active_rows().cloned().collect();
1611 let scrollback: Vec<Vec<Cell>> = if self.state.alt_active {
1616 Vec::new()
1617 } else {
1618 self.state.scrollback.iter().cloned().collect()
1619 };
1620 PaneSnapshot {
1621 rows: self.state.rows,
1622 cols: self.state.cols,
1623 cells,
1624 cursor_row: self.state.cursor_row,
1625 cursor_col: self.state.cursor_col,
1626 alt_screen_active: self.state.alt_active,
1627 cursor_visible: self.state.cursor_visible,
1628 title: self.state.title.clone(),
1629 cursor_keys_mode: self.state.cursor_keys_mode,
1630 scrollback,
1631 combining: self.state.combining.clone(),
1632 modes: self.modes(),
1633 graphics: self.state.graphics.clone(),
1634 }
1635 }
1636
1637 #[must_use]
1640 pub fn title(&self) -> Option<&str> {
1641 self.state.title.as_deref()
1642 }
1643
1644 pub fn stamp_yurai(&mut self, y: tear_types::Yurai) -> bool {
1656 self.state.blocks.stamp_yurai(y)
1657 }
1658
1659 #[must_use]
1661 pub fn yurai(&self) -> &tear_types::Yurai {
1662 self.state.blocks.yurai()
1663 }
1664
1665 #[must_use]
1672 pub fn cursor_keys_mode(&self) -> bool {
1673 self.state.cursor_keys_mode
1674 }
1675
1676 #[must_use]
1679 pub fn scrollback_len(&self) -> usize {
1680 self.state.scrollback.len()
1681 }
1682
1683 pub fn resize(&mut self, cols: usize, rows: usize) {
1684 let mut new_primary: VecDeque<Vec<Cell>> = VecDeque::with_capacity(rows);
1688 for r in 0..rows {
1689 let mut new_row = vec![Cell::BLANK; cols];
1690 if let Some(existing) = self.state.primary.get(r) {
1691 let n = existing.len().min(cols);
1692 new_row[..n].copy_from_slice(&existing[..n]);
1693 }
1694 new_primary.push_back(new_row);
1695 }
1696 let mut new_alt = vec![vec![Cell::BLANK; cols]; rows];
1697 for r in 0..rows.min(self.state.alternate.len()) {
1698 let existing = &self.state.alternate[r];
1699 let n = existing.len().min(cols);
1700 new_alt[r][..n].copy_from_slice(&existing[..n]);
1701 }
1702 self.state.primary = new_primary;
1703 self.state.alternate = new_alt;
1704 self.state.rows = rows;
1705 self.state.cols = cols;
1706 self.state.cursor_row = self.state.cursor_row.min(rows.saturating_sub(1));
1707 self.state.cursor_col = self.state.cursor_col.min(cols.saturating_sub(1));
1708 self.state.scroll_top = 0;
1709 self.state.scroll_bottom = rows.saturating_sub(1);
1710 }
1711}
1712
1713#[cfg(test)]
1726mod width_parity {
1727 use super::*;
1728
1729 #[test]
1732 fn wide_glyph_advances_two_columns() {
1733 let mut g = PaneGrid::new(20, 3);
1734 g.feed("你".as_bytes());
1735 let s = g.snapshot();
1736 assert_eq!(s.cells[0][0].ch, '你', "lead cell holds the glyph");
1737 assert_eq!(s.cells[0][0].width, 2, "lead is marked double-width");
1738 assert_eq!(s.cells[0][1].width, 0, "col 1 is a continuation cell");
1739 assert_eq!(s.cursor_col, 2, "cursor advances by the glyph's WIDTH");
1740 }
1741
1742 #[test]
1746 fn later_cells_are_not_displaced_by_wide_glyphs() {
1747 let mut g = PaneGrid::new(20, 3);
1748 g.feed("你好X".as_bytes());
1749 let s = g.snapshot();
1750 assert_eq!(s.cells[0][0].ch, '你');
1751 assert_eq!(s.cells[0][2].ch, '好', "second glyph starts at col 2, not 1");
1752 assert_eq!(s.cells[0][4].ch, 'X', "ASCII lands at col 4, not 2");
1753 assert_eq!(s.cursor_col, 5);
1754 }
1755
1756 #[test]
1759 fn wide_glyph_that_does_not_fit_wraps_whole() {
1760 let mut g = PaneGrid::new(20, 3);
1761 g.feed("A".repeat(19).as_bytes());
1762 g.feed("你".as_bytes());
1763 let s = g.snapshot();
1764 assert_eq!(s.cells[0][19].ch, ' ', "last col of row 0 stays blank");
1765 assert_eq!(s.cells[1][0].ch, '你', "glyph moved to the next row whole");
1766 assert_eq!(s.cells[1][1].width, 0);
1767 }
1768
1769 #[test]
1773 fn wide_glyph_flush_to_margin_parks_at_last_column() {
1774 let mut g = PaneGrid::new(20, 3);
1775 g.feed("A".repeat(18).as_bytes());
1776 g.feed("你".as_bytes());
1777 let s = g.snapshot();
1778 assert_eq!(s.cells[0][18].ch, '你');
1779 assert_eq!(s.cells[0][19].width, 0);
1780 assert_eq!(s.cursor_col, 19, "parked at the last column, not at 18");
1781 }
1782
1783 #[test]
1786 fn overwriting_a_wide_pair_clears_its_orphan() {
1787 let mut g = PaneGrid::new(20, 3);
1788 g.feed("你".as_bytes());
1789 g.feed(b"\x1b[1;1H");
1790 g.feed(b"X");
1791 let s = g.snapshot();
1792 assert_eq!(s.cells[0][0].ch, 'X');
1793 assert_eq!(s.cells[0][0].width, 1);
1794 assert_eq!(
1795 s.cells[0][1].ch, ' ',
1796 "the orphaned continuation is cleared, not left as a half-glyph"
1797 );
1798 assert_eq!(s.cells[0][1].width, 1);
1799 }
1800
1801 #[test]
1805 fn a_combining_mark_attaches_to_the_base_cell() {
1806 let mut g = PaneGrid::new(20, 3);
1807 g.feed("e\u{301}X".as_bytes()); let s = g.snapshot();
1809 assert_eq!(s.cells[0][0].ch, 'e');
1810 assert_eq!(
1811 s.cells[0][0].marks(&s.combining),
1812 &['\u{301}'],
1813 "the mark belongs to the base cell"
1814 );
1815 assert_eq!(s.cells[0][1].ch, 'X', "X is at col 1, not col 2");
1816 assert_eq!(s.cursor_col, 2, "a mark consumes no column");
1817 }
1818
1819 #[test]
1821 fn stacked_marks_accumulate_on_one_cell() {
1822 let mut g = PaneGrid::new(20, 3);
1823 g.feed("a\u{301}\u{308}".as_bytes());
1824 let s = g.snapshot();
1825 assert_eq!(s.cells[0][0].marks(&s.combining), &['\u{301}', '\u{308}']);
1826 assert_eq!(s.cursor_col, 1);
1827 }
1828
1829 #[test]
1835 fn a_mark_after_a_margin_flush_wide_glyph_lands_on_the_lead() {
1836 let mut g = PaneGrid::new(20, 3);
1837 g.feed("A".repeat(18).as_bytes());
1838 g.feed("你\u{301}".as_bytes());
1839 let s = g.snapshot();
1840 assert_eq!(s.cells[0][18].ch, '你', "lead at col 18");
1841 assert_eq!(
1842 s.cells[0][18].marks(&s.combining),
1843 &['\u{301}'],
1844 "the mark must attach to the LEAD, not the continuation"
1845 );
1846 assert!(
1847 s.cells[0][19].marks(&s.combining).is_empty(),
1848 "the continuation owns no marks"
1849 );
1850 }
1851
1852 #[test]
1855 fn a_mark_at_column_zero_is_dropped() {
1856 let mut g = PaneGrid::new(20, 3);
1857 g.feed("\u{301}".as_bytes());
1858 let s = g.snapshot();
1859 assert_eq!(s.cursor_col, 0, "no column consumed");
1860 assert!(s.combining.is_empty(), "no table entry minted");
1861 assert_eq!(s.cells[0][0].ch, ' ');
1862 }
1863
1864 #[test]
1867 fn rep_after_a_mark_repeats_the_base_glyph() {
1868 let mut g = PaneGrid::new(20, 3);
1869 g.feed("e\u{301}".as_bytes());
1870 g.feed(b"\x1b[2b");
1871 let s = g.snapshot();
1872 assert_eq!(s.cells[0][1].ch, 'e', "REP repeats the base, not the mark");
1873 assert_eq!(s.cells[0][2].ch, 'e');
1874 }
1875
1876 #[test]
1879 fn marks_survive_a_to_ansi_round_trip() {
1880 let mut a = PaneGrid::new(20, 3);
1881 a.feed("e\u{301}X".as_bytes());
1882 let first = a.snapshot();
1883
1884 let mut b = PaneGrid::new(20, 3);
1885 b.feed(&first.to_ansi());
1886 let second = b.snapshot();
1887
1888 assert_eq!(second.cells[0][0].ch, 'e');
1889 assert_eq!(second.cells[0][0].marks(&second.combining), &['\u{301}']);
1890 assert_eq!(second.cells[0][1].ch, 'X');
1891 }
1892
1893 #[test]
1897 fn to_ansi_round_trips_wide_glyphs_without_drift() {
1898 let mut a = PaneGrid::new(20, 3);
1899 a.feed("你好X".as_bytes());
1900 let first = a.snapshot();
1901
1902 let mut b = PaneGrid::new(20, 3);
1903 b.feed(&first.to_ansi());
1904 let second = b.snapshot();
1905
1906 for col in 0..20 {
1907 assert_eq!(
1908 first.cells[0][col].ch, second.cells[0][col].ch,
1909 "col {col} drifted across a to_ansi round-trip"
1910 );
1911 assert_eq!(
1912 first.cells[0][col].width, second.cells[0][col].width,
1913 "col {col} width drifted across a to_ansi round-trip"
1914 );
1915 }
1916 }
1917}
1918
1919#[cfg(test)]
1944mod graphics_rows {
1945 use super::*;
1946
1947 #[test]
1948 fn a_sixel_payload_reaches_the_snapshot() {
1949 let mut g = PaneGrid::new(80, 24);
1950 g.feed(b"\x1bPq#0;2;0;0;0#0~~@@vv@@~~@@~~$\x1b\\");
1951 let s = g.snapshot();
1952 assert_eq!(s.graphics.len(), 1, "the sixel must not vanish");
1953 assert_eq!(s.graphics[0].protocol, GraphicProtocol::Sixel);
1954 assert!(!s.graphics[0].data.is_empty());
1955 assert!(!s.graphics[0].truncated);
1956 }
1957
1958 #[test]
1959 fn a_kitty_payload_reaches_the_snapshot_with_its_params_split_off() {
1960 let mut g = PaneGrid::new(80, 24);
1961 g.feed(b"\x1b_Ga=T,f=100,s=2,v=2;iVBORw0KGgo=\x1b\\");
1962 let s = g.snapshot();
1963 assert_eq!(s.graphics.len(), 1, "the kitty image must not vanish");
1964 let img = &s.graphics[0];
1965 assert_eq!(img.protocol, GraphicProtocol::Kitty);
1966 assert_eq!(img.params, "a=T,f=100,s=2,v=2");
1967 assert_eq!(img.data, b"iVBORw0KGgo=".to_vec());
1968 }
1969
1970 #[test]
1975 fn an_apc_split_across_feeds_reassembles() {
1976 let whole = b"\x1b_Ga=T,f=100;PAYLOAD\x1b\\";
1977 for cut in 1..whole.len() {
1978 let mut g = PaneGrid::new(80, 24);
1979 g.feed(&whole[..cut]);
1980 g.feed(&whole[cut..]);
1981 let s = g.snapshot();
1982 assert_eq!(s.graphics.len(), 1, "lost the image when cut at {cut}");
1983 assert_eq!(s.graphics[0].data, b"PAYLOAD".to_vec(), "cut at {cut}");
1984 assert!(
1985 s.to_text_rows().iter().all(|r| r.trim().is_empty()),
1986 "APC bytes leaked into the grid when cut at {cut}"
1987 );
1988 }
1989 }
1990
1991 #[test]
1994 fn a_non_apc_escape_still_reaches_the_parser() {
1995 let mut g = PaneGrid::new(80, 24);
1996 g.feed(b"AB\x1b");
1998 g.feed(b"[1;1HX");
1999 let s = g.snapshot();
2000 assert_eq!(
2001 s.cells[0][0].ch, 'X',
2002 "the CUP that followed a withheld ESC must still be honoured"
2003 );
2004 }
2005
2006 #[test]
2007 fn an_apc_terminated_by_bel_is_accepted() {
2008 let mut g = PaneGrid::new(80, 24);
2009 g.feed(b"\x1b_Ga=T;DATA\x07");
2010 assert_eq!(g.snapshot().graphics.len(), 1, "BEL terminates APC too");
2011 }
2012
2013 #[test]
2016 fn a_kitty_control_command_without_a_payload_is_kept() {
2017 let mut g = PaneGrid::new(80, 24);
2018 g.feed(b"\x1b_Ga=d,d=A\x1b\\");
2019 let s = g.snapshot();
2020 assert_eq!(s.graphics.len(), 1);
2021 assert_eq!(s.graphics[0].params, "a=d,d=A");
2022 assert!(s.graphics[0].data.is_empty());
2023 }
2024
2025 #[test]
2028 fn an_unrecognised_apc_is_dropped_without_reaching_the_grid() {
2029 let mut g = PaneGrid::new(80, 24);
2030 g.feed(b"\x1b_Zsomething-else\x1b\\after");
2031 let s = g.snapshot();
2032 assert!(s.graphics.is_empty(), "not a kitty payload");
2033 assert_eq!(s.cells[0][0].ch, 'a', "the text after it still lands");
2034 }
2035
2036 #[test]
2040 fn an_oversized_payload_is_bounded_and_flagged() {
2041 let mut g = PaneGrid::new(80, 24);
2042 g.feed(b"\x1b_Ga=T;");
2043 let chunk = vec![b'x'; 1024 * 1024];
2045 for _ in 0..10 {
2046 g.feed(&chunk);
2047 }
2048 g.feed(b"\x1b\\");
2049 let s = g.snapshot();
2050 assert_eq!(s.graphics.len(), 1);
2051 assert!(s.graphics[0].truncated, "the cut must be visible");
2052 assert!(
2053 s.graphics[0].data.len() <= GRAPHIC_PAYLOAD_MAX + 1,
2054 "payload not bounded: {}",
2055 s.graphics[0].data.len()
2056 );
2057 }
2058
2059 #[test]
2062 fn image_bytes_leave_no_residue_in_the_grid() {
2063 let mut g = PaneGrid::new(80, 24);
2064 g.feed(b"before|");
2065 g.feed(b"\x1b_Ga=T,f=100;iVBORw0KGgo=\x1b\\");
2066 g.feed(b"\x1bPq#0;2;0;0;0#0~~$\x1b\\");
2067 g.feed(b"|after");
2068 let row0 = g.snapshot().to_text_rows().into_iter().next().unwrap();
2069 assert_eq!(row0.trim_end(), "before||after");
2070 }
2071}
2072
2073#[cfg(test)]
2074mod mode_rows {
2075 use super::*;
2076
2077 #[test]
2078 fn a_fresh_pane_reports_xterm_defaults() {
2079 let g = PaneGrid::new(80, 24);
2080 let m = g.modes();
2081 assert!(m.autowrap.enabled(), "DECAWM is ON by default per xterm");
2082 assert!(m.cursor_visible.enabled());
2083 assert!(!m.bracketed_paste.enabled());
2084 assert!(!m.sync_output.enabled());
2085 assert!(!m.mouse.is_on());
2086 }
2087
2088 #[test]
2090 fn bracketed_paste_is_tracked() {
2091 let mut g = PaneGrid::new(80, 24);
2092 assert!(!g.modes().bracketed_paste.enabled());
2093 g.feed(b"\x1b[?2004h");
2094 assert!(g.modes().bracketed_paste.enabled(), "DEC 2004 set");
2095 g.feed(b"\x1b[?2004l");
2096 assert!(!g.modes().bracketed_paste.enabled(), "DEC 2004 reset");
2097 }
2098
2099 #[test]
2100 fn the_remaining_flag_modes_are_tracked() {
2101 let mut g = PaneGrid::new(80, 24);
2102 g.feed(b"\x1b[?1004h\x1b[?2026h\x1b[?1006h\x1b[?7l\x1b[?1h\x1b[?25l");
2103 let m = g.modes();
2104 assert!(m.focus_reporting.enabled(), "DEC 1004");
2105 assert!(m.sync_output.enabled(), "DEC 2026");
2106 assert!(m.mouse_sgr.enabled(), "DEC 1006");
2107 assert!(!m.autowrap.enabled(), "DEC 7 reset");
2108 assert!(m.cursor_keys.enabled(), "DEC 1 (DECCKM)");
2109 assert!(!m.cursor_visible.enabled(), "DEC 25 reset");
2110 }
2111
2112 #[test]
2116 fn mouse_tracking_levels_replace_rather_than_accumulate() {
2117 let mut g = PaneGrid::new(80, 24);
2118 g.feed(b"\x1b[?1000h");
2119 assert_eq!(g.modes().mouse, MouseTracking::Click);
2120 g.feed(b"\x1b[?1003h");
2121 assert_eq!(
2122 g.modes().mouse,
2123 MouseTracking::Motion,
2124 "the later level replaces the earlier one"
2125 );
2126 g.feed(b"\x1b[?1003l");
2127 assert_eq!(g.modes().mouse, MouseTracking::Off);
2128 }
2129
2130 #[test]
2131 fn alt_screen_is_reported_as_a_mode() {
2132 let mut g = PaneGrid::new(80, 24);
2133 assert!(!g.modes().alt_screen.enabled());
2134 g.feed(b"\x1b[?1049h");
2135 assert!(g.modes().alt_screen.enabled());
2136 g.feed(b"\x1b[?1049l");
2137 assert!(!g.modes().alt_screen.enabled());
2138 }
2139}
2140
2141#[cfg(test)]
2142mod host_role_rows {
2143 use super::*;
2144
2145 #[test]
2150 fn a_relay_answers_nothing_at_all() {
2151 let mut g = PaneGrid::new(80, 24);
2152 g.feed(b"\x1b[6n\x1b[5n\x1b[c\x1b[>c");
2154 assert!(
2155 g.take_response().is_none(),
2156 "a Relay must stay byte-for-byte silent — otherwise the shipped \
2157 mado+tear pair produces two answers per query"
2158 );
2159 }
2160
2161 #[test]
2162 fn a_host_answers_cursor_position_one_based() {
2163 let mut g = PaneGrid::new(80, 24);
2164 g.set_host_role(HostRole::Host);
2165 g.feed(b"hi\r\n");
2166 g.feed(b"\x1b[6n");
2167 let r = g.take_response().expect("host must answer CPR");
2168 assert_eq!(r, b"\x1b[2;1R".to_vec());
2170 }
2171
2172 #[test]
2176 fn a_host_reports_the_clamped_column_after_a_margin_flush_wide_glyph() {
2177 let mut g = PaneGrid::new(20, 3);
2178 g.set_host_role(HostRole::Host);
2179 g.feed("A".repeat(18).as_bytes());
2180 g.feed("你".as_bytes());
2181 g.feed(b"\x1b[6n");
2182 let r = g.take_response().expect("host must answer CPR");
2183 assert_eq!(r, b"\x1b[1;20R".to_vec(), "column is 1-based and clamped");
2184 }
2185
2186 #[test]
2187 fn a_host_answers_device_status_and_both_device_attributes() {
2188 let mut g = PaneGrid::new(80, 24);
2189 g.set_host_role(HostRole::Host);
2190
2191 g.feed(b"\x1b[5n");
2192 assert_eq!(g.take_response().unwrap(), TearCaps::STATUS_OK.to_vec());
2193
2194 g.feed(b"\x1b[c");
2195 assert_eq!(g.take_response().unwrap(), TearCaps::PRIMARY_DA.to_vec());
2196
2197 g.feed(b"\x1b[>c");
2200 assert_eq!(g.take_response().unwrap(), TearCaps::SECONDARY_DA.to_vec());
2201 }
2202
2203 #[test]
2207 fn a_query_leaves_no_residue_in_the_rendered_grid() {
2208 for role in [HostRole::Relay, HostRole::Host] {
2209 let mut g = PaneGrid::new(80, 24);
2210 g.set_host_role(role);
2211 g.feed(b"before|");
2212 g.feed(b"\x1b[6n");
2213 g.feed(b"|after");
2214 let row0 = g.snapshot().to_text_rows().into_iter().next().unwrap();
2215 assert_eq!(
2216 row0.trim_end(),
2217 "before||after",
2218 "query bytes must never reach the grid ({role:?})"
2219 );
2220 }
2221 }
2222
2223 #[test]
2224 fn taking_a_response_drains_it() {
2225 let mut g = PaneGrid::new(80, 24);
2226 g.set_host_role(HostRole::Host);
2227 g.feed(b"\x1b[5n");
2228 assert!(g.take_response().is_some());
2229 assert!(g.take_response().is_none(), "a reply is delivered once");
2230 }
2231}
2232
2233#[cfg(test)]
2273mod perf_measurements {
2274 use super::*;
2275 use std::time::Instant;
2276
2277 #[test]
2281 #[ignore = "measurement, not an assertion"]
2282 fn snapshot_cost_by_scrollback_depth() {
2283 for rows in [1_000usize, 10_000, 100_000] {
2284 let mut g = PaneGrid::new(80, 24);
2285 for i in 0..rows {
2286 g.feed(format!("line {i} with some ordinary ascii payload\r\n").as_bytes());
2287 }
2288 let _ = g.snapshot();
2290 let t = Instant::now();
2291 const N: u32 = 10;
2292 for _ in 0..N {
2293 let s = g.snapshot();
2294 std::hint::black_box(&s);
2295 }
2296 let per = t.elapsed() / N;
2297 let sb = g.snapshot().scrollback.len();
2298 println!("scrollback {sb:>7} rows -> snapshot {per:?} each");
2299 }
2300 }
2301
2302 #[test]
2305 #[ignore = "measurement, not an assertion"]
2306 fn snapshot_cost_with_and_without_combining_marks() {
2307 let mut plain = PaneGrid::new(80, 24);
2308 let mut marked = PaneGrid::new(80, 24);
2309 for _ in 0..5_000 {
2310 plain.feed(b"plain ascii line here\r\n");
2311 marked.feed("ma\u{301}rked li\u{308}ne he\u{301}re\r\n".as_bytes());
2312 }
2313 for (name, g) in [("plain", &plain), ("marked", &marked)] {
2314 let _ = g.snapshot();
2315 let t = Instant::now();
2316 const N: u32 = 10;
2317 for _ in 0..N {
2318 std::hint::black_box(g.snapshot());
2319 }
2320 let s = g.snapshot();
2321 println!(
2322 "{name:>7}: snapshot {:?} each, combining table {} entries",
2323 t.elapsed() / N,
2324 s.combining.len()
2325 );
2326 }
2327 }
2328}
2329
2330#[cfg(test)]
2331mod tests {
2332 use super::*;
2333 use tear_types::pane_snapshot::{CellAttrs, Color};
2334
2335 #[test]
2336 fn print_plain_text() {
2337 let mut g = PaneGrid::new(10, 3);
2338 g.feed(b"hi");
2339 let snap = g.snapshot();
2340 assert_eq!(snap.cells[0][0].ch, 'h');
2341 assert_eq!(snap.cells[0][1].ch, 'i');
2342 assert_eq!(snap.cursor_row, 0);
2343 assert_eq!(snap.cursor_col, 2);
2344 }
2345
2346 #[test]
2347 fn newline_advances_row() {
2348 let mut g = PaneGrid::new(10, 3);
2349 g.feed(b"hi\r\nworld");
2350 let snap = g.snapshot();
2351 assert_eq!(snap.cells[0][0].ch, 'h');
2352 assert_eq!(snap.cells[1][0].ch, 'w');
2353 assert_eq!(snap.cursor_row, 1);
2354 assert_eq!(snap.cursor_col, 5);
2355 }
2356
2357 #[test]
2358 fn cursor_move_csi_cup() {
2359 let mut g = PaneGrid::new(10, 5);
2360 g.feed(b"\x1b[3;5H");
2361 let snap = g.snapshot();
2362 assert_eq!(snap.cursor_row, 2);
2363 assert_eq!(snap.cursor_col, 4);
2364 }
2365
2366 #[test]
2367 fn erase_in_display_clear_all() {
2368 let mut g = PaneGrid::new(5, 2);
2369 g.feed(b"abcde\r\nfghij");
2370 g.feed(b"\x1b[2J");
2371 let snap = g.snapshot();
2372 for row in snap.cells {
2373 for cell in row {
2374 assert_eq!(cell.ch, ' ');
2375 }
2376 }
2377 }
2378
2379 #[test]
2380 fn auto_wrap_overflows_to_next_row() {
2381 let mut g = PaneGrid::new(3, 3);
2382 g.feed(b"abcdef");
2383 let snap = g.snapshot();
2384 assert_eq!(snap.cells[0][2].ch, 'c');
2385 assert_eq!(snap.cells[1][0].ch, 'd');
2386 }
2387
2388 #[test]
2389 fn scroll_into_scrollback_on_overflow() {
2390 let mut g = PaneGrid::with_scrollback(3, 2, 100);
2391 g.feed(b"a\r\nb\r\nc");
2392 let snap = g.snapshot();
2393 assert_eq!(snap.cells[0][0].ch, 'b');
2395 assert_eq!(snap.cells[1][0].ch, 'c');
2396 assert!(g.scrollback_len() >= 1);
2397 }
2398
2399 #[test]
2400 fn sgr_red_foreground_sticks_through_a_word() {
2401 let mut g = PaneGrid::new(10, 1);
2402 g.feed(b"\x1b[31mRED\x1b[0m");
2403 let snap = g.snapshot();
2404 let red = tear_types::pane_snapshot::ANSI_COLORS[1];
2405 assert_eq!(snap.cells[0][0].ch, 'R');
2406 assert_eq!(snap.cells[0][0].fg, red);
2407 assert_eq!(snap.cells[0][1].fg, red);
2408 assert_eq!(snap.cells[0][2].fg, red);
2409 }
2410
2411 #[test]
2412 fn sgr_truecolor_fg() {
2413 let mut g = PaneGrid::new(10, 1);
2414 g.feed(b"\x1b[38;2;200;100;50mORANGE");
2415 let snap = g.snapshot();
2416 assert_eq!(snap.cells[0][0].fg, Color::new(200, 100, 50));
2417 assert_eq!(snap.cells[0][5].fg, Color::new(200, 100, 50));
2418 }
2419
2420 #[test]
2421 fn sgr_256_color_index() {
2422 let mut g = PaneGrid::new(10, 1);
2423 g.feed(b"\x1b[38;5;196mX");
2424 let snap = g.snapshot();
2425 assert!(snap.cells[0][0].fg.r > 200);
2427 }
2428
2429 #[test]
2430 fn sgr_bold_attr_sticks() {
2431 let mut g = PaneGrid::new(10, 1);
2432 g.feed(b"\x1b[1mBOLD");
2433 let snap = g.snapshot();
2434 assert!(snap.cells[0][0].attrs.contains(CellAttrs::BOLD));
2435 }
2436
2437 #[test]
2447 fn no_sgr_form_leaves_underline_stuck_on_the_pen() {
2448 let cases: &[(&str, &[u8])] = &[
2449 ("4m then 24m", b"\x1b[4mU\x1b[24mX"),
2450 ("4m then 0m", b"\x1b[4mU\x1b[0mX"),
2451 ("4:3m then 4:0m", b"\x1b[4:3mU\x1b[4:0mX"),
2452 ("4:3m then 24m", b"\x1b[4:3mU\x1b[24mX"),
2453 ("21m (double-underline)", b"\x1b[21mX"),
2454 ("58:2::255:0:0 then 59m", b"\x1b[58:2::255:0:0mU\x1b[59mX"),
2455 ("fg truecolor semicolon", b"\x1b[38;2;177;185;249mX"),
2456 ("fg truecolor COLON", b"\x1b[38:2::177:185:249mX"),
2457 ("fg 256 semicolon", b"\x1b[38;5;4mX"),
2458 ("fg 256 COLON", b"\x1b[38:5:4mX"),
2459 ("bold+italic only", b"\x1b[1;3mX"),
2460 ];
2461 let mut leaked = Vec::new();
2462 for (name, bytes) in cases {
2463 let mut g = PaneGrid::new(20, 1);
2464 g.feed(bytes);
2465 let snap = g.snapshot();
2466 let marker = snap.cells[0]
2468 .iter()
2469 .rev()
2470 .find(|c| c.ch == 'X')
2471 .expect("marker X present");
2472 if marker.attrs.contains(CellAttrs::UNDERLINE) {
2473 leaked.push(*name);
2474 }
2475 }
2476 assert!(
2477 leaked.is_empty(),
2478 "these SGR forms leave UNDERLINE stuck on the pen: {leaked:?}"
2479 );
2480 }
2481
2482 fn marker_attrs(seq: &[u8]) -> CellAttrs {
2484 let mut g = PaneGrid::new(20, 1);
2485 let mut buf = seq.to_vec();
2486 buf.push(b'X');
2487 g.feed(&buf);
2488 g.snapshot().cells[0]
2489 .iter()
2490 .rev()
2491 .find(|c| c.ch == 'X')
2492 .expect("marker X present")
2493 .attrs
2494 }
2495
2496 #[test]
2502 fn xtmodkeys_is_not_sgr() {
2503 let attrs = marker_attrs(b"\x1b[>4;2m");
2504 assert!(
2505 !attrs.contains(CellAttrs::UNDERLINE),
2506 "CSI >4;2m (XTMODKEYS) must not set UNDERLINE"
2507 );
2508 assert!(
2509 !attrs.contains(CellAttrs::DIM),
2510 "CSI >4;2m (XTMODKEYS) must not set DIM"
2511 );
2512 assert_eq!(attrs, CellAttrs::NONE, "XTMODKEYS must touch no attribute");
2513 }
2514
2515 #[test]
2521 fn private_parameter_csi_never_runs_the_standard_command() {
2522 for seq in [
2523 &b"\x1b[>4;2m"[..],
2524 &b"\x1b[>1m"[..],
2525 &b"\x1b[?4m"[..],
2526 &b"\x1b[=4m"[..],
2527 &b"\x1b[<4m"[..],
2528 ] {
2529 assert_eq!(
2530 marker_attrs(seq),
2531 CellAttrs::NONE,
2532 "private CSI {:?} must not act as SGR",
2533 String::from_utf8_lossy(seq),
2534 );
2535 }
2536
2537 for seq in [
2538 &b"\x1b[>5A"[..],
2539 &b"\x1b[>5C"[..],
2540 &b"\x1b[?5G"[..],
2541 &b"\x1b[>2;3H"[..],
2542 ] {
2543 let mut g = PaneGrid::new(20, 3);
2544 g.feed(b"\x1b[H");
2545 g.feed(seq);
2546 let snap = g.snapshot();
2547 assert_eq!(
2548 (snap.cursor_row, snap.cursor_col),
2549 (0, 0),
2550 "private CSI {:?} must not move the cursor",
2551 String::from_utf8_lossy(seq),
2552 );
2553 }
2554
2555 let mut g = PaneGrid::new(20, 1);
2556 g.feed(b"keep\x1b[H\x1b[?2J\x1b[?0K");
2557 let row: String = g.snapshot().cells[0].iter().map(|c| c.ch).collect();
2558 assert!(
2559 row.starts_with("keep"),
2560 "private CSI ?J/?K must not erase; row was {row:?}"
2561 );
2562 }
2563
2564 fn marker_fg(seq: &[u8]) -> Color {
2565 let mut g = PaneGrid::new(20, 1);
2566 let mut buf = seq.to_vec();
2567 buf.push(b'X');
2568 g.feed(&buf);
2569 g.snapshot().cells[0]
2570 .iter()
2571 .rev()
2572 .find(|c| c.ch == 'X')
2573 .expect("marker X present")
2574 .fg
2575 }
2576
2577 #[test]
2585 fn semicolon_and_colon_extended_colour_agree() {
2586 let cases: &[(&[u8], &[u8], Color)] = &[
2587 (
2588 b"\x1b[38;2;248;248;242m",
2589 b"\x1b[38:2::248:248:242m",
2590 Color::new(248, 248, 242),
2591 ),
2592 (
2593 b"\x1b[38;2;177;185;249m",
2594 b"\x1b[38:2::177:185:249m",
2595 Color::new(177, 185, 249),
2596 ),
2597 (
2599 b"\x1b[38;2;4;4;4m",
2600 b"\x1b[38:2::4:4:4m",
2601 Color::new(4, 4, 4),
2602 ),
2603 ];
2604 for (semi, colon, want) in cases {
2605 assert_eq!(marker_fg(semi), *want, "semicolon form {semi:?}");
2606 assert_eq!(marker_fg(colon), *want, "COLON form {colon:?}");
2607 }
2608 assert_eq!(
2610 marker_fg(b"\x1b[38:2:10:20:30m"),
2611 Color::new(10, 20, 30),
2612 "5-slot colon truecolor"
2613 );
2614 }
2615
2616 #[test]
2621 fn extended_colour_never_leaks_an_attribute() {
2622 let mut leaked = Vec::new();
2623 for seq in [
2624 &b"\x1b[38;2;4;4;4m"[..],
2625 &b"\x1b[38:2::4:4:4m"[..],
2626 &b"\x1b[48;2;4;4;4m"[..],
2627 &b"\x1b[48:2::4:4:4m"[..],
2628 &b"\x1b[38;5;4m"[..],
2629 &b"\x1b[38:5:4m"[..],
2630 &b"\x1b[48;5;4m"[..],
2631 &b"\x1b[58;5;4m"[..],
2634 &b"\x1b[58;2;4;4;4m"[..],
2635 &b"\x1b[58:2::255:0:0m"[..],
2636 &b"\x1b[59m"[..],
2637 &b"\x1b[38m"[..],
2639 &b"\x1b[38;2m"[..],
2640 &b"\x1b[38;5m"[..],
2641 ] {
2642 if marker_attrs(seq) != CellAttrs::NONE {
2643 leaked.push(String::from_utf8_lossy(seq).replace('\x1b', "ESC"));
2644 }
2645 }
2646 assert!(
2647 leaked.is_empty(),
2648 "these forms leaked an attribute: {leaked:?}"
2649 );
2650 }
2651
2652 #[test]
2657 fn styled_underline_subparams() {
2658 assert!(marker_attrs(b"\x1b[4:3m").contains(CellAttrs::UNDERLINE));
2659 assert!(
2660 !marker_attrs(b"\x1b[4:3m").contains(CellAttrs::ITALIC),
2661 "4:3 is a curly underline, not underline + italic"
2662 );
2663 assert!(!marker_attrs(b"\x1b[4:0m").contains(CellAttrs::UNDERLINE));
2664 assert!(!marker_attrs(b"\x1b[4mU\x1b[4:0m").contains(CellAttrs::UNDERLINE));
2665 }
2666
2667 #[test]
2670 fn plain_sgr_still_works() {
2671 assert!(marker_attrs(b"\x1b[1m").contains(CellAttrs::BOLD));
2672 assert!(marker_attrs(b"\x1b[3m").contains(CellAttrs::ITALIC));
2673 assert!(marker_attrs(b"\x1b[1;3m").contains(CellAttrs::BOLD));
2674 assert!(marker_attrs(b"\x1b[1;3m").contains(CellAttrs::ITALIC));
2675 assert_eq!(marker_attrs(b"\x1b[1;3m\x1b[0m"), CellAttrs::NONE);
2676 assert_eq!(
2677 marker_attrs(b"\x1b[1m\x1b[m"),
2678 CellAttrs::NONE,
2679 "bare ESC[m resets"
2680 );
2681 assert_eq!(marker_fg(b"\x1b[31m"), default_ansi_palette()[1]);
2683 assert_eq!(marker_fg(b"\x1b[31m\x1b[39m"), Color::WHITE);
2684 }
2685
2686 #[test]
2689 fn dec_private_modes_still_dispatch() {
2690 let mut g = PaneGrid::new(20, 2);
2691 g.feed(b"\x1b[?25l");
2692 assert!(
2693 !g.snapshot().cursor_visible,
2694 "DECTCEM reset must hide cursor"
2695 );
2696 g.feed(b"\x1b[?25h");
2697 assert!(g.snapshot().cursor_visible, "DECTCEM set must show cursor");
2698 }
2699
2700 #[test]
2701 fn sgr_reset_returns_default_pen() {
2702 let mut g = PaneGrid::new(10, 1);
2703 g.feed(b"\x1b[31m\x1b[0mX");
2704 let snap = g.snapshot();
2705 assert_eq!(snap.cells[0][0].fg, Color::WHITE);
2706 }
2707
2708 #[test]
2709 fn alt_screen_isolates_writes_and_preserves_primary() {
2710 let mut g = PaneGrid::new(5, 2);
2711 g.feed(b"AAAAA\r\nBBBBB");
2712 g.feed(b"\x1b[?1049h");
2714 let alt_snap = g.snapshot();
2716 assert!(alt_snap.alt_screen_active);
2717 assert_eq!(alt_snap.cells[0][0].ch, ' ');
2718 g.feed(b"ZZZZZ");
2720 g.feed(b"\x1b[?1049l");
2722 let primary_snap = g.snapshot();
2723 assert!(!primary_snap.alt_screen_active);
2724 assert_eq!(primary_snap.cells[0][0].ch, 'A');
2725 assert_eq!(primary_snap.cells[1][0].ch, 'B');
2726 }
2727
2728 #[test]
2729 fn save_restore_cursor_via_decsc_decrc() {
2730 let mut g = PaneGrid::new(10, 5);
2731 g.feed(b"\x1b[3;5H");
2732 g.feed(b"\x1b7"); g.feed(b"\x1b[1;1H");
2734 g.feed(b"\x1b8"); let snap = g.snapshot();
2736 assert_eq!(snap.cursor_row, 2);
2737 assert_eq!(snap.cursor_col, 4);
2738 }
2739
2740 #[test]
2741 fn snapshot_text_helpers() {
2742 let mut g = PaneGrid::new(5, 2);
2743 g.feed(b"hi\r\nbye");
2744 let snap = g.snapshot();
2745 let rows = snap.to_text_rows();
2746 assert_eq!(rows[0], "hi ");
2747 assert_eq!(rows[1], "bye ");
2748 }
2749
2750 #[test]
2751 fn osc_2_sets_window_title() {
2752 let mut g = PaneGrid::new(10, 1);
2753 g.feed(b"\x1b]2;hello world\x07");
2754 assert_eq!(g.title(), Some("hello world"));
2755 let snap = g.snapshot();
2756 assert_eq!(snap.title.as_deref(), Some("hello world"));
2757 }
2758
2759 #[test]
2760 fn dec_25_hides_cursor() {
2761 let mut g = PaneGrid::new(10, 1);
2762 let snap_before = g.snapshot();
2763 assert!(snap_before.cursor_visible);
2764 g.feed(b"\x1b[?25l");
2765 let snap_hidden = g.snapshot();
2766 assert!(!snap_hidden.cursor_visible);
2767 g.feed(b"\x1b[?25h");
2768 let snap_back = g.snapshot();
2769 assert!(snap_back.cursor_visible);
2770 }
2771
2772 #[test]
2773 fn ich_inserts_cells_and_shifts_right() {
2774 let mut g = PaneGrid::new(6, 1);
2775 g.feed(b"abcdef");
2776 g.feed(b"\x1b[1;1H"); g.feed(b"\x1b[2@"); let snap = g.snapshot();
2779 assert_eq!(snap.cells[0][0].ch, ' ');
2780 assert_eq!(snap.cells[0][1].ch, ' ');
2781 assert_eq!(snap.cells[0][2].ch, 'a');
2782 assert_eq!(snap.cells[0][3].ch, 'b');
2783 }
2784
2785 #[test]
2786 fn dch_deletes_cells_and_shifts_left() {
2787 let mut g = PaneGrid::new(6, 1);
2788 g.feed(b"abcdef");
2789 g.feed(b"\x1b[1;2H"); g.feed(b"\x1b[2P"); let snap = g.snapshot();
2792 assert_eq!(snap.cells[0][0].ch, 'a');
2793 assert_eq!(snap.cells[0][1].ch, 'd');
2794 assert_eq!(snap.cells[0][2].ch, 'e');
2795 assert_eq!(snap.cells[0][3].ch, 'f');
2796 }
2797
2798 #[test]
2799 fn ech_erases_in_place() {
2800 let mut g = PaneGrid::new(6, 1);
2801 g.feed(b"abcdef");
2802 g.feed(b"\x1b[1;2H");
2803 g.feed(b"\x1b[2X"); let snap = g.snapshot();
2805 assert_eq!(snap.cells[0][0].ch, 'a');
2806 assert_eq!(snap.cells[0][1].ch, ' ');
2807 assert_eq!(snap.cells[0][2].ch, ' ');
2808 assert_eq!(snap.cells[0][3].ch, 'd');
2809 }
2810
2811 #[test]
2812 fn il_dl_insert_delete_line() {
2813 let mut g = PaneGrid::new(3, 4);
2814 g.feed(b"AAA\r\nBBB\r\nCCC\r\nDDD");
2815 g.feed(b"\x1b[2;1H"); g.feed(b"\x1b[1L"); let snap1 = g.snapshot();
2818 assert_eq!(snap1.cells[0][0].ch, 'A');
2821 assert_eq!(snap1.cells[1][0].ch, ' ');
2822 assert_eq!(snap1.cells[2][0].ch, 'B');
2823 g.feed(b"\x1b[1M"); let snap2 = g.snapshot();
2826 assert_eq!(snap2.cells[1][0].ch, 'B');
2827 }
2828
2829 #[test]
2830 fn rep_repeats_last_printable_char() {
2831 let mut g = PaneGrid::new(10, 1);
2832 g.feed(b"X\x1b[5b"); let snap = g.snapshot();
2834 for c in 0..6 {
2835 assert_eq!(snap.cells[0][c].ch, 'X', "col {c}");
2836 }
2837 }
2838
2839 #[test]
2840 fn irm_inserts_on_print() {
2841 let mut g = PaneGrid::new(6, 1);
2842 g.feed(b"abcdef");
2843 g.feed(b"\x1b[1;1H"); g.feed(b"\x1b[4hZ"); let snap = g.snapshot();
2846 assert_eq!(snap.cells[0][0].ch, 'Z');
2847 assert_eq!(snap.cells[0][1].ch, 'a');
2848 assert_eq!(snap.cells[0][2].ch, 'b');
2849 }
2850
2851 #[test]
2852 fn ri_scrolls_down_at_top_of_region() {
2853 let mut g = PaneGrid::new(3, 3);
2854 g.feed(b"a\r\nb\r\nc"); g.feed(b"\x1b[1;1H"); g.feed(b"\x1bM"); let snap = g.snapshot();
2858 assert_eq!(snap.cells[0][0].ch, ' ');
2859 assert_eq!(snap.cells[1][0].ch, 'a');
2860 }
2861
2862 #[test]
2863 fn resize_preserves_top_left_content() {
2864 let mut g = PaneGrid::new(5, 3);
2865 g.feed(b"HELLO\r\nWORLD\r\nTHERE");
2866 g.resize(4, 2);
2868 let snap = g.snapshot();
2869 assert_eq!(snap.cols, 4);
2870 assert_eq!(snap.rows, 2);
2871 assert_eq!(snap.cells[0][0].ch, 'H');
2872 assert_eq!(snap.cells[0][3].ch, 'L');
2873 assert_eq!(snap.cells[1][0].ch, 'W');
2874 assert_eq!(snap.cursor_row, 1);
2876 assert_eq!(snap.cursor_col, 3);
2877 }
2878
2879 #[test]
2880 fn resize_grow_pads_with_blanks() {
2881 let mut g = PaneGrid::new(3, 2);
2882 g.feed(b"AB\r\nCD");
2883 g.resize(5, 4);
2884 let snap = g.snapshot();
2885 assert_eq!(snap.cols, 5);
2886 assert_eq!(snap.rows, 4);
2887 assert_eq!(snap.cells[0][0].ch, 'A');
2888 assert_eq!(snap.cells[0][3].ch, ' ');
2889 assert_eq!(snap.cells[2][0].ch, ' ');
2890 }
2891
2892 #[test]
2893 fn scrollback_caps_at_configured_size() {
2894 let mut g = PaneGrid::with_scrollback(3, 2, 3);
2895 for i in 0..10u8 {
2897 g.feed(&[b'a' + i, b'\r', b'\n']);
2898 }
2899 assert!(g.scrollback_len() <= 3);
2900 }
2901
2902 #[test]
2905 fn sgr_truecolor_with_missing_params_does_not_panic() {
2906 let mut g = PaneGrid::new(5, 1);
2908 g.feed(b"\x1b[38;2;200mX");
2909 let snap = g.snapshot();
2911 assert_eq!(snap.cells[0][0].ch, 'X');
2912 }
2913
2914 #[test]
2915 fn sgr_256_with_missing_index_does_not_panic() {
2916 let mut g = PaneGrid::new(5, 1);
2917 g.feed(b"\x1b[38;5mX"); let snap = g.snapshot();
2919 assert_eq!(snap.cells[0][0].ch, 'X');
2920 }
2921
2922 #[test]
2923 fn sgr_unknown_param_is_ignored() {
2924 let mut g = PaneGrid::new(5, 1);
2925 g.feed(b"\x1b[999mX");
2926 let snap = g.snapshot();
2927 assert_eq!(snap.cells[0][0].ch, 'X');
2928 assert_eq!(snap.cells[0][0].fg, Color::WHITE);
2930 }
2931
2932 #[test]
2933 fn sgr_empty_params_resets() {
2934 let mut g = PaneGrid::new(5, 1);
2935 g.feed(b"\x1b[31m"); g.feed(b"\x1b[m"); g.feed(b"X");
2938 let snap = g.snapshot();
2939 assert_eq!(snap.cells[0][0].fg, Color::WHITE);
2940 }
2941
2942 #[test]
2943 fn sgr_bright_bg_100_107() {
2944 let mut g = PaneGrid::new(3, 1);
2945 g.feed(b"\x1b[104mX"); let snap = g.snapshot();
2947 let bright_blue = tear_types::pane_snapshot::ANSI_BRIGHT_COLORS[4];
2948 assert_eq!(snap.cells[0][0].bg, bright_blue);
2949 }
2950
2951 #[test]
2952 fn sgr_disable_attrs_21_to_29() {
2953 let mut g = PaneGrid::new(3, 1);
2954 g.feed(b"\x1b[1;4;7m"); g.feed(b"\x1b[22;24;27m"); g.feed(b"X");
2957 let snap = g.snapshot();
2958 assert!(snap.cells[0][0].attrs.is_empty());
2959 }
2960
2961 #[test]
2964 fn ech_past_end_of_row_clamps() {
2965 let mut g = PaneGrid::new(3, 1);
2966 g.feed(b"abc");
2967 g.feed(b"\x1b[1;2H"); g.feed(b"\x1b[100X"); let snap = g.snapshot();
2970 assert_eq!(snap.cells[0][0].ch, 'a');
2971 assert_eq!(snap.cells[0][1].ch, ' ');
2972 assert_eq!(snap.cells[0][2].ch, ' ');
2973 }
2974
2975 #[test]
2976 fn ich_at_end_of_row_no_overflow() {
2977 let mut g = PaneGrid::new(3, 1);
2978 g.feed(b"abc");
2979 g.feed(b"\x1b[1;3H"); g.feed(b"\x1b[5@"); let snap = g.snapshot();
2982 assert_eq!(snap.cells[0][0].ch, 'a');
2985 assert_eq!(snap.cells[0][1].ch, 'b');
2986 assert_eq!(snap.cells[0][2].ch, ' ');
2987 }
2988
2989 #[test]
2990 fn dch_more_than_row_clamps() {
2991 let mut g = PaneGrid::new(3, 1);
2992 g.feed(b"abc");
2993 g.feed(b"\x1b[1;1H");
2994 g.feed(b"\x1b[100P"); let snap = g.snapshot();
2996 for c in 0..3 {
2997 assert_eq!(snap.cells[0][c].ch, ' ', "col {c}");
2998 }
2999 }
3000
3001 #[test]
3004 fn osc_with_no_params_is_dropped() {
3005 let mut g = PaneGrid::new(3, 1);
3006 g.feed(b"\x1b]\x07"); let snap = g.snapshot();
3008 assert!(snap.title.is_none());
3009 }
3010
3011 #[test]
3012 fn osc_very_long_title_works() {
3013 let mut g = PaneGrid::new(3, 1);
3014 let long_title: String = "x".repeat(1000);
3015 let payload = format!("\x1b]2;{}\x07", long_title);
3016 g.feed(payload.as_bytes());
3017 assert_eq!(g.title().map(str::len), Some(1000));
3018 }
3019
3020 #[test]
3023 fn dec_1049_save_and_restore_cursor_around_alt_screen() {
3024 let mut g = PaneGrid::new(10, 3);
3025 g.feed(b"AAA\r\nBBB");
3026 g.feed(b"\x1b[?1049h"); g.feed(b"\x1b[5;5H"); let alt = g.snapshot();
3030 assert!(alt.alt_screen_active);
3031 g.feed(b"\x1b[?1049l");
3033 let back = g.snapshot();
3034 assert!(!back.alt_screen_active);
3035 assert_eq!(back.cursor_row, 1);
3036 assert_eq!(back.cursor_col, 3);
3037 assert_eq!(back.cells[0][0].ch, 'A');
3039 assert_eq!(back.cells[1][0].ch, 'B');
3040 }
3041
3042 #[test]
3043 fn dec_25_cursor_visibility_round_trip() {
3044 let mut g = PaneGrid::new(3, 1);
3045 g.feed(b"\x1b[?25l"); assert!(!g.snapshot().cursor_visible);
3047 g.feed(b"\x1b[?25h"); assert!(g.snapshot().cursor_visible);
3049 g.feed(b"\x1b[?25l"); assert!(!g.snapshot().cursor_visible);
3051 }
3052
3053 #[test]
3056 fn bel_does_not_crash_or_consume_cell() {
3057 let mut g = PaneGrid::new(3, 1);
3058 g.feed(b"A\x07B"); let snap = g.snapshot();
3060 assert_eq!(snap.cells[0][0].ch, 'A');
3061 assert_eq!(snap.cells[0][1].ch, 'B');
3062 }
3063
3064 #[test]
3065 fn tab_aligns_to_next_multiple_of_8() {
3066 let mut g = PaneGrid::new(20, 1);
3067 g.feed(b"\tX"); let snap = g.snapshot();
3069 assert_eq!(snap.cells[0][8].ch, 'X');
3070 }
3071
3072 #[test]
3073 fn resize_to_zero_clamps_safely() {
3074 let mut g = PaneGrid::new(5, 3);
3075 g.feed(b"hello");
3076 g.resize(0, 0);
3079 let snap = g.snapshot();
3080 assert_eq!(snap.cursor_row, 0);
3082 assert_eq!(snap.cursor_col, 0);
3083 }
3084
3085 #[test]
3086 fn ris_resets_pen_and_clears_screen() {
3087 let mut g = PaneGrid::new(5, 2);
3088 g.feed(b"\x1b[31m"); g.feed(b"AB\r\nCD");
3090 g.feed(b"\x1bc"); let snap = g.snapshot();
3092 for row in snap.cells {
3093 for cell in row {
3094 assert_eq!(cell.ch, ' ');
3095 assert_eq!(cell.fg, Color::WHITE);
3096 }
3097 }
3098 assert_eq!(snap.cursor_row, 0);
3099 assert_eq!(snap.cursor_col, 0);
3100 }
3101
3102 #[test]
3112 fn cursor_keys_mode_defaults_to_false() {
3113 let g = PaneGrid::new(5, 1);
3114 assert!(!g.cursor_keys_mode());
3115 assert!(!g.snapshot().cursor_keys_mode);
3116 }
3117
3118 #[test]
3119 fn decckm_set_via_csi_question_1_h() {
3120 let mut g = PaneGrid::new(5, 1);
3121 g.feed(b"\x1b[?1h"); assert!(g.cursor_keys_mode());
3123 assert!(g.snapshot().cursor_keys_mode);
3124 }
3125
3126 #[test]
3127 fn decckm_reset_via_csi_question_1_l() {
3128 let mut g = PaneGrid::new(5, 1);
3129 g.feed(b"\x1b[?1h"); g.feed(b"\x1b[?1l"); assert!(!g.cursor_keys_mode());
3132 assert!(!g.snapshot().cursor_keys_mode);
3133 }
3134
3135 #[test]
3136 fn decckm_survives_unrelated_modes() {
3137 let mut g = PaneGrid::new(5, 1);
3138 g.feed(b"\x1b[?1h"); g.feed(b"\x1b[?25l"); g.feed(b"\x1b[?1049h"); assert!(
3142 g.cursor_keys_mode(),
3143 "DECCKM must persist across cursor-visibility + alt-screen toggles"
3144 );
3145 }
3146
3147 #[test]
3148 fn ris_resets_cursor_keys_mode() {
3149 let mut g = PaneGrid::new(5, 1);
3150 g.feed(b"\x1b[?1h"); assert!(g.cursor_keys_mode());
3152 g.feed(b"\x1bc"); assert!(
3154 !g.cursor_keys_mode(),
3155 "RIS must reset DECCKM to normal mode"
3156 );
3157 }
3158
3159 #[test]
3160 fn decckm_multi_param_csi() {
3161 let mut g = PaneGrid::new(5, 1);
3164 g.feed(b"\x1b[?25l"); g.feed(b"\x1b[?1;25h"); assert!(g.cursor_keys_mode());
3167 assert!(g.snapshot().cursor_visible);
3168 }
3169}
3170
3171#[cfg(test)]
3172mod proptests {
3173 use super::*;
3174 use proptest::prelude::*;
3175
3176 proptest! {
3177 #[test]
3181 fn random_bytes_never_panic_and_cursor_stays_in_bounds(
3182 cols in 1usize..=80,
3183 rows in 1usize..=24,
3184 bytes in proptest::collection::vec(any::<u8>(), 0..2048),
3185 ) {
3186 let mut g = PaneGrid::new(cols, rows);
3187 g.feed(&bytes);
3188 let snap = g.snapshot();
3189 prop_assert_eq!(snap.cols, cols);
3190 prop_assert_eq!(snap.rows, rows);
3191 prop_assert_eq!(snap.cells.len(), rows);
3192 for row in &snap.cells {
3193 prop_assert_eq!(row.len(), cols);
3194 }
3195 prop_assert!(snap.cursor_row < rows.max(1));
3196 prop_assert!(snap.cursor_col < cols.max(1));
3197 }
3198
3199 #[test]
3202 fn printable_ascii_runs_fill_cells_in_order(
3203 text in r"[A-Za-z0-9 ]{1,40}",
3204 ) {
3205 let mut g = PaneGrid::new(40, 3);
3206 g.feed(text.as_bytes());
3207 let snap = g.snapshot();
3208 for (i, c) in text.chars().enumerate() {
3209 if i < snap.cols {
3210 prop_assert_eq!(snap.cells[0][i].ch, c);
3211 }
3212 }
3213 }
3214
3215 #[test]
3217 fn snapshot_text_dimensions_match(
3218 cols in 1usize..=120,
3219 rows in 1usize..=40,
3220 bytes in proptest::collection::vec(any::<u8>(), 0..1024),
3221 ) {
3222 let mut g = PaneGrid::new(cols, rows);
3223 g.feed(&bytes);
3224 let snap = g.snapshot();
3225 let text_rows = snap.to_text_rows();
3226 prop_assert_eq!(text_rows.len(), rows);
3227 for row in &text_rows {
3228 prop_assert_eq!(row.chars().count(), cols);
3231 }
3232 }
3233
3234 #[test]
3236 fn resize_keeps_cursor_in_bounds(
3237 cols1 in 1usize..=60,
3238 rows1 in 1usize..=20,
3239 cols2 in 1usize..=60,
3240 rows2 in 1usize..=20,
3241 bytes in proptest::collection::vec(any::<u8>(), 0..512),
3242 ) {
3243 let mut g = PaneGrid::new(cols1, rows1);
3244 g.feed(&bytes);
3245 g.resize(cols2, rows2);
3246 let snap = g.snapshot();
3247 prop_assert_eq!(snap.cols, cols2);
3248 prop_assert_eq!(snap.rows, rows2);
3249 prop_assert!(snap.cursor_row < rows2.max(1));
3250 prop_assert!(snap.cursor_col < cols2.max(1));
3251 }
3252 }
3253}