1use std::collections::VecDeque;
60use unicode_width::UnicodeWidthChar;
61
62const WIDE_CONTINUATION: char = '\0';
64
65fn dec_graphics_char(ch: char) -> char {
70 match ch {
71 '`' => '\u{25C6}', 'a' => '\u{2592}', 'b' => '\u{2409}', 'c' => '\u{240C}', 'd' => '\u{240D}', 'e' => '\u{240A}', 'f' => '\u{00B0}', 'g' => '\u{00B1}', 'h' => '\u{2424}', 'i' => '\u{240B}', 'j' => '\u{2518}', 'k' => '\u{2510}', 'l' => '\u{250C}', 'm' => '\u{2514}', 'n' => '\u{253C}', 'o' => '\u{23BA}', 'p' => '\u{23BB}', 'q' => '\u{2500}', 'r' => '\u{23BC}', 's' => '\u{23BD}', 't' => '\u{251C}', 'u' => '\u{2524}', 'v' => '\u{2534}', 'w' => '\u{252C}', 'x' => '\u{2502}', 'y' => '\u{2264}', 'z' => '\u{2265}', '{' => '\u{03C0}', '|' => '\u{2260}', '}' => '\u{00A3}', '~' => '\u{00B7}', _ => ch,
103 }
104}
105
106fn translate_charset(ch: char, designator: u8) -> char {
108 match designator {
109 b'0' => dec_graphics_char(ch),
110 _ => ch,
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
116pub struct Color {
117 pub r: u8,
118 pub g: u8,
119 pub b: u8,
120}
121
122impl Color {
123 #[must_use]
124 pub const fn new(r: u8, g: u8, b: u8) -> Self {
125 Self { r, g, b }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Default)]
131pub struct CellStyle {
132 pub fg: Option<Color>,
133 pub bg: Option<Color>,
134 pub bold: bool,
135 pub dim: bool,
136 pub italic: bool,
137 pub underline: bool,
138 pub blink: bool,
139 pub reverse: bool,
140 pub strikethrough: bool,
141 pub hidden: bool,
142 pub overline: bool,
143}
144
145impl CellStyle {
146 fn reset(&mut self) {
147 *self = Self::default();
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct VCell {
154 pub ch: char,
155 pub style: CellStyle,
156}
157
158impl Default for VCell {
159 fn default() -> Self {
160 Self {
161 ch: ' ',
162 style: CellStyle::default(),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169enum ParseState {
170 Ground,
171 Escape,
172 EscapeHash,
173 EscapeCharset(u8),
174 Csi,
175 Osc,
176 OscEscapeSeen,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum TerminalQuirk {
183 TmuxNestedCursorSaveRestore,
185 ScreenImmediateWrap,
187 WindowsNoAltScreen,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub struct QuirkSet {
194 tmux_nested_cursor: bool,
195 screen_immediate_wrap: bool,
196 windows_no_alt_screen: bool,
197}
198
199impl Default for QuirkSet {
200 fn default() -> Self {
201 Self::empty()
202 }
203}
204
205impl QuirkSet {
206 #[must_use]
208 pub const fn empty() -> Self {
209 Self {
210 tmux_nested_cursor: false,
211 screen_immediate_wrap: false,
212 windows_no_alt_screen: false,
213 }
214 }
215
216 #[must_use]
218 pub const fn tmux_nested() -> Self {
219 Self {
220 tmux_nested_cursor: true,
221 ..Self::empty()
222 }
223 }
224
225 #[must_use]
227 pub const fn gnu_screen() -> Self {
228 Self {
229 screen_immediate_wrap: true,
230 ..Self::empty()
231 }
232 }
233
234 #[must_use]
236 pub const fn windows_console() -> Self {
237 Self {
238 windows_no_alt_screen: true,
239 ..Self::empty()
240 }
241 }
242
243 #[must_use]
245 pub const fn with_tmux_nested_cursor(mut self, enabled: bool) -> Self {
246 self.tmux_nested_cursor = enabled;
247 self
248 }
249
250 #[must_use]
252 pub const fn with_screen_immediate_wrap(mut self, enabled: bool) -> Self {
253 self.screen_immediate_wrap = enabled;
254 self
255 }
256
257 #[must_use]
259 pub const fn with_windows_no_alt_screen(mut self, enabled: bool) -> Self {
260 self.windows_no_alt_screen = enabled;
261 self
262 }
263
264 #[must_use]
266 pub const fn has(self, quirk: TerminalQuirk) -> bool {
267 match quirk {
268 TerminalQuirk::TmuxNestedCursorSaveRestore => self.tmux_nested_cursor,
269 TerminalQuirk::ScreenImmediateWrap => self.screen_immediate_wrap,
270 TerminalQuirk::WindowsNoAltScreen => self.windows_no_alt_screen,
271 }
272 }
273}
274
275pub struct VirtualTerminal {
289 width: u16,
290 height: u16,
291 grid: Vec<VCell>,
292 cursor_x: u16,
293 cursor_y: u16,
294 cursor_visible: bool,
295 current_style: CellStyle,
296 scrollback: VecDeque<Vec<VCell>>,
297 max_scrollback: usize,
298 saved_cursor: Option<(u16, u16)>,
300 scroll_top: u16,
302 scroll_bottom: u16,
303 parse_state: ParseState,
305 csi_params: Vec<u16>,
306 csi_intermediate: Vec<u8>,
307 osc_data: Vec<u8>,
308 alternate_screen: bool,
310 alternate_grid: Option<Vec<VCell>>,
311 alternate_cursor: Option<(u16, u16)>,
312 title: String,
314 quirks: QuirkSet,
315 origin_mode: bool,
318 last_char: Option<char>,
320 utf8_buf: [u8; 4],
322 utf8_len: u8,
324 utf8_expected: u8,
326 tab_stops: Vec<bool>,
328 insert_mode: bool,
331 autowrap: bool,
334 charset_slots: [u8; 4],
336 active_charset: u8,
338 single_shift: Option<u8>,
340}
341
342impl VirtualTerminal {
343 #[must_use]
349 pub fn new(width: u16, height: u16) -> Self {
350 Self::with_quirks(width, height, QuirkSet::default())
351 }
352
353 #[must_use]
359 pub fn with_quirks(width: u16, height: u16, quirks: QuirkSet) -> Self {
360 assert!(width > 0 && height > 0, "terminal dimensions must be > 0");
361 let grid = vec![VCell::default(); usize::from(width) * usize::from(height)];
362 Self {
363 width,
364 height,
365 grid,
366 cursor_x: 0,
367 cursor_y: 0,
368 cursor_visible: true,
369 current_style: CellStyle::default(),
370 scrollback: VecDeque::new(),
371 max_scrollback: 1000,
372 saved_cursor: None,
373 scroll_top: 0,
374 scroll_bottom: height.saturating_sub(1),
375 parse_state: ParseState::Ground,
376 csi_params: Vec::new(),
377 csi_intermediate: Vec::new(),
378 osc_data: Vec::new(),
379 alternate_screen: false,
380 alternate_grid: None,
381 alternate_cursor: None,
382 title: String::new(),
383 quirks,
384 origin_mode: false,
385 last_char: None,
386 utf8_buf: [0; 4],
387 utf8_len: 0,
388 utf8_expected: 0,
389 tab_stops: Self::default_tab_stops(width),
390 insert_mode: false,
391 autowrap: true,
392 charset_slots: [b'B'; 4],
393 active_charset: 0,
394 single_shift: None,
395 }
396 }
397
398 fn default_tab_stops(width: u16) -> Vec<bool> {
400 (0..width).map(|c| c > 0 && c % 8 == 0).collect()
401 }
402
403 #[must_use]
407 pub const fn width(&self) -> u16 {
408 self.width
409 }
410
411 #[must_use]
413 pub const fn height(&self) -> u16 {
414 self.height
415 }
416
417 #[must_use]
419 pub const fn cursor(&self) -> (u16, u16) {
420 (self.cursor_x, self.cursor_y)
421 }
422
423 #[must_use]
425 pub const fn cursor_visible(&self) -> bool {
426 self.cursor_visible
427 }
428
429 #[must_use]
431 pub const fn is_alternate_screen(&self) -> bool {
432 self.alternate_screen
433 }
434
435 #[must_use]
437 pub fn title(&self) -> &str {
438 &self.title
439 }
440
441 #[must_use]
443 pub const fn quirks(&self) -> QuirkSet {
444 self.quirks
445 }
446
447 pub fn set_quirks(&mut self, quirks: QuirkSet) {
449 self.quirks = quirks;
450 }
451
452 #[must_use]
454 pub fn scrollback_len(&self) -> usize {
455 self.scrollback.len()
456 }
457
458 pub fn set_max_scrollback(&mut self, max: usize) {
460 self.max_scrollback = max;
461 while self.scrollback.len() > self.max_scrollback {
462 self.scrollback.pop_front();
463 }
464 }
465
466 #[must_use]
470 pub fn char_at(&self, x: u16, y: u16) -> Option<char> {
471 self.cell_at(x, y).map(|c| c.ch)
472 }
473
474 #[must_use]
476 pub fn style_at(&self, x: u16, y: u16) -> Option<&CellStyle> {
477 self.cell_at(x, y).map(|c| &c.style)
478 }
479
480 #[must_use]
482 pub fn cell_at(&self, x: u16, y: u16) -> Option<&VCell> {
483 if x < self.width && y < self.height {
484 Some(&self.grid[self.idx(x, y)])
485 } else {
486 None
487 }
488 }
489
490 #[must_use]
492 pub fn row_text(&self, y: u16) -> String {
493 if y >= self.height {
494 return String::new();
495 }
496 let start = self.idx(0, y);
497 let end = start + usize::from(self.width);
498 let s: String = self.grid[start..end]
499 .iter()
500 .filter(|c| c.ch != WIDE_CONTINUATION)
501 .map(|c| c.ch)
502 .collect();
503 s.trim_end().to_string()
504 }
505
506 #[must_use]
508 pub fn screen_text(&self) -> String {
509 (0..self.height)
510 .map(|y| self.row_text(y))
511 .collect::<Vec<_>>()
512 .join("\n")
513 }
514
515 #[must_use]
517 pub fn scrollback_line(&self, idx: usize) -> Option<String> {
518 self.scrollback.get(idx).map(|cells| {
519 let s: String = cells
520 .iter()
521 .filter(|c| c.ch != WIDE_CONTINUATION)
522 .map(|c| c.ch)
523 .collect();
524 s.trim_end().to_string()
525 })
526 }
527
528 pub fn feed(&mut self, data: &[u8]) {
532 for &byte in data {
533 self.process_byte(byte);
534 }
535 }
536
537 pub fn feed_str(&mut self, s: &str) {
539 self.feed(s.as_bytes());
540 }
541
542 pub fn put_str(&mut self, s: &str) {
563 for ch in s.chars() {
564 self.put_char(ch);
565 }
566 }
567
568 pub fn set_cursor_position(&mut self, x: u16, y: u16) {
587 self.cursor_x = x.min(self.width.saturating_sub(1));
588 self.cursor_y = y.min(self.height.saturating_sub(1));
589 }
590
591 pub fn clear(&mut self) {
609 let blank = self.styled_blank();
610 for cell in &mut self.grid {
611 *cell = blank.clone();
612 }
613 }
614
615 pub fn clear_scrollback(&mut self) {
620 self.scrollback.clear();
621 }
622
623 #[must_use]
628 pub fn cpr_response(&self) -> Vec<u8> {
629 format!("\x1b[{};{}R", self.cursor_y + 1, self.cursor_x + 1).into_bytes()
630 }
631
632 #[must_use]
635 pub fn da1_response(&self) -> Vec<u8> {
636 b"\x1b[?62;22c".to_vec()
637 }
638
639 fn idx(&self, x: u16, y: u16) -> usize {
642 usize::from(y) * usize::from(self.width) + usize::from(x)
643 }
644
645 fn process_byte(&mut self, byte: u8) {
646 match self.parse_state {
647 ParseState::Ground => self.ground(byte),
648 ParseState::Escape => self.escape(byte),
649 ParseState::EscapeHash => self.escape_hash(byte),
650 ParseState::EscapeCharset(slot) => self.escape_charset(slot, byte),
651 ParseState::Csi => self.csi(byte),
652 ParseState::Osc => self.osc(byte),
653 ParseState::OscEscapeSeen => self.osc_escape_seen(byte),
654 }
655 }
656
657 fn ground(&mut self, byte: u8) {
658 match byte {
659 0x1b => {
660 self.parse_state = ParseState::Escape;
661 }
662 b'\n' | b'\x0b' | b'\x0c' => {
663 self.linefeed();
665 }
666 b'\r' => {
667 self.cursor_x = 0;
668 }
669 b'\x08' => {
670 self.cursor_x = self.cursor_x.saturating_sub(1);
672 }
673 b'\t' => {
674 if self.cursor_x >= self.width {
676 self.cursor_x = self.width.saturating_sub(1);
677 } else {
678 let max_col = self.width.saturating_sub(1);
679 let mut col = self.cursor_x + 1;
680 while col < self.width {
681 if self.tab_stops[usize::from(col)] {
682 break;
683 }
684 col += 1;
685 }
686 self.cursor_x = col.min(max_col);
687 }
688 }
689 b'\x07' => {
690 }
692 b'\x0e' => {
693 self.active_charset = 1;
695 }
696 b'\x0f' => {
697 self.active_charset = 0;
699 }
700 0x20..=0x7e => {
701 self.put_char(byte as char);
702 }
703 0xc2..=0xdf => {
704 self.utf8_buf[0] = byte;
706 self.utf8_len = 1;
707 self.utf8_expected = 2;
708 }
709 0xe0..=0xef => {
710 self.utf8_buf[0] = byte;
712 self.utf8_len = 1;
713 self.utf8_expected = 3;
714 }
715 0xf0..=0xf4 => {
716 self.utf8_buf[0] = byte;
718 self.utf8_len = 1;
719 self.utf8_expected = 4;
720 }
721 0x80..=0xbf if self.utf8_len > 0 => {
722 let idx = usize::from(self.utf8_len);
724 self.utf8_buf[idx] = byte;
725 self.utf8_len += 1;
726 if self.utf8_len == self.utf8_expected {
727 let len = usize::from(self.utf8_len);
728 let mut buf = [0u8; 4];
729 buf[..len].copy_from_slice(&self.utf8_buf[..len]);
730 self.utf8_len = 0;
731 self.utf8_expected = 0;
732 if let Ok(decoded) = std::str::from_utf8(&buf[..len]) {
733 for ch in decoded.chars() {
734 self.put_char(ch);
735 }
736 }
737 }
738 }
739 _ => {
740 self.utf8_len = 0;
742 self.utf8_expected = 0;
743 }
744 }
745 }
746
747 fn escape(&mut self, byte: u8) {
748 match byte {
749 b'[' => {
750 self.parse_state = ParseState::Csi;
751 self.csi_params.clear();
752 self.csi_intermediate.clear();
753 }
754 b']' => {
755 self.parse_state = ParseState::Osc;
756 self.osc_data.clear();
757 }
758 b'7' => {
759 if !(self.quirks.tmux_nested_cursor && self.alternate_screen) {
761 self.saved_cursor = Some((self.cursor_x, self.cursor_y));
762 }
763 self.parse_state = ParseState::Ground;
764 }
765 b'8' => {
766 if !(self.quirks.tmux_nested_cursor && self.alternate_screen)
768 && let Some((x, y)) = self.saved_cursor
769 {
770 self.cursor_x = x.min(self.width.saturating_sub(1));
771 self.cursor_y = y.min(self.height.saturating_sub(1));
772 }
773 self.parse_state = ParseState::Ground;
774 }
775 b'H' => {
776 let col = usize::from(self.cursor_x);
778 if col < self.tab_stops.len() {
779 self.tab_stops[col] = true;
780 }
781 self.parse_state = ParseState::Ground;
782 }
783 b'D' => {
784 self.linefeed();
786 self.parse_state = ParseState::Ground;
787 }
788 b'E' => {
789 self.cursor_x = 0;
791 self.linefeed();
792 self.parse_state = ParseState::Ground;
793 }
794 b'M' => {
795 if self.cursor_y == self.scroll_top {
797 self.scroll_down();
798 } else {
799 self.cursor_y = self.cursor_y.saturating_sub(1);
800 }
801 self.parse_state = ParseState::Ground;
802 }
803 b'#' => {
804 self.parse_state = ParseState::EscapeHash;
806 }
807 b'(' => self.parse_state = ParseState::EscapeCharset(0), b')' => self.parse_state = ParseState::EscapeCharset(1), b'*' => self.parse_state = ParseState::EscapeCharset(2), b'+' => self.parse_state = ParseState::EscapeCharset(3), b'N' => {
812 self.single_shift = Some(2);
814 self.parse_state = ParseState::Ground;
815 }
816 b'O' => {
817 self.single_shift = Some(3);
819 self.parse_state = ParseState::Ground;
820 }
821 b'n' => {
822 self.active_charset = 2;
824 self.parse_state = ParseState::Ground;
825 }
826 b'o' => {
827 self.active_charset = 3;
829 self.parse_state = ParseState::Ground;
830 }
831 b'c' => {
832 self.reset();
834 self.parse_state = ParseState::Ground;
835 }
836 _ => {
837 self.parse_state = ParseState::Ground;
839 }
840 }
841 }
842
843 fn escape_hash(&mut self, byte: u8) {
844 if byte == b'8' {
845 for cell in self.grid.iter_mut() {
847 *cell = VCell {
848 ch: 'E',
849 style: CellStyle::default(),
850 };
851 }
852 self.scroll_top = 0;
853 self.scroll_bottom = self.height.saturating_sub(1);
854 self.cursor_x = 0;
855 self.cursor_y = 0;
856 }
857 self.parse_state = ParseState::Ground;
858 }
859
860 fn escape_charset(&mut self, slot: u8, byte: u8) {
861 let idx = (slot as usize).min(3);
863 self.charset_slots[idx] = byte;
864 self.parse_state = ParseState::Ground;
865 }
866
867 fn csi(&mut self, byte: u8) {
868 match byte {
869 b'0'..=b'9' => {
870 let digit = u16::from(byte - b'0');
871 if let Some(last) = self.csi_params.last_mut() {
872 *last = last.saturating_mul(10).saturating_add(digit);
873 } else if self.csi_params.len() < 32 {
874 self.csi_params.push(digit);
875 }
876 }
877 b';' => {
878 if self.csi_params.len() < 32 {
879 if self.csi_params.is_empty() {
880 self.csi_params.push(0);
881 }
882 self.csi_params.push(0);
883 }
884 }
885 b'?' | b'>' | b'!' | b' ' => {
886 if self.csi_intermediate.len() < 16 {
887 self.csi_intermediate.push(byte);
888 }
889 }
890 0x40..=0x7e => {
891 self.dispatch_csi(byte);
893 self.parse_state = ParseState::Ground;
894 }
895 _ => {
896 self.parse_state = ParseState::Ground;
898 }
899 }
900 }
901
902 fn osc(&mut self, byte: u8) {
903 match byte {
904 0x07 => {
905 self.dispatch_osc();
907 self.parse_state = ParseState::Ground;
908 }
909 0x1b => {
910 self.parse_state = ParseState::OscEscapeSeen;
913 }
914 _ => {
915 if self.osc_data.len() < 4096 {
917 self.osc_data.push(byte);
918 }
919 }
920 }
921 }
922
923 fn osc_escape_seen(&mut self, byte: u8) {
925 match byte {
926 b'\\' => {
927 self.dispatch_osc();
929 self.parse_state = ParseState::Ground;
930 }
931 _ => {
932 self.dispatch_osc();
935 self.parse_state = ParseState::Escape;
936 self.escape(byte);
937 }
938 }
939 }
940
941 fn dispatch_csi(&mut self, final_byte: u8) {
942 let params = &self.csi_params;
943 let has_question = self.csi_intermediate.contains(&b'?');
944
945 match final_byte {
946 b'A' => {
947 let n = Self::param(params, 0, 1);
949 self.cursor_y = self.cursor_y.saturating_sub(n);
950 }
951 b'B' => {
952 let n = Self::param(params, 0, 1);
954 self.cursor_y = (self.cursor_y + n).min(self.height.saturating_sub(1));
955 }
956 b'C' => {
957 let n = Self::param(params, 0, 1);
959 self.cursor_x = (self.cursor_x + n).min(self.width.saturating_sub(1));
960 }
961 b'D' => {
962 let n = Self::param(params, 0, 1);
964 self.cursor_x = self.cursor_x.saturating_sub(n);
965 }
966 b'E' => {
967 let n = Self::param(params, 0, 1);
969 self.cursor_y = (self.cursor_y + n).min(self.height.saturating_sub(1));
970 self.cursor_x = 0;
971 }
972 b'F' => {
973 let n = Self::param(params, 0, 1);
975 self.cursor_y = self.cursor_y.saturating_sub(n);
976 self.cursor_x = 0;
977 }
978 b'G' => {
979 let col = Self::param(params, 0, 1).saturating_sub(1);
981 self.cursor_x = col.min(self.width.saturating_sub(1));
982 }
983 b'H' | b'f' => {
984 let row = Self::param(params, 0, 1).saturating_sub(1);
986 let col = Self::param(params, 1, 1).saturating_sub(1);
987 if self.origin_mode {
988 let abs_row = row.saturating_add(self.scroll_top);
989 self.cursor_y = abs_row.min(self.scroll_bottom);
990 } else {
991 self.cursor_y = row.min(self.height.saturating_sub(1));
992 }
993 self.cursor_x = col.min(self.width.saturating_sub(1));
994 }
995 b'J' => {
996 let mode = Self::param(params, 0, 0);
998 self.erase_display(mode);
999 }
1000 b'K' => {
1001 let mode = Self::param(params, 0, 0);
1003 self.erase_line(mode);
1004 }
1005 b'L' => {
1006 let n = Self::param(params, 0, 1);
1008 if self.cursor_y >= self.scroll_top && self.cursor_y <= self.scroll_bottom {
1009 let blank = self.styled_blank();
1010 for _ in 0..n {
1011 for row in (self.cursor_y + 1..=self.scroll_bottom).rev() {
1013 let src_start = self.idx(0, row - 1);
1014 let dst_start = self.idx(0, row);
1015 let w = usize::from(self.width);
1016 if src_start < dst_start {
1017 let (left, right) = self.grid.split_at_mut(dst_start);
1018 right[..w].clone_from_slice(&left[src_start..src_start + w]);
1019 }
1020 }
1021 let row_start = self.idx(0, self.cursor_y);
1023 for i in 0..usize::from(self.width) {
1024 self.grid[row_start + i] = blank.clone();
1025 }
1026 }
1027 }
1028 }
1029 b'M' => {
1030 let n = Self::param(params, 0, 1);
1032 if self.cursor_y >= self.scroll_top && self.cursor_y <= self.scroll_bottom {
1033 let blank = self.styled_blank();
1034 for _ in 0..n {
1035 for row in self.cursor_y..self.scroll_bottom {
1037 let src_start = self.idx(0, row + 1);
1038 let dst_start = self.idx(0, row);
1039 let w = usize::from(self.width);
1040 let (left, right) = self.grid.split_at_mut(src_start);
1041 left[dst_start..dst_start + w].clone_from_slice(&right[..w]);
1042 }
1043 let bottom_start = self.idx(0, self.scroll_bottom);
1045 for i in 0..usize::from(self.width) {
1046 self.grid[bottom_start + i] = blank.clone();
1047 }
1048 }
1049 }
1050 }
1051 b'S' => {
1052 let n = Self::param(params, 0, 1);
1054 for _ in 0..n {
1055 self.scroll_up();
1056 }
1057 }
1058 b'T' => {
1059 let n = Self::param(params, 0, 1);
1061 for _ in 0..n {
1062 self.scroll_down();
1063 }
1064 }
1065 b'd' => {
1066 let row = Self::param(params, 0, 1).saturating_sub(1);
1068 if self.origin_mode {
1069 let abs_row = row.saturating_add(self.scroll_top);
1070 self.cursor_y = abs_row.min(self.scroll_bottom);
1071 } else {
1072 self.cursor_y = row.min(self.height.saturating_sub(1));
1073 }
1074 }
1075 b'm' => {
1076 self.dispatch_sgr();
1078 }
1079 b'n' => {
1080 }
1083 b'r' => {
1084 let top = Self::param(params, 0, 1).saturating_sub(1);
1086 let bottom = Self::param(params, 1, self.height).saturating_sub(1);
1087 if top < bottom && bottom < self.height {
1088 self.scroll_top = top;
1089 self.scroll_bottom = bottom;
1090 }
1091 self.cursor_x = 0;
1092 if self.origin_mode {
1093 self.cursor_y = self.scroll_top;
1094 } else {
1095 self.cursor_y = 0;
1096 }
1097 }
1098 b'@' => {
1099 let n = Self::param(params, 0, 1);
1101 let n = n.min(self.width.saturating_sub(self.cursor_x));
1102 self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, n);
1104 let row_start = self.idx(0, self.cursor_y);
1105 let w = usize::from(self.width);
1106 let cx = usize::from(self.cursor_x);
1107 let count = usize::from(n);
1108 if count < w {
1111 let cutoff = w - count;
1112 if cutoff > 0
1113 && cutoff < w
1114 && self.grid[row_start + cutoff].ch == WIDE_CONTINUATION
1115 {
1116 self.grid[row_start + cutoff - 1] = VCell::default();
1117 }
1118 }
1119 let blank = self.styled_blank();
1121 let row = &mut self.grid[row_start..row_start + w];
1122 row[cx..].rotate_right(count.min(w - cx));
1123 for cell in row.iter_mut().skip(cx).take(count.min(w - cx)) {
1125 *cell = blank.clone();
1126 }
1127 if cx + count < w && row[cx + count].ch == WIDE_CONTINUATION {
1130 row[cx + count] = blank.clone();
1131 }
1132 }
1133 b'P' => {
1134 let n = Self::param(params, 0, 1);
1136 let n = n.min(self.width.saturating_sub(self.cursor_x));
1137 self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, n);
1139 let blank = self.styled_blank();
1140 let row_start = self.idx(0, self.cursor_y);
1141 let w = usize::from(self.width);
1142 let cx = usize::from(self.cursor_x);
1143 let count = usize::from(n);
1144 let row = &mut self.grid[row_start..row_start + w];
1146 row[cx..].rotate_left(count.min(w - cx));
1147 for cell in row.iter_mut().skip(w - count.min(w - cx)) {
1149 *cell = blank.clone();
1150 }
1151 }
1152 b'X' => {
1153 let n = Self::param(params, 0, 1);
1155 let n = n.min(self.width.saturating_sub(self.cursor_x));
1156 self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, n);
1157 let blank = self.styled_blank();
1158 let start = self.idx(self.cursor_x, self.cursor_y);
1159 for i in 0..usize::from(n) {
1160 self.grid[start + i] = blank.clone();
1161 }
1162 }
1163 b'b' => {
1164 let n = Self::param(params, 0, 1);
1166 if let Some(ch) = self.last_char {
1167 for _ in 0..n {
1168 self.put_char(ch);
1169 }
1170 }
1171 }
1172 b'Z' => {
1173 let n = Self::param(params, 0, 1);
1175 for _ in 0..n {
1176 if self.cursor_x == 0 {
1177 break;
1178 }
1179 let mut col = self.cursor_x - 1;
1180 loop {
1181 if self.tab_stops[usize::from(col)] {
1182 break;
1183 }
1184 if col == 0 {
1185 break;
1186 }
1187 col -= 1;
1188 }
1189 self.cursor_x = col;
1190 }
1191 }
1192 b'g' => {
1193 let mode = Self::param(params, 0, 0);
1195 match mode {
1196 0 => {
1197 let col = usize::from(self.cursor_x);
1199 if col < self.tab_stops.len() {
1200 self.tab_stops[col] = false;
1201 }
1202 }
1203 3 | 5 => {
1204 self.tab_stops.fill(false);
1206 }
1207 _ => {}
1208 }
1209 }
1210 b'p' if self.csi_intermediate.contains(&b'!') => {
1211 self.current_style = CellStyle::default();
1213 self.cursor_visible = true;
1214 self.origin_mode = false;
1215 self.scroll_top = 0;
1216 self.scroll_bottom = self.height.saturating_sub(1);
1217 self.insert_mode = false;
1218 self.autowrap = true;
1219 self.charset_slots = [b'B'; 4];
1220 self.active_charset = 0;
1221 self.single_shift = None;
1222 }
1223 b'h' if has_question => {
1224 let modes: Vec<u16> = self.csi_params.clone();
1226 for p in modes {
1227 self.set_dec_mode(p, true);
1228 }
1229 }
1230 b'l' if has_question => {
1231 let modes: Vec<u16> = self.csi_params.clone();
1233 for p in modes {
1234 self.set_dec_mode(p, false);
1235 }
1236 }
1237 b'h' if !has_question => {
1238 let modes: Vec<u16> = self.csi_params.clone();
1240 for p in modes {
1241 self.set_ansi_mode(p, true);
1242 }
1243 }
1244 b'l' if !has_question => {
1245 let modes: Vec<u16> = self.csi_params.clone();
1247 for p in modes {
1248 self.set_ansi_mode(p, false);
1249 }
1250 }
1251 _ => {
1252 }
1254 }
1255 }
1256
1257 fn dispatch_sgr(&mut self) {
1258 if self.csi_params.is_empty() {
1259 self.current_style.reset();
1260 return;
1261 }
1262
1263 let params = self.csi_params.clone();
1264 let mut i = 0;
1265 while i < params.len() {
1266 match params[i] {
1267 0 => self.current_style.reset(),
1268 1 => self.current_style.bold = true,
1269 2 => self.current_style.dim = true,
1270 3 => self.current_style.italic = true,
1271 4 => self.current_style.underline = true,
1272 5 => self.current_style.blink = true,
1273 7 => self.current_style.reverse = true,
1274 8 => self.current_style.hidden = true,
1275 9 => self.current_style.strikethrough = true,
1276 22 => {
1277 self.current_style.bold = false;
1278 self.current_style.dim = false;
1279 }
1280 23 => self.current_style.italic = false,
1281 24 => self.current_style.underline = false,
1282 25 => self.current_style.blink = false,
1283 27 => self.current_style.reverse = false,
1284 28 => self.current_style.hidden = false,
1285 29 => self.current_style.strikethrough = false,
1286 53 => self.current_style.overline = true,
1287 55 => self.current_style.overline = false,
1288 30..=37 => {
1290 self.current_style.fg = Some(ansi_color(params[i] - 30));
1291 }
1292 38 => {
1293 if let Some(color) = parse_extended_color(¶ms, &mut i) {
1295 self.current_style.fg = Some(color);
1296 }
1297 }
1298 39 => self.current_style.fg = None,
1299 40..=47 => {
1301 self.current_style.bg = Some(ansi_color(params[i] - 40));
1302 }
1303 48 => {
1304 if let Some(color) = parse_extended_color(¶ms, &mut i) {
1306 self.current_style.bg = Some(color);
1307 }
1308 }
1309 49 => self.current_style.bg = None,
1310 90..=97 => {
1312 self.current_style.fg = Some(ansi_bright_color(params[i] - 90));
1313 }
1314 100..=107 => {
1316 self.current_style.bg = Some(ansi_bright_color(params[i] - 100));
1317 }
1318 _ => {} }
1320 i += 1;
1321 }
1322 }
1323
1324 fn dispatch_osc(&mut self) {
1325 let data = String::from_utf8_lossy(&self.osc_data).to_string();
1326 if let Some(rest) = data.strip_prefix("0;").or_else(|| data.strip_prefix("2;")) {
1327 self.title = rest.to_string();
1328 }
1329 }
1331
1332 fn set_dec_mode(&mut self, mode: u16, enable: bool) {
1333 match mode {
1334 6 => {
1335 self.origin_mode = enable;
1337 if enable {
1340 self.cursor_x = 0;
1341 self.cursor_y = self.scroll_top;
1342 } else {
1343 self.cursor_x = 0;
1344 self.cursor_y = 0;
1345 }
1346 }
1347 7 => self.autowrap = enable,
1348 25 => self.cursor_visible = enable,
1349 1049 => {
1350 if self.quirks.windows_no_alt_screen {
1352 return;
1353 }
1354 if enable && !self.alternate_screen {
1355 self.alternate_grid = Some(std::mem::replace(
1356 &mut self.grid,
1357 vec![VCell::default(); usize::from(self.width) * usize::from(self.height)],
1358 ));
1359 self.alternate_cursor = Some((self.cursor_x, self.cursor_y));
1360 self.cursor_x = 0;
1361 self.cursor_y = 0;
1362 self.alternate_screen = true;
1363 } else if !enable && self.alternate_screen {
1364 if let Some(main_grid) = self.alternate_grid.take() {
1365 self.grid = main_grid;
1366 }
1367 if let Some((x, y)) = self.alternate_cursor.take() {
1368 self.cursor_x = x;
1369 self.cursor_y = y;
1370 }
1371 self.alternate_screen = false;
1372 }
1373 }
1374 1047 => {
1375 if self.quirks.windows_no_alt_screen {
1377 return;
1378 }
1379 if enable && !self.alternate_screen {
1380 self.alternate_grid = Some(std::mem::replace(
1381 &mut self.grid,
1382 vec![VCell::default(); usize::from(self.width) * usize::from(self.height)],
1383 ));
1384 self.alternate_screen = true;
1385 } else if !enable && self.alternate_screen {
1386 if let Some(main_grid) = self.alternate_grid.take() {
1387 self.grid = main_grid;
1388 }
1389 self.alternate_screen = false;
1390 }
1391 }
1392 _ => {
1393 }
1395 }
1396 }
1397
1398 fn set_ansi_mode(&mut self, mode: u16, enable: bool) {
1399 if mode == 4 {
1400 self.insert_mode = enable;
1401 }
1402 }
1403
1404 pub fn put_char(&mut self, ch: char) {
1428 let designator = if let Some(shift) = self.single_shift {
1430 let slot = (shift as usize).min(3);
1431 self.single_shift = None;
1432 self.charset_slots[slot]
1433 } else {
1434 self.charset_slots[(self.active_charset as usize) & 3]
1435 };
1436 let ch = translate_charset(ch, designator);
1437
1438 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
1439 if char_width == 0 {
1440 return; }
1442
1443 if self.cursor_x >= self.width {
1445 if self.autowrap {
1446 self.cursor_x = 0;
1447 self.linefeed();
1448 } else {
1449 self.cursor_x = self.width.saturating_sub(1);
1451 }
1452 }
1453
1454 if char_width == 2 && self.cursor_x + 1 >= self.width {
1456 if self.autowrap {
1457 let idx = self.idx(self.cursor_x, self.cursor_y);
1458 self.grid[idx] = VCell::default();
1459 self.cursor_x = 0;
1460 self.linefeed();
1461 } else {
1462 self.cursor_x = self.width.saturating_sub(1);
1464 }
1465 }
1466
1467 let last_col = self.width.saturating_sub(1);
1468 let immediate_wrap = self.quirks.screen_immediate_wrap && self.cursor_x == last_col;
1469 let idx = self.idx(self.cursor_x, self.cursor_y);
1470
1471 if self.insert_mode {
1473 let row_start = self.idx(0, self.cursor_y);
1474 let w = usize::from(self.width);
1475 let cx = usize::from(self.cursor_x);
1476 let shift = usize::from(u16::try_from(char_width).unwrap_or(1));
1477 let row = &mut self.grid[row_start..row_start + w];
1478 if cx + shift <= w {
1479 row[cx..].rotate_right(shift.min(w - cx));
1480 }
1481 }
1482
1483 if self.grid[idx].ch == WIDE_CONTINUATION && self.cursor_x > 0 {
1485 let lead_idx = self.idx(self.cursor_x - 1, self.cursor_y);
1486 self.grid[lead_idx] = VCell::default();
1487 }
1488 if char_width == 1 && self.cursor_x + 1 < self.width {
1490 let next_idx = idx + 1;
1491 if self.grid[next_idx].ch == WIDE_CONTINUATION {
1492 self.grid[next_idx] = VCell::default();
1493 }
1494 }
1495
1496 self.grid[idx] = VCell {
1497 ch,
1498 style: self.current_style.clone(),
1499 };
1500
1501 if char_width == 2 && self.cursor_x + 1 < self.width {
1503 let cont_idx = idx + 1;
1504 self.grid[cont_idx] = VCell {
1505 ch: WIDE_CONTINUATION,
1506 style: self.current_style.clone(),
1507 };
1508 }
1509
1510 self.last_char = Some(ch);
1511 let advance = u16::try_from(char_width).unwrap_or(1);
1512 if immediate_wrap {
1513 self.cursor_x = 0;
1514 self.linefeed();
1515 } else if self.autowrap {
1516 self.cursor_x = self.cursor_x.saturating_add(advance).min(self.width);
1517 } else {
1518 self.cursor_x = self
1520 .cursor_x
1521 .saturating_add(advance)
1522 .min(self.width.saturating_sub(1));
1523 }
1524 }
1525
1526 fn linefeed(&mut self) {
1527 if self.cursor_y == self.scroll_bottom {
1528 self.scroll_up();
1529 } else if self.cursor_y < self.height.saturating_sub(1) {
1530 self.cursor_y += 1;
1531 }
1532 }
1533
1534 fn scroll_up(&mut self) {
1535 let top_start = self.idx(0, self.scroll_top);
1538 let top_end = top_start + usize::from(self.width);
1539 let line: Vec<VCell> = self.grid[top_start..top_end].to_vec();
1540 self.scrollback.push_back(line);
1541 while self.scrollback.len() > self.max_scrollback {
1542 self.scrollback.pop_front();
1543 }
1544
1545 for row in self.scroll_top..self.scroll_bottom {
1547 let src_start = self.idx(0, row + 1);
1548 let dst_start = self.idx(0, row);
1549 let w = usize::from(self.width);
1550 let (left, right) = self.grid.split_at_mut(src_start);
1552 left[dst_start..dst_start + w].clone_from_slice(&right[..w]);
1553 }
1554
1555 let blank = self.styled_blank();
1557 let bottom_start = self.idx(0, self.scroll_bottom);
1558 for i in 0..usize::from(self.width) {
1559 self.grid[bottom_start + i] = blank.clone();
1560 }
1561 }
1562
1563 fn scroll_down(&mut self) {
1564 for row in (self.scroll_top + 1..=self.scroll_bottom).rev() {
1566 let src_start = self.idx(0, row - 1);
1567 let dst_start = self.idx(0, row);
1568 let w = usize::from(self.width);
1569 if src_start < dst_start {
1570 let (left, right) = self.grid.split_at_mut(dst_start);
1571 right[..w].clone_from_slice(&left[src_start..src_start + w]);
1572 }
1573 }
1574
1575 let blank = self.styled_blank();
1577 let top_start = self.idx(0, self.scroll_top);
1578 for i in 0..usize::from(self.width) {
1579 self.grid[top_start + i] = blank.clone();
1580 }
1581 }
1582
1583 fn styled_blank(&self) -> VCell {
1586 VCell {
1587 ch: ' ',
1588 style: self.current_style.clone(),
1589 }
1590 }
1591
1592 fn fixup_wide_erase_row(&mut self, row_y: u16, start_col: u16, count: u16) {
1596 let w = self.width;
1597 let sc = start_col;
1598 let n = count;
1599 if n == 0 || sc >= w {
1600 return;
1601 }
1602 let row_start = self.idx(0, row_y);
1603 if sc > 0 && self.grid[row_start + usize::from(sc)].ch == WIDE_CONTINUATION {
1605 self.grid[row_start + usize::from(sc - 1)] = VCell::default();
1606 }
1607 let end_col = sc.saturating_add(n);
1609 if end_col < w && self.grid[row_start + usize::from(end_col)].ch == WIDE_CONTINUATION {
1610 self.grid[row_start + usize::from(end_col)] = VCell::default();
1611 }
1612 }
1613
1614 fn erase_display(&mut self, mode: u16) {
1615 let blank = self.styled_blank();
1616 match mode {
1617 0 => {
1618 let count = self.width.saturating_sub(self.cursor_x);
1620 self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, count);
1621 let start = self.idx(self.cursor_x, self.cursor_y);
1622 for cell in &mut self.grid[start..] {
1623 *cell = blank.clone();
1624 }
1625 }
1626 1 => {
1627 let count = self.cursor_x + 1;
1629 self.fixup_wide_erase_row(self.cursor_y, 0, count);
1630 let end = self.idx(self.cursor_x, self.cursor_y) + 1;
1631 for cell in &mut self.grid[..end] {
1632 *cell = blank.clone();
1633 }
1634 }
1635 2 | 3 => {
1636 for cell in &mut self.grid {
1638 *cell = blank.clone();
1639 }
1640 if mode == 3 {
1641 self.scrollback.clear();
1642 }
1643 }
1644 _ => {}
1645 }
1646 }
1647
1648 fn erase_line(&mut self, mode: u16) {
1649 let y = self.cursor_y;
1650 let blank = self.styled_blank();
1651 let row_start = self.idx(0, y);
1652 match mode {
1653 0 => {
1654 let count = self.width.saturating_sub(self.cursor_x);
1656 self.fixup_wide_erase_row(y, self.cursor_x, count);
1657 let start = row_start + usize::from(self.cursor_x);
1658 let end = row_start + usize::from(self.width);
1659 for cell in &mut self.grid[start..end] {
1660 *cell = blank.clone();
1661 }
1662 }
1663 1 => {
1664 let count = self.cursor_x + 1;
1666 self.fixup_wide_erase_row(y, 0, count);
1667 let end = row_start + usize::from(count);
1668 for cell in &mut self.grid[row_start..end] {
1669 *cell = blank.clone();
1670 }
1671 }
1672 2 => {
1673 let end = row_start + usize::from(self.width);
1675 for cell in &mut self.grid[row_start..end] {
1676 *cell = blank.clone();
1677 }
1678 }
1679 _ => {}
1680 }
1681 }
1682
1683 fn reset(&mut self) {
1684 self.grid = vec![VCell::default(); usize::from(self.width) * usize::from(self.height)];
1685 self.cursor_x = 0;
1686 self.cursor_y = 0;
1687 self.cursor_visible = true;
1688 self.current_style = CellStyle::default();
1689 self.scrollback.clear();
1690 self.saved_cursor = None;
1691 self.scroll_top = 0;
1692 self.scroll_bottom = self.height.saturating_sub(1);
1693 self.title.clear();
1694 self.alternate_screen = false;
1695 self.alternate_grid = None;
1696 self.alternate_cursor = None;
1697 self.last_char = None;
1698 self.utf8_len = 0;
1699 self.utf8_expected = 0;
1700 self.tab_stops = Self::default_tab_stops(self.width);
1701 self.insert_mode = false;
1702 self.autowrap = true;
1703 self.charset_slots = [b'B'; 4];
1704 self.active_charset = 0;
1705 self.single_shift = None;
1706 }
1707
1708 fn param(params: &[u16], idx: usize, default: u16) -> u16 {
1709 params
1710 .get(idx)
1711 .copied()
1712 .filter(|&v| v > 0)
1713 .unwrap_or(default)
1714 }
1715}
1716
1717fn ansi_color(idx: u16) -> Color {
1720 match idx {
1721 0 => Color::new(0, 0, 0), 1 => Color::new(170, 0, 0), 2 => Color::new(0, 170, 0), 3 => Color::new(170, 170, 0), 4 => Color::new(0, 0, 170), 5 => Color::new(170, 0, 170), 6 => Color::new(0, 170, 170), 7 => Color::new(170, 170, 170), _ => Color::default(),
1730 }
1731}
1732
1733fn ansi_bright_color(idx: u16) -> Color {
1734 match idx {
1735 0 => Color::new(85, 85, 85), 1 => Color::new(255, 85, 85), 2 => Color::new(85, 255, 85), 3 => Color::new(255, 255, 85), 4 => Color::new(85, 85, 255), 5 => Color::new(255, 85, 255), 6 => Color::new(85, 255, 255), 7 => Color::new(255, 255, 255), _ => Color::default(),
1744 }
1745}
1746
1747fn parse_extended_color(params: &[u16], i: &mut usize) -> Option<Color> {
1749 if *i + 1 >= params.len() {
1750 return None;
1751 }
1752 match params[*i + 1] {
1753 2 => {
1754 if *i + 4 < params.len() {
1756 let r = params[*i + 2] as u8;
1757 let g = params[*i + 3] as u8;
1758 let b = params[*i + 4] as u8;
1759 *i += 4;
1760 Some(Color::new(r, g, b))
1761 } else {
1762 None
1763 }
1764 }
1765 5 => {
1766 if *i + 2 < params.len() {
1768 let idx = params[*i + 2];
1769 *i += 2;
1770 Some(color_256(idx))
1771 } else {
1772 None
1773 }
1774 }
1775 _ => None,
1776 }
1777}
1778
1779fn color_256(idx: u16) -> Color {
1781 match idx {
1782 0..=7 => ansi_color(idx),
1783 8..=15 => ansi_bright_color(idx - 8),
1784 16..=231 => {
1785 let n = idx - 16;
1787 let b = (n % 6) as u8;
1788 let g = ((n / 6) % 6) as u8;
1789 let r = (n / 36) as u8;
1790 let to_rgb = |v: u8| if v == 0 { 0u8 } else { 55 + 40 * v };
1791 Color::new(to_rgb(r), to_rgb(g), to_rgb(b))
1792 }
1793 232..=255 => {
1794 let v = (8 + 10 * (idx - 232)) as u8;
1796 Color::new(v, v, v)
1797 }
1798 _ => Color::default(),
1799 }
1800}
1801
1802#[cfg(test)]
1803mod tests {
1804 use super::*;
1805
1806 fn assert_invariants(vt: &VirtualTerminal) {
1807 assert!(vt.cursor_x <= vt.width);
1809 assert!(vt.cursor_y < vt.height);
1810 assert_eq!(vt.grid.len(), vt.width as usize * vt.height as usize);
1811 assert!(vt.scroll_top <= vt.scroll_bottom);
1812 assert!(vt.scroll_bottom < vt.height);
1813 for line in &vt.scrollback {
1814 assert_eq!(line.len(), vt.width as usize);
1815 }
1816 }
1817
1818 #[test]
1819 fn new_terminal_dimensions() {
1820 let vt = VirtualTerminal::new(80, 24);
1821 assert_eq!(vt.width(), 80);
1822 assert_eq!(vt.height(), 24);
1823 assert_eq!(vt.cursor(), (0, 0));
1824 assert!(vt.cursor_visible());
1825 }
1826
1827 #[test]
1828 #[should_panic(expected = "dimensions must be > 0")]
1829 fn zero_width_panics() {
1830 let _ = VirtualTerminal::new(0, 24);
1831 }
1832
1833 #[test]
1834 #[should_panic(expected = "dimensions must be > 0")]
1835 fn zero_height_panics() {
1836 let _ = VirtualTerminal::new(80, 0);
1837 }
1838
1839 #[test]
1840 fn invariants_hold_for_varied_inputs() {
1841 let inputs: [&[u8]; 6] = [
1842 b"",
1843 b"Hello",
1844 b"ABCDE\r\nFGHIJ",
1845 b"\x1b[2J",
1846 b"\x1b[1;1H\x1b[2;2H",
1847 b"\x1b[?1049hAlt\x1b[?1049l",
1848 ];
1849
1850 for width in 1..=6 {
1851 for height in 1..=4 {
1852 for input in inputs {
1853 let mut vt = VirtualTerminal::new(width, height);
1854 for chunk in input.chunks(3) {
1855 vt.feed(chunk);
1856 assert_invariants(&vt);
1857 }
1858 }
1859 }
1860 }
1861 }
1862
1863 #[test]
1864 fn chunked_feed_matches_whole_stream_for_split_utf8_and_control_sequences() {
1865 let input = b"Main\x1b]2;Title\x07\x1b[2;3H\x1b[1;32m\xe4\xb8\xad\x1b[0m\x1b[?25l";
1866 let chunks: [&[u8]; 9] = [
1867 &input[0..5],
1868 &input[5..9],
1869 &input[9..17],
1870 &input[17..23],
1871 &input[23..28],
1872 &input[28..29],
1873 &input[29..31],
1874 &input[31..34],
1875 &input[34..],
1876 ];
1877
1878 let mut whole = VirtualTerminal::new(12, 4);
1879 whole.feed(input);
1880
1881 let mut chunked = VirtualTerminal::new(12, 4);
1882 for chunk in chunks {
1883 chunked.feed(chunk);
1884 assert_invariants(&chunked);
1885 }
1886
1887 assert_eq!(chunked.screen_text(), whole.screen_text());
1888 assert_eq!(chunked.cursor(), whole.cursor());
1889 assert_eq!(chunked.title(), whole.title());
1890 assert_eq!(chunked.cursor_visible(), whole.cursor_visible());
1891 assert_eq!(chunked.is_alternate_screen(), whole.is_alternate_screen());
1892 assert_eq!(chunked.style_at(2, 1), whole.style_at(2, 1));
1893 }
1894
1895 #[test]
1896 fn plain_text_output() {
1897 let mut vt = VirtualTerminal::new(80, 24);
1898 vt.feed(b"Hello, World!");
1899 assert_eq!(vt.char_at(0, 0), Some('H'));
1900 assert_eq!(vt.char_at(12, 0), Some('!'));
1901 assert_eq!(vt.cursor(), (13, 0));
1902 assert_eq!(vt.row_text(0), "Hello, World!");
1903 }
1904
1905 #[test]
1906 fn newline_advances_cursor() {
1907 let mut vt = VirtualTerminal::new(80, 24);
1908 vt.feed(b"Line 1\r\nLine 2");
1909 assert_eq!(vt.row_text(0), "Line 1");
1910 assert_eq!(vt.row_text(1), "Line 2");
1911 assert_eq!(vt.cursor(), (6, 1));
1912 }
1913
1914 #[test]
1915 fn carriage_return() {
1916 let mut vt = VirtualTerminal::new(80, 24);
1917 vt.feed(b"AAAA\rBB");
1918 assert_eq!(vt.row_text(0), "BBAA");
1919 }
1920
1921 #[test]
1922 fn auto_wrap() {
1923 let mut vt = VirtualTerminal::new(5, 3);
1924 vt.feed(b"ABCDEFGH");
1925 assert_eq!(vt.row_text(0), "ABCDE");
1926 assert_eq!(vt.row_text(1), "FGH");
1927 assert_eq!(vt.cursor(), (3, 1));
1928 }
1929
1930 #[test]
1931 fn screen_immediate_wrap_quirk_wraps_on_last_column() {
1932 let mut vt = VirtualTerminal::with_quirks(5, 3, QuirkSet::gnu_screen());
1933 vt.feed(b"ABCDE");
1934 assert_eq!(vt.row_text(0), "ABCDE");
1935 assert_eq!(vt.cursor(), (0, 1));
1936
1937 vt.feed(b"F");
1938 assert_eq!(vt.row_text(1), "F");
1939 assert_eq!(vt.cursor(), (1, 1));
1940 }
1941
1942 #[test]
1943 fn scroll_on_overflow() {
1944 let mut vt = VirtualTerminal::new(10, 3);
1945 vt.feed(b"AAA\r\nBBB\r\nCCC\r\nDDD");
1946 assert_eq!(vt.row_text(0), "BBB");
1948 assert_eq!(vt.row_text(1), "CCC");
1949 assert_eq!(vt.row_text(2), "DDD");
1950 assert_eq!(vt.scrollback_len(), 1);
1951 assert_eq!(vt.scrollback_line(0), Some("AAA".to_string()));
1952 }
1953
1954 #[test]
1955 fn cursor_movement_csi() {
1956 let mut vt = VirtualTerminal::new(80, 24);
1957 vt.feed(b"\x1b[4;6H");
1959 assert_eq!(vt.cursor(), (5, 3));
1960 }
1961
1962 #[test]
1963 fn cursor_up_down_forward_back() {
1964 let mut vt = VirtualTerminal::new(80, 24);
1965 vt.feed(b"\x1b[10;10H"); vt.feed(b"\x1b[3A"); assert_eq!(vt.cursor(), (9, 6));
1968 vt.feed(b"\x1b[2B"); assert_eq!(vt.cursor(), (9, 8));
1970 vt.feed(b"\x1b[5C"); assert_eq!(vt.cursor(), (14, 8));
1972 vt.feed(b"\x1b[3D"); assert_eq!(vt.cursor(), (11, 8));
1974 }
1975
1976 #[test]
1977 fn cursor_clamps_to_bounds() {
1978 let mut vt = VirtualTerminal::new(10, 5);
1979 vt.feed(b"\x1b[100;100H");
1980 assert_eq!(vt.cursor(), (9, 4));
1981 vt.feed(b"\x1b[99A");
1982 assert_eq!(vt.cursor(), (9, 0));
1983 }
1984
1985 #[test]
1986 fn erase_to_end_of_line() {
1987 let mut vt = VirtualTerminal::new(80, 24);
1988 vt.feed(b"ABCDE");
1989 vt.feed(b"\x1b[1;6H"); vt.feed(b"\x1b[K"); assert_eq!(vt.row_text(0), "ABCDE");
1992 }
1993
1994 #[test]
1995 fn erase_entire_line() {
1996 let mut vt = VirtualTerminal::new(80, 24);
1997 vt.feed(b"ABCDE");
1998 vt.feed(b"\x1b[2K"); assert_eq!(vt.row_text(0), "");
2000 }
2001
2002 #[test]
2003 fn erase_display_from_cursor() {
2004 let mut vt = VirtualTerminal::new(10, 3);
2005 vt.feed(b"AAAAAAAAAA");
2006 vt.feed(b"BBBBBBBBBB");
2007 vt.feed(b"CCCCCCCCCC");
2008 vt.feed(b"\x1b[2;5H"); vt.feed(b"\x1b[J"); assert_eq!(vt.row_text(0), "AAAAAAAAAA");
2011 assert_eq!(vt.row_text(1), "BBBB");
2012 assert_eq!(vt.row_text(2), "");
2013 }
2014
2015 #[test]
2016 fn sgr_bold_and_color() {
2017 let mut vt = VirtualTerminal::new(80, 24);
2018 vt.feed(b"\x1b[1;31mHello\x1b[0m World");
2019 let style = vt.style_at(0, 0).unwrap();
2021 assert!(style.bold);
2022 assert_eq!(style.fg, Some(Color::new(170, 0, 0)));
2023 let style2 = vt.style_at(6, 0).unwrap();
2025 assert!(!style2.bold);
2026 assert_eq!(style2.fg, None);
2027 }
2028
2029 #[test]
2030 fn sgr_truecolor() {
2031 let mut vt = VirtualTerminal::new(80, 24);
2032 vt.feed(b"\x1b[38;2;100;200;50mX");
2033 let style = vt.style_at(0, 0).unwrap();
2034 assert_eq!(style.fg, Some(Color::new(100, 200, 50)));
2035 }
2036
2037 #[test]
2038 fn sgr_256_color() {
2039 let mut vt = VirtualTerminal::new(80, 24);
2040 vt.feed(b"\x1b[48;5;196mX"); let style = vt.style_at(0, 0).unwrap();
2042 assert!(style.bg.is_some());
2043 }
2044
2045 #[test]
2046 fn dec_save_restore_cursor() {
2047 let mut vt = VirtualTerminal::new(80, 24);
2048 vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b7"); vt.feed(b"\x1b[1;1H"); assert_eq!(vt.cursor(), (0, 0));
2052 vt.feed(b"\x1b8"); assert_eq!(vt.cursor(), (9, 4));
2054 }
2055
2056 #[test]
2057 fn tmux_nested_cursor_quirk_ignores_save_restore_in_alt_screen() {
2058 let mut vt = VirtualTerminal::with_quirks(80, 24, QuirkSet::tmux_nested());
2059 vt.feed(b"\x1b[?1049h"); vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b7"); vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b8"); assert_eq!(vt.cursor(), (0, 0));
2065 }
2066
2067 #[test]
2068 fn combined_quirks_apply_independently() {
2069 let quirks = QuirkSet::empty()
2070 .with_screen_immediate_wrap(true)
2071 .with_tmux_nested_cursor(true);
2072 let mut vt = VirtualTerminal::with_quirks(5, 3, quirks);
2073
2074 vt.feed(b"\x1b[?1049h");
2075 vt.feed(b"ABCDE");
2076 assert_eq!(vt.cursor(), (0, 1));
2077
2078 vt.feed(b"\x1b[2;2H\x1b7\x1b[1;1H\x1b8");
2079 assert_eq!(vt.cursor(), (0, 0));
2080 }
2081
2082 #[test]
2083 fn cursor_visibility() {
2084 let mut vt = VirtualTerminal::new(80, 24);
2085 assert!(vt.cursor_visible());
2086 vt.feed(b"\x1b[?25l"); assert!(!vt.cursor_visible());
2088 vt.feed(b"\x1b[?25h"); assert!(vt.cursor_visible());
2090 }
2091
2092 #[test]
2093 fn alternate_screen() {
2094 let mut vt = VirtualTerminal::new(10, 3);
2095 vt.feed(b"Main");
2096 assert_eq!(vt.row_text(0), "Main");
2097 assert!(!vt.is_alternate_screen());
2098
2099 vt.feed(b"\x1b[?1049h"); assert!(vt.is_alternate_screen());
2101 assert_eq!(vt.row_text(0), ""); vt.feed(b"Alt");
2103 assert_eq!(vt.row_text(0), "Alt");
2104
2105 vt.feed(b"\x1b[?1049l"); assert!(!vt.is_alternate_screen());
2107 assert_eq!(vt.row_text(0), "Main"); }
2109
2110 #[test]
2111 fn windows_no_alt_screen_quirk_ignores_alternate_buffer() {
2112 let mut vt = VirtualTerminal::with_quirks(10, 3, QuirkSet::windows_console());
2113 vt.feed(b"Main");
2114 vt.feed(b"\x1b[?1049h"); vt.feed(b"Alt");
2116 vt.feed(b"\x1b[?1049l"); assert!(!vt.is_alternate_screen());
2118 assert_eq!(vt.row_text(0), "MainAlt");
2119 }
2120
2121 #[test]
2122 fn osc_title() {
2123 let mut vt = VirtualTerminal::new(80, 24);
2124 vt.feed(b"\x1b]0;My Title\x07");
2125 assert_eq!(vt.title(), "My Title");
2126 }
2127
2128 #[test]
2129 fn full_reset() {
2130 let mut vt = VirtualTerminal::new(80, 24);
2131 vt.feed(b"Some text\x1b[1;31m");
2132 vt.feed(b"\x1bc"); assert_eq!(vt.cursor(), (0, 0));
2134 assert_eq!(vt.row_text(0), "");
2135 assert!(vt.cursor_visible());
2136 }
2137
2138 #[test]
2139 fn cpr_response_format() {
2140 let mut vt = VirtualTerminal::new(80, 24);
2141 vt.feed(b"\x1b[5;10H");
2142 let response = vt.cpr_response();
2143 assert_eq!(response, b"\x1b[5;10R");
2144 }
2145
2146 #[test]
2147 fn da1_response() {
2148 let vt = VirtualTerminal::new(80, 24);
2149 let response = vt.da1_response();
2150 assert_eq!(response, b"\x1b[?62;22c");
2151 }
2152
2153 #[test]
2154 fn scroll_region() {
2155 let mut vt = VirtualTerminal::new(10, 5);
2156 vt.feed(b"\x1b[2;4r");
2158 vt.feed(b"\x1b[1;1HROW1");
2160 vt.feed(b"\x1b[2;1HROW2");
2161 vt.feed(b"\x1b[3;1HROW3");
2162 vt.feed(b"\x1b[4;1HROW4");
2163 vt.feed(b"\x1b[5;1HROW5");
2164 assert_eq!(vt.row_text(0), "ROW1");
2165 assert_eq!(vt.row_text(4), "ROW5");
2166 }
2167
2168 #[test]
2169 fn tab_advances_to_stop() {
2170 let mut vt = VirtualTerminal::new(80, 24);
2171 vt.feed(b"AB\tC");
2172 assert_eq!(vt.char_at(8, 0), Some('C'));
2173 }
2174
2175 #[test]
2176 fn backspace() {
2177 let mut vt = VirtualTerminal::new(80, 24);
2178 vt.feed(b"ABC\x08X");
2179 assert_eq!(vt.row_text(0), "ABX");
2180 }
2181
2182 #[test]
2183 fn screen_text() {
2184 let mut vt = VirtualTerminal::new(10, 3);
2185 vt.feed(b"AAA\r\nBBB\r\nCCC");
2186 let text = vt.screen_text();
2187 assert_eq!(text, "AAA\nBBB\nCCC");
2188 }
2189
2190 #[test]
2191 fn scrollback_truncation() {
2192 let mut vt = VirtualTerminal::new(10, 2);
2193 vt.set_max_scrollback(3);
2194 for i in 0..5 {
2196 vt.feed_str(&format!("Line{i}\n"));
2197 }
2198 assert!(vt.scrollback_len() <= 3);
2199 }
2200
2201 #[test]
2202 fn out_of_bounds_cell_returns_none() {
2203 let vt = VirtualTerminal::new(10, 5);
2204 assert_eq!(vt.char_at(10, 0), None);
2205 assert_eq!(vt.char_at(0, 5), None);
2206 assert!(vt.style_at(99, 99).is_none());
2207 }
2208
2209 #[test]
2210 fn reverse_index_at_scroll_top() {
2211 let mut vt = VirtualTerminal::new(10, 5);
2212 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1bM"); assert_eq!(vt.cursor(), (0, 1));
2217 }
2218
2219 #[test]
2220 fn cursor_horizontal_absolute() {
2221 let mut vt = VirtualTerminal::new(10, 3);
2222 vt.feed(b"\x1b[10G");
2223 assert_eq!(vt.cursor(), (9, 0));
2224 }
2225
2226 #[test]
2227 fn vertical_position_absolute() {
2228 let mut vt = VirtualTerminal::new(80, 24);
2229 vt.feed(b"\x1b[5d");
2230 assert_eq!(vt.cursor(), (0, 4));
2231 }
2232
2233 #[test]
2234 fn cursor_next_previous_line() {
2235 let mut vt = VirtualTerminal::new(80, 24);
2236 vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b[2E"); assert_eq!(vt.cursor(), (0, 6));
2239 vt.feed(b"\x1b[1F"); assert_eq!(vt.cursor(), (0, 5));
2241 }
2242
2243 #[test]
2244 fn bright_colors() {
2245 let mut vt = VirtualTerminal::new(80, 24);
2246 vt.feed(b"\x1b[91mX"); let style = vt.style_at(0, 0).unwrap();
2248 assert_eq!(style.fg, Some(Color::new(255, 85, 85)));
2249 }
2250
2251 #[test]
2252 fn nel_next_line() {
2253 let mut vt = VirtualTerminal::new(10, 3);
2254 vt.feed(b"ABCDE\x1bEX");
2255 assert_eq!(vt.row_text(0), "ABCDE");
2257 assert_eq!(vt.row_text(1), "X");
2258 assert_eq!(vt.cursor(), (1, 1));
2259 }
2260
2261 #[test]
2262 fn nel_at_bottom_scrolls() {
2263 let mut vt = VirtualTerminal::new(5, 3);
2264 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC");
2265 vt.feed(b"\x1b[3;3H\x1bE"); assert_eq!(vt.row_text(0), "BBBBB");
2267 assert_eq!(vt.row_text(1), "CCCCC");
2268 assert_eq!(vt.row_text(2), "");
2269 assert_eq!(vt.cursor(), (0, 2));
2270 }
2271
2272 #[test]
2273 fn decaln_fills_with_e() {
2274 let mut vt = VirtualTerminal::new(5, 3);
2275 vt.feed(b"ABC\x1b#8");
2276 assert_eq!(vt.row_text(0), "EEEEE");
2277 assert_eq!(vt.row_text(1), "EEEEE");
2278 assert_eq!(vt.row_text(2), "EEEEE");
2279 assert_eq!(vt.cursor(), (0, 0));
2280 }
2281
2282 #[test]
2283 fn decaln_resets_scroll_region() {
2284 let mut vt = VirtualTerminal::new(5, 3);
2285 vt.feed(b"\x1b[2;3r"); vt.feed(b"\x1b#8"); vt.feed(b"\x1b[3;1HZZZZZ\n"); assert_eq!(vt.row_text(0), "EEEEE");
2290 assert_eq!(vt.row_text(1), "ZZZZZ");
2291 assert_eq!(vt.row_text(2), "");
2292 }
2293
2294 #[test]
2295 fn utf8_basic_multibyte() {
2296 let mut vt = VirtualTerminal::new(10, 3);
2297 vt.feed("Aé B".as_bytes());
2299 assert_eq!(vt.row_text(0), "Aé B");
2300 assert_eq!(vt.cursor(), (4, 0));
2301 }
2302
2303 #[test]
2304 fn wide_char_basic() {
2305 let mut vt = VirtualTerminal::new(10, 3);
2306 vt.feed("A中B".as_bytes());
2308 assert_eq!(vt.row_text(0), "A中B");
2309 assert_eq!(vt.cursor(), (4, 0)); }
2311
2312 #[test]
2313 fn wide_char_wraps_at_last_column() {
2314 let mut vt = VirtualTerminal::new(5, 3);
2315 vt.feed("ABCD中".as_bytes());
2317 assert_eq!(vt.row_text(0), "ABCD");
2318 assert_eq!(vt.row_text(1), "中");
2319 assert_eq!(vt.cursor(), (2, 1));
2320 }
2321
2322 #[test]
2323 fn wide_char_on_single_column_enters_valid_pending_wrap_state() {
2324 let mut vt = VirtualTerminal::new(1, 2);
2325 vt.feed("中".as_bytes());
2326
2327 assert_eq!(vt.char_at(0, 1), Some('中'));
2328 assert_eq!(vt.cursor(), (1, 1));
2329 assert_invariants(&vt);
2330 }
2331
2332 #[test]
2333 fn narrow_overwrites_wide_lead() {
2334 let mut vt = VirtualTerminal::new(10, 3);
2335 vt.feed("中".as_bytes()); vt.feed(b"\x1b[1;1HX"); assert_eq!(vt.row_text(0), "X");
2339 assert_eq!(vt.cursor(), (1, 0));
2340 }
2341
2342 #[test]
2343 fn narrow_overwrites_wide_continuation() {
2344 let mut vt = VirtualTerminal::new(10, 3);
2345 vt.feed("中".as_bytes()); vt.feed(b"\x1b[1;2HX"); assert_eq!(vt.row_text(0), " X");
2349 assert_eq!(vt.cursor(), (2, 0));
2350 }
2351
2352 #[test]
2355 fn default_tab_stops_every_8() {
2356 let vt = VirtualTerminal::new(20, 3);
2357 assert!(!vt.tab_stops[0]);
2359 assert!(vt.tab_stops[8]);
2360 assert!(vt.tab_stops[16]);
2361 assert!(!vt.tab_stops[1]);
2362 assert!(!vt.tab_stops[7]);
2363 }
2364
2365 #[test]
2366 fn hts_sets_custom_tab_stop() {
2367 let mut vt = VirtualTerminal::new(20, 3);
2368 vt.feed(b"\x1b[1;6H\x1bH");
2370 assert!(vt.tab_stops[5]);
2371 vt.feed(b"\x1b[1;1H\t");
2373 assert_eq!(vt.cursor(), (5, 0));
2374 }
2375
2376 #[test]
2377 fn tbc_clears_single_tab_stop() {
2378 let mut vt = VirtualTerminal::new(20, 3);
2379 vt.feed(b"\x1b[1;9H\x1b[0g");
2381 assert!(!vt.tab_stops[8]);
2382 vt.feed(b"\x1b[1;1H\t");
2384 assert_eq!(vt.cursor(), (16, 0));
2385 }
2386
2387 #[test]
2388 fn tbc_clears_all_tab_stops() {
2389 let mut vt = VirtualTerminal::new(20, 3);
2390 vt.feed(b"\x1b[3g");
2392 vt.feed(b"\x1b[1;1H\t");
2394 assert_eq!(vt.cursor(), (19, 0));
2395 }
2396
2397 #[test]
2398 fn cbt_moves_to_previous_tab_stop() {
2399 let mut vt = VirtualTerminal::new(20, 3);
2400 vt.feed(b"\x1b[1;11H\x1b[Z");
2402 assert_eq!(vt.cursor(), (8, 0));
2403 }
2404
2405 #[test]
2406 fn cbt_at_col_zero() {
2407 let mut vt = VirtualTerminal::new(20, 3);
2408 vt.feed(b"\x1b[Z");
2410 assert_eq!(vt.cursor(), (0, 0));
2411 }
2412
2413 #[test]
2414 fn reset_restores_default_tab_stops() {
2415 let mut vt = VirtualTerminal::new(20, 3);
2416 vt.feed(b"\x1b[3g");
2418 assert!(!vt.tab_stops[8]);
2419 vt.feed(b"\x1bc"); assert!(vt.tab_stops[8]);
2421 }
2422
2423 #[test]
2426 fn irm_insert_mode_shifts_right() {
2427 let mut vt = VirtualTerminal::new(10, 3);
2428 vt.feed(b"ABCDE");
2429 vt.feed(b"\x1b[4h\x1b[1;3HXY");
2431 assert_eq!(vt.row_text(0), "ABXYCDE");
2433 }
2434
2435 #[test]
2436 fn irm_replace_mode_default() {
2437 let mut vt = VirtualTerminal::new(10, 3);
2438 vt.feed(b"ABCDE");
2439 vt.feed(b"\x1b[1;3HXY");
2441 assert_eq!(vt.row_text(0), "ABXYE");
2442 }
2443
2444 #[test]
2445 fn irm_disable_returns_to_replace() {
2446 let mut vt = VirtualTerminal::new(10, 3);
2447 vt.feed(b"ABCDE");
2448 vt.feed(b"\x1b[4h\x1b[4l\x1b[1;3HXY");
2450 assert_eq!(vt.row_text(0), "ABXYE");
2452 }
2453
2454 #[test]
2455 fn irm_insert_pushes_off_right_edge() {
2456 let mut vt = VirtualTerminal::new(5, 3);
2457 vt.feed(b"ABCDE");
2458 vt.feed(b"\x1b[4h\x1b[1;1HX");
2460 assert_eq!(vt.row_text(0), "XABCD");
2462 }
2463
2464 #[test]
2467 fn decawm_enabled_wraps_at_edge() {
2468 let mut vt = VirtualTerminal::new(5, 3);
2469 vt.feed(b"ABCDEF");
2471 assert_eq!(vt.row_text(0), "ABCDE");
2472 assert_eq!(vt.row_text(1), "F");
2473 }
2474
2475 #[test]
2476 fn decawm_disabled_no_wrap() {
2477 let mut vt = VirtualTerminal::new(5, 3);
2478 vt.feed(b"\x1b[?7l");
2480 vt.feed(b"ABCDEFGH");
2481 assert_eq!(vt.row_text(0), "ABCDH");
2483 assert_eq!(vt.row_text(1), "");
2484 assert_eq!(vt.cursor(), (4, 0));
2485 }
2486
2487 #[test]
2488 fn decawm_reenable_wraps_again() {
2489 let mut vt = VirtualTerminal::new(5, 3);
2490 vt.feed(b"\x1b[?7l\x1b[?7h");
2492 vt.feed(b"ABCDEF");
2493 assert_eq!(vt.row_text(0), "ABCDE");
2494 assert_eq!(vt.row_text(1), "F");
2495 }
2496
2497 #[test]
2500 fn dec_graphics_g0_designation() {
2501 let mut vt = VirtualTerminal::new(10, 3);
2502 vt.feed(b"\x1b(0qqxx\x1b(B");
2505 assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(2, 0).unwrap(), '\u{2502}'); assert_eq!(vt.char_at(3, 0).unwrap(), '\u{2502}'); }
2510
2511 #[test]
2512 fn dec_graphics_g1_with_so_si() {
2513 let mut vt = VirtualTerminal::new(10, 3);
2514 vt.feed(b"\x1b)0\x0el\x0fl");
2518 assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'l');
2520 }
2521
2522 #[test]
2523 fn dec_graphics_box_chars() {
2524 let mut vt = VirtualTerminal::new(10, 3);
2525 vt.feed(b"\x1b(0lkjmn\x1b(B");
2527 assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2510}'); assert_eq!(vt.char_at(2, 0).unwrap(), '\u{2518}'); assert_eq!(vt.char_at(3, 0).unwrap(), '\u{2514}'); assert_eq!(vt.char_at(4, 0).unwrap(), '\u{253C}'); }
2533
2534 #[test]
2535 fn charset_reset_restores_ascii() {
2536 let mut vt = VirtualTerminal::new(10, 3);
2537 vt.feed(b"\x1b(0");
2539 vt.feed(b"\x1bc"); vt.feed(b"q");
2541 assert_eq!(vt.char_at(0, 0).unwrap(), 'q'); }
2543
2544 #[test]
2545 fn charset_soft_reset_restores_ascii() {
2546 let mut vt = VirtualTerminal::new(10, 3);
2547 vt.feed(b"\x1b(0");
2549 vt.feed(b"\x1b[!p"); vt.feed(b"q");
2551 assert_eq!(vt.char_at(0, 0).unwrap(), 'q'); }
2553
2554 #[test]
2555 fn so_si_toggle_charset() {
2556 let mut vt = VirtualTerminal::new(10, 3);
2557 vt.feed(b"\x1b)0");
2559 vt.feed(b"A"); vt.feed(b"\x0e"); vt.feed(b"q"); vt.feed(b"\x0f"); vt.feed(b"B"); assert_eq!(vt.char_at(0, 0).unwrap(), 'A');
2565 assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(2, 0).unwrap(), 'B');
2567 }
2568
2569 #[test]
2570 fn ascii_passthrough_in_dec_graphics() {
2571 let mut vt = VirtualTerminal::new(10, 3);
2572 vt.feed(b"\x1b(0ABC\x1b(B");
2574 assert_eq!(vt.char_at(0, 0).unwrap(), 'A');
2575 assert_eq!(vt.char_at(1, 0).unwrap(), 'B');
2576 assert_eq!(vt.char_at(2, 0).unwrap(), 'C');
2577 }
2578
2579 #[test]
2582 fn ich_basic_insert() {
2583 let mut vt = VirtualTerminal::new(10, 3);
2584 vt.feed(b"ABCDE");
2585 vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "AB CDE");
2588 assert_eq!(vt.cursor(), (2, 0));
2589 assert_invariants(&vt);
2590 }
2591
2592 #[test]
2593 fn ich_pushes_off_right_edge() {
2594 let mut vt = VirtualTerminal::new(5, 3);
2595 vt.feed(b"ABCDE");
2596 vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "A BC");
2599 assert_invariants(&vt);
2600 }
2601
2602 #[test]
2603 fn ich_at_wide_char_continuation() {
2604 let mut vt = VirtualTerminal::new(10, 3);
2605 vt.feed("A中B".as_bytes()); vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "A B");
2610 assert_invariants(&vt);
2611 }
2612
2613 #[test]
2616 fn dch_basic_delete() {
2617 let mut vt = VirtualTerminal::new(10, 3);
2618 vt.feed(b"ABCDE");
2619 vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[2P"); assert_eq!(vt.row_text(0), "ADE");
2622 assert_eq!(vt.cursor(), (1, 0));
2623 assert_invariants(&vt);
2624 }
2625
2626 #[test]
2627 fn dch_fills_blanks_at_end() {
2628 let mut vt = VirtualTerminal::new(5, 3);
2629 vt.feed(b"ABCDE");
2630 vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b[3P"); assert_eq!(vt.row_text(0), "DE");
2633 assert_invariants(&vt);
2634 }
2635
2636 #[test]
2637 fn dch_at_wide_char_boundary() {
2638 let mut vt = VirtualTerminal::new(10, 3);
2639 vt.feed("A中B".as_bytes()); vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[1P"); assert_eq!(vt.row_text(0), "A B");
2644 assert_invariants(&vt);
2645 }
2646
2647 #[test]
2650 fn ech_basic_erase() {
2651 let mut vt = VirtualTerminal::new(10, 3);
2652 vt.feed(b"ABCDE");
2653 vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[3X"); assert_eq!(vt.row_text(0), "A E");
2656 assert_eq!(vt.cursor(), (1, 0)); assert_invariants(&vt);
2658 }
2659
2660 #[test]
2661 fn ech_does_not_move_cursor() {
2662 let mut vt = VirtualTerminal::new(10, 3);
2663 vt.feed(b"ABCDE");
2664 vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[1X");
2666 assert_eq!(vt.cursor(), (2, 0));
2667 assert_eq!(vt.char_at(2, 0), Some(' '));
2668 assert_eq!(vt.char_at(3, 0), Some('D'));
2669 assert_invariants(&vt);
2670 }
2671
2672 #[test]
2673 fn ech_at_wide_char_continuation() {
2674 let mut vt = VirtualTerminal::new(10, 3);
2675 vt.feed("X中Y".as_bytes()); vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[1X"); assert_eq!(vt.row_text(0), "X Y");
2680 assert_invariants(&vt);
2681 }
2682
2683 #[test]
2684 fn ech_clamped_to_line_end() {
2685 let mut vt = VirtualTerminal::new(5, 3);
2686 vt.feed(b"ABCDE");
2687 vt.feed(b"\x1b[1;4H"); vt.feed(b"\x1b[99X"); assert_eq!(vt.row_text(0), "ABC");
2690 assert_invariants(&vt);
2691 }
2692
2693 #[test]
2696 fn il_basic_insert_line() {
2697 let mut vt = VirtualTerminal::new(5, 5);
2698 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2699 vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA");
2702 assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB");
2704 assert_eq!(vt.row_text(3), "CCCCC");
2705 assert_eq!(vt.row_text(4), "DDDDD");
2706 }
2708
2709 #[test]
2710 fn il_within_scroll_region() {
2711 let mut vt = VirtualTerminal::new(5, 5);
2712 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2713 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB"); assert_eq!(vt.row_text(3), "CCCCC"); assert_eq!(vt.row_text(4), "EEEEE"); }
2722
2723 #[test]
2724 fn il_outside_scroll_region_ignored() {
2725 let mut vt = VirtualTerminal::new(5, 5);
2726 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2727 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA");
2731 assert_eq!(vt.row_text(1), "BBBBB");
2732 assert_invariants(&vt);
2733 }
2734
2735 #[test]
2738 fn dl_basic_delete_line() {
2739 let mut vt = VirtualTerminal::new(5, 5);
2740 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2741 vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(0), "AAAAA");
2744 assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD");
2746 assert_eq!(vt.row_text(3), "EEEEE");
2747 assert_eq!(vt.row_text(4), ""); }
2749
2750 #[test]
2751 fn dl_within_scroll_region() {
2752 let mut vt = VirtualTerminal::new(5, 5);
2753 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2754 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD"); assert_eq!(vt.row_text(3), ""); assert_eq!(vt.row_text(4), "EEEEE"); }
2763
2764 #[test]
2765 fn dl_outside_scroll_region_ignored() {
2766 let mut vt = VirtualTerminal::new(5, 5);
2767 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2768 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[5;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(4), "EEEEE");
2772 }
2773
2774 #[test]
2777 fn su_scroll_up_within_region() {
2778 let mut vt = VirtualTerminal::new(5, 5);
2779 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2780 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1S"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD"); assert_eq!(vt.row_text(3), ""); assert_eq!(vt.row_text(4), "EEEEE"); assert_eq!(vt.scrollback_len(), 1);
2788 assert_eq!(vt.scrollback_line(0), Some("BBBBB".to_string()));
2789 }
2790
2791 #[test]
2792 fn sd_scroll_down_within_region() {
2793 let mut vt = VirtualTerminal::new(5, 5);
2794 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2795 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1T"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB"); assert_eq!(vt.row_text(3), "CCCCC"); assert_eq!(vt.row_text(4), "EEEEE"); }
2803
2804 #[test]
2805 fn su_multiple_lines() {
2806 let mut vt = VirtualTerminal::new(5, 3);
2807 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC");
2808 vt.feed(b"\x1b[2S"); assert_eq!(vt.row_text(0), "CCCCC");
2810 assert_eq!(vt.row_text(1), "");
2811 assert_eq!(vt.row_text(2), "");
2812 assert_eq!(vt.scrollback_len(), 2);
2813 assert_eq!(vt.scrollback_line(0), Some("AAAAA".to_string()));
2814 assert_eq!(vt.scrollback_line(1), Some("BBBBB".to_string()));
2815 }
2816
2817 #[test]
2820 fn rep_basic_repeat() {
2821 let mut vt = VirtualTerminal::new(10, 3);
2822 vt.feed(b"X\x1b[3b"); assert_eq!(vt.row_text(0), "XXXX");
2824 assert_eq!(vt.cursor(), (4, 0));
2825 }
2826
2827 #[test]
2828 fn rep_no_previous_char() {
2829 let mut vt = VirtualTerminal::new(10, 3);
2830 vt.feed(b"\x1b[5b"); assert_eq!(vt.row_text(0), "");
2832 assert_eq!(vt.cursor(), (0, 0));
2833 }
2834
2835 #[test]
2836 fn rep_wraps_across_lines() {
2837 let mut vt = VirtualTerminal::new(5, 3);
2838 vt.feed(b"A\x1b[6b"); assert_eq!(vt.row_text(0), "AAAAA");
2840 assert_eq!(vt.row_text(1), "AA");
2841 assert_eq!(vt.cursor(), (2, 1));
2842 }
2843
2844 #[test]
2847 fn decom_cup_relative_to_scroll_region() {
2848 let mut vt = VirtualTerminal::new(10, 10);
2849 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[1;1H");
2853 assert_eq!(vt.cursor(), (0, 2));
2854 vt.feed(b"\x1b[3;5H");
2856 assert_eq!(vt.cursor(), (4, 4));
2857 }
2858
2859 #[test]
2860 fn decom_clamps_to_scroll_region() {
2861 let mut vt = VirtualTerminal::new(10, 10);
2862 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[99;1H");
2866 assert_eq!(vt.cursor(), (0, 6)); }
2868
2869 #[test]
2870 fn decom_disable_homes_to_origin() {
2871 let mut vt = VirtualTerminal::new(10, 10);
2872 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[5;5H"); vt.feed(b"\x1b[?6l"); assert_eq!(vt.cursor(), (0, 0));
2877 }
2878
2879 #[test]
2880 fn decom_vpa_relative_to_scroll_region() {
2881 let mut vt = VirtualTerminal::new(10, 10);
2882 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[2d"); assert_eq!(vt.cursor().1, 3);
2886 }
2887
2888 #[test]
2889 fn decom_decstbm_homes_cursor() {
2890 let mut vt = VirtualTerminal::new(10, 10);
2891 vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[5;5H"); vt.feed(b"\x1b[3;7r"); assert_eq!(vt.cursor(), (0, 2)); }
2896
2897 #[test]
2900 fn ss2_translates_one_char_from_g2() {
2901 let mut vt = VirtualTerminal::new(10, 3);
2902 vt.feed(b"\x1b*0");
2904 vt.feed(b"\x1bNq"); vt.feed(b"q"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'q');
2909 }
2910
2911 #[test]
2912 fn ss3_translates_one_char_from_g3() {
2913 let mut vt = VirtualTerminal::new(10, 3);
2914 vt.feed(b"\x1b+0");
2916 vt.feed(b"\x1bOx"); vt.feed(b"x"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2502}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'x');
2921 }
2922
2923 #[test]
2924 fn ss2_only_affects_one_character() {
2925 let mut vt = VirtualTerminal::new(10, 3);
2926 vt.feed(b"\x1b*0"); vt.feed(b"\x1bNlk"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'k'); }
2931
2932 #[test]
2935 fn ls2_invokes_g2_into_gl() {
2936 let mut vt = VirtualTerminal::new(10, 3);
2937 vt.feed(b"\x1b*0"); vt.feed(b"\x1bn"); vt.feed(b"jm"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2518}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2514}'); }
2943
2944 #[test]
2945 fn ls3_invokes_g3_into_gl() {
2946 let mut vt = VirtualTerminal::new(10, 3);
2947 vt.feed(b"\x1b+0"); vt.feed(b"\x1bo"); vt.feed(b"n"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{253C}'); }
2952
2953 #[test]
2954 fn ls2_persists_across_characters() {
2955 let mut vt = VirtualTerminal::new(10, 3);
2956 vt.feed(b"\x1b*0"); vt.feed(b"\x1bn"); vt.feed(b"tuvw"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{251C}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2524}'); assert_eq!(vt.char_at(2, 0).unwrap(), '\u{2534}'); assert_eq!(vt.char_at(3, 0).unwrap(), '\u{252C}'); }
2964
2965 #[test]
2968 fn alt_screen_1047_no_cursor_save() {
2969 let mut vt = VirtualTerminal::new(10, 3);
2970 vt.feed(b"Main");
2971 vt.feed(b"\x1b[1;5H"); let (_cx, _cy) = vt.cursor();
2973 vt.feed(b"\x1b[?1047h"); assert!(vt.is_alternate_screen());
2975 assert_eq!(vt.row_text(0), ""); vt.feed(b"Alt");
2978 vt.feed(b"\x1b[?1047l"); assert!(!vt.is_alternate_screen());
2980 assert_eq!(vt.row_text(0), "Main"); let (_, _) = vt.cursor();
2984 }
2985
2986 #[test]
2987 fn alt_screen_1047_double_enter_ignored() {
2988 let mut vt = VirtualTerminal::new(10, 3);
2989 vt.feed(b"Main");
2990 vt.feed(b"\x1b[?1047h"); vt.feed(b"\x1b[?1047h"); assert!(vt.is_alternate_screen());
2993 assert_eq!(vt.row_text(0), ""); }
2995
2996 #[test]
2999 fn decstbm_invalid_range_ignored() {
3000 let mut vt = VirtualTerminal::new(10, 5);
3001 vt.feed(b"\x1b[4;2r");
3003 assert_eq!(vt.scroll_top, 0);
3004 assert_eq!(vt.scroll_bottom, 4);
3005 }
3006
3007 #[test]
3008 fn decstbm_bottom_at_screen_edge() {
3009 let mut vt = VirtualTerminal::new(10, 5);
3010 vt.feed(b"\x1b[2;5r"); assert_eq!(vt.scroll_top, 1);
3012 assert_eq!(vt.scroll_bottom, 4);
3013 }
3014
3015 #[test]
3016 fn decstbm_homes_cursor_without_decom() {
3017 let mut vt = VirtualTerminal::new(10, 5);
3018 vt.feed(b"\x1b[3;3H"); vt.feed(b"\x1b[2;4r"); assert_eq!(vt.cursor(), (0, 0));
3021 }
3022
3023 #[test]
3026 fn erase_display_mode1_from_start_to_cursor() {
3027 let mut vt = VirtualTerminal::new(10, 3);
3028 vt.feed(b"AAAAAAAAAA");
3029 vt.feed(b"BBBBBBBBBB");
3030 vt.feed(b"CCCCCCCCCC");
3031 vt.feed(b"\x1b[2;5H"); vt.feed(b"\x1b[1J"); assert_eq!(vt.row_text(0), ""); assert_eq!(vt.row_text(1), " BBBBB"); assert_eq!(vt.row_text(2), "CCCCCCCCCC"); }
3037
3038 #[test]
3039 fn erase_display_mode3_clears_scrollback() {
3040 let mut vt = VirtualTerminal::new(5, 2);
3041 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC"); assert!(vt.scrollback_len() > 0);
3043 vt.feed(b"\x1b[3J"); assert_eq!(vt.scrollback_len(), 0);
3045 }
3046
3047 #[test]
3050 fn erase_line_mode1() {
3051 let mut vt = VirtualTerminal::new(10, 3);
3052 vt.feed(b"ABCDEFGHIJ");
3053 vt.feed(b"\x1b[1;6H"); vt.feed(b"\x1b[1K"); assert_eq!(vt.row_text(0), " GHIJ");
3056 }
3057
3058 #[test]
3061 fn soft_reset_restores_defaults() {
3062 let mut vt = VirtualTerminal::new(10, 5);
3063 vt.feed(b"\x1b[?7l"); vt.feed(b"\x1b[?25l"); vt.feed(b"\x1b[4h"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[!p");
3071 assert!(vt.cursor_visible());
3072 assert_eq!(vt.scroll_top, 0);
3073 assert_eq!(vt.scroll_bottom, 4);
3074 vt.feed(b"\x1b[1;1H");
3076 vt.feed(b"ABCDEFGHIJK");
3077 assert_eq!(vt.row_text(0), "ABCDEFGHIJ");
3078 assert_eq!(vt.row_text(1), "K"); }
3080
3081 #[test]
3084 fn erase_line_splits_wide_char_at_boundary() {
3085 let mut vt = VirtualTerminal::new(10, 3);
3086 vt.feed("AB中DE".as_bytes()); vt.feed(b"\x1b[1;4H"); vt.feed(b"\x1b[K"); assert_invariants(&vt);
3090 assert_eq!(vt.char_at(2, 0), Some(' '));
3092 }
3093
3094 #[test]
3095 fn dch_wide_char_continuation_at_boundary() {
3096 let mut vt = VirtualTerminal::new(10, 3);
3097 vt.feed("AB中DE".as_bytes());
3098 vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2P"); assert_eq!(vt.row_text(0), "ABDE");
3101 assert_invariants(&vt);
3102 }
3103
3104 #[test]
3107 fn invariants_after_insert_delete_scroll_sequence() {
3108 let mut vt = VirtualTerminal::new(8, 4);
3109 vt.feed(b"AABBCCDD");
3111 vt.feed(b"EEFFGGHH");
3112 vt.feed(b"IIJJKKLL");
3113 vt.feed(b"MMNNOOPP");
3114 vt.feed(b"\x1b[2;3r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); vt.feed(b"\x1b[1M"); vt.feed(b"\x1b[1S"); vt.feed(b"\x1b[1T"); vt.feed(b"\x1b[2;4H\x1b[2@"); vt.feed(b"\x1b[1P"); assert_invariants(&vt);
3123 }
3124
3125 #[test]
3126 fn invariants_after_wide_char_operations() {
3127 let mut vt = VirtualTerminal::new(6, 3);
3128 vt.feed("中文字".as_bytes()); vt.feed(b"\x1b[1;1H\x1b[2@"); assert_invariants(&vt);
3132
3133 let mut vt2 = VirtualTerminal::new(6, 3);
3134 vt2.feed("中文字".as_bytes());
3135 vt2.feed(b"\x1b[1;1H\x1b[2P"); assert_invariants(&vt2);
3137
3138 let mut vt3 = VirtualTerminal::new(6, 3);
3139 vt3.feed("中文字".as_bytes());
3140 vt3.feed(b"\x1b[1;3H\x1b[2X"); assert_invariants(&vt3);
3142 }
3143
3144 #[test]
3147 fn put_char_basic_ascii() {
3148 let mut vt = VirtualTerminal::new(80, 24);
3149 vt.put_char('H');
3150 vt.put_char('e');
3151 vt.put_char('l');
3152 vt.put_char('l');
3153 vt.put_char('o');
3154 assert_eq!(vt.row_text(0), "Hello");
3155 assert_eq!(vt.cursor(), (5, 0));
3156 assert_invariants(&vt);
3157 }
3158
3159 #[test]
3160 fn put_char_matches_feed_str() {
3161 let mut vt_feed = VirtualTerminal::new(40, 10);
3162 vt_feed.feed_str("Hello!");
3163
3164 let mut vt_put = VirtualTerminal::new(40, 10);
3165 for ch in "Hello!".chars() {
3166 vt_put.put_char(ch);
3167 }
3168
3169 assert_eq!(vt_feed.screen_text(), vt_put.screen_text());
3170 assert_eq!(vt_feed.cursor(), vt_put.cursor());
3171 }
3172
3173 #[test]
3174 fn put_char_wide_characters() {
3175 let mut vt = VirtualTerminal::new(10, 3);
3176 vt.put_char('中');
3177 vt.put_char('文');
3178 assert_eq!(vt.row_text(0), "中文");
3179 assert_eq!(vt.cursor(), (4, 0)); assert_invariants(&vt);
3181 }
3182
3183 #[test]
3184 fn put_char_autowrap() {
3185 let mut vt = VirtualTerminal::new(5, 3);
3186 for ch in "ABCDE".chars() {
3187 vt.put_char(ch);
3188 }
3189 assert_eq!(vt.row_text(0), "ABCDE");
3191 vt.put_char('F');
3193 assert_eq!(vt.row_text(1), "F");
3194 assert_eq!(vt.cursor(), (1, 1));
3195 assert_invariants(&vt);
3196 }
3197
3198 #[test]
3199 fn put_char_wide_wrap_at_margin() {
3200 let mut vt = VirtualTerminal::new(5, 3);
3203 for ch in "ABCD".chars() {
3204 vt.put_char(ch);
3205 }
3206 vt.put_char('中'); assert_eq!(vt.row_text(0), "ABCD");
3208 assert_eq!(vt.row_text(1), "中");
3209 assert_invariants(&vt);
3210 }
3211
3212 #[test]
3213 fn put_char_zero_width_skipped() {
3214 let mut vt = VirtualTerminal::new(10, 3);
3215 vt.put_char('A');
3216 vt.put_char('\u{0300}'); assert_eq!(vt.cursor(), (1, 0)); assert_eq!(vt.char_at(0, 0), Some('A'));
3219 assert_invariants(&vt);
3220 }
3221
3222 #[test]
3223 fn put_char_preserves_style() {
3224 let mut vt = VirtualTerminal::new(10, 3);
3225 vt.feed(b"\x1b[1m");
3227 vt.put_char('X');
3228 let style = vt.style_at(0, 0).unwrap();
3229 assert!(style.bold);
3230 assert_invariants(&vt);
3231 }
3232
3233 #[test]
3236 fn put_str_basic() {
3237 let mut vt = VirtualTerminal::new(20, 3);
3238 vt.put_str("Hello, world!");
3239 assert_eq!(vt.row_text(0), "Hello, world!");
3240 assert_eq!(vt.cursor(), (13, 0));
3241 assert_invariants(&vt);
3242 }
3243
3244 #[test]
3245 fn put_str_empty() {
3246 let mut vt = VirtualTerminal::new(10, 1);
3247 vt.put_str("");
3248 assert_eq!(vt.cursor(), (0, 0));
3249 assert_eq!(vt.row_text(0), "");
3250 assert_invariants(&vt);
3251 }
3252
3253 #[test]
3254 fn put_str_wraps_at_margin() {
3255 let mut vt = VirtualTerminal::new(5, 3);
3256 vt.put_str("ABCDEFGH");
3257 assert_eq!(vt.row_text(0), "ABCDE");
3258 assert_eq!(vt.row_text(1), "FGH");
3259 assert_eq!(vt.cursor(), (3, 1));
3260 assert_invariants(&vt);
3261 }
3262
3263 #[test]
3264 fn put_str_wide_chars() {
3265 let mut vt = VirtualTerminal::new(10, 1);
3266 vt.put_str("中文");
3267 assert_eq!(vt.row_text(0), "中文");
3268 assert_eq!(vt.cursor(), (4, 0));
3269 assert_invariants(&vt);
3270 }
3271
3272 #[test]
3273 fn put_str_matches_put_char_sequence() {
3274 let text = "Hi 😀!";
3275 let mut vt_str = VirtualTerminal::new(20, 3);
3276 vt_str.put_str(text);
3277
3278 let mut vt_char = VirtualTerminal::new(20, 3);
3279 for ch in text.chars() {
3280 vt_char.put_char(ch);
3281 }
3282
3283 assert_eq!(vt_str.screen_text(), vt_char.screen_text());
3284 assert_eq!(vt_str.cursor(), vt_char.cursor());
3285 }
3286
3287 #[test]
3288 fn put_str_does_not_interpret_escapes() {
3289 let mut vt = VirtualTerminal::new(20, 1);
3290 vt.put_str("\x1b[1mBold");
3292 let text = vt.row_text(0);
3295 assert!(text.contains("[1mBold"), "got: {text:?}");
3296 let style = vt.style_at(0, 0).unwrap();
3298 assert!(!style.bold);
3299 assert_invariants(&vt);
3300 }
3301
3302 #[test]
3305 fn set_cursor_position_basic() {
3306 let mut vt = VirtualTerminal::new(80, 24);
3307 vt.set_cursor_position(10, 5);
3308 assert_eq!(vt.cursor(), (10, 5));
3309 assert_invariants(&vt);
3310 }
3311
3312 #[test]
3313 fn set_cursor_position_clamps_x() {
3314 let mut vt = VirtualTerminal::new(80, 24);
3315 vt.set_cursor_position(200, 5);
3316 assert_eq!(vt.cursor(), (79, 5));
3317 assert_invariants(&vt);
3318 }
3319
3320 #[test]
3321 fn set_cursor_position_clamps_y() {
3322 let mut vt = VirtualTerminal::new(80, 24);
3323 vt.set_cursor_position(10, 100);
3324 assert_eq!(vt.cursor(), (10, 23));
3325 assert_invariants(&vt);
3326 }
3327
3328 #[test]
3329 fn set_cursor_position_origin() {
3330 let mut vt = VirtualTerminal::new(80, 24);
3331 vt.put_str("test");
3332 vt.set_cursor_position(0, 0);
3333 assert_eq!(vt.cursor(), (0, 0));
3334 assert_invariants(&vt);
3335 }
3336
3337 #[test]
3338 fn set_cursor_position_then_put_char() {
3339 let mut vt = VirtualTerminal::new(10, 3);
3340 vt.set_cursor_position(3, 1);
3341 vt.put_char('X');
3342 assert_eq!(vt.char_at(3, 1), Some('X'));
3343 assert_eq!(vt.cursor(), (4, 1));
3344 assert_invariants(&vt);
3345 }
3346
3347 #[test]
3350 fn clear_empties_display() {
3351 let mut vt = VirtualTerminal::new(10, 3);
3352 vt.put_str("Hello");
3353 vt.set_cursor_position(0, 1);
3354 vt.put_str("World");
3355 vt.clear();
3356 assert_eq!(vt.row_text(0), "");
3357 assert_eq!(vt.row_text(1), "");
3358 assert_invariants(&vt);
3359 }
3360
3361 #[test]
3362 fn clear_preserves_cursor_position() {
3363 let mut vt = VirtualTerminal::new(10, 3);
3364 vt.set_cursor_position(5, 2);
3365 vt.clear();
3366 assert_eq!(vt.cursor(), (5, 2));
3367 assert_invariants(&vt);
3368 }
3369
3370 #[test]
3371 fn clear_does_not_affect_scrollback() {
3372 let mut vt = VirtualTerminal::new(5, 2);
3373 vt.put_str("AAAAABBBBBCCCCC");
3375 let sb_before = vt.scrollback_len();
3376 assert!(sb_before > 0);
3377 vt.clear();
3378 assert_eq!(vt.scrollback_len(), sb_before);
3379 assert_invariants(&vt);
3380 }
3381
3382 #[test]
3385 fn clear_scrollback_empties_history() {
3386 let mut vt = VirtualTerminal::new(5, 2);
3387 vt.put_str("AAAAABBBBBCCCCC");
3388 assert!(vt.scrollback_len() > 0);
3389 vt.clear_scrollback();
3390 assert_eq!(vt.scrollback_len(), 0);
3391 assert_invariants(&vt);
3392 }
3393
3394 #[test]
3395 fn clear_scrollback_preserves_display() {
3396 let mut vt = VirtualTerminal::new(10, 2);
3397 vt.put_str("Hello");
3398 vt.set_cursor_position(0, 1);
3399 vt.put_str("World");
3400 vt.clear_scrollback();
3401 assert_eq!(vt.row_text(0), "Hello");
3402 assert_eq!(vt.row_text(1), "World");
3403 assert_invariants(&vt);
3404 }
3405}