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 += advance;
1517 } else {
1518 self.cursor_x = (self.cursor_x + advance).min(self.width.saturating_sub(1));
1520 }
1521 }
1522
1523 fn linefeed(&mut self) {
1524 if self.cursor_y == self.scroll_bottom {
1525 self.scroll_up();
1526 } else if self.cursor_y < self.height.saturating_sub(1) {
1527 self.cursor_y += 1;
1528 }
1529 }
1530
1531 fn scroll_up(&mut self) {
1532 let top_start = self.idx(0, self.scroll_top);
1535 let top_end = top_start + usize::from(self.width);
1536 let line: Vec<VCell> = self.grid[top_start..top_end].to_vec();
1537 self.scrollback.push_back(line);
1538 while self.scrollback.len() > self.max_scrollback {
1539 self.scrollback.pop_front();
1540 }
1541
1542 for row in self.scroll_top..self.scroll_bottom {
1544 let src_start = self.idx(0, row + 1);
1545 let dst_start = self.idx(0, row);
1546 let w = usize::from(self.width);
1547 let (left, right) = self.grid.split_at_mut(src_start);
1549 left[dst_start..dst_start + w].clone_from_slice(&right[..w]);
1550 }
1551
1552 let blank = self.styled_blank();
1554 let bottom_start = self.idx(0, self.scroll_bottom);
1555 for i in 0..usize::from(self.width) {
1556 self.grid[bottom_start + i] = blank.clone();
1557 }
1558 }
1559
1560 fn scroll_down(&mut self) {
1561 for row in (self.scroll_top + 1..=self.scroll_bottom).rev() {
1563 let src_start = self.idx(0, row - 1);
1564 let dst_start = self.idx(0, row);
1565 let w = usize::from(self.width);
1566 if src_start < dst_start {
1567 let (left, right) = self.grid.split_at_mut(dst_start);
1568 right[..w].clone_from_slice(&left[src_start..src_start + w]);
1569 }
1570 }
1571
1572 let blank = self.styled_blank();
1574 let top_start = self.idx(0, self.scroll_top);
1575 for i in 0..usize::from(self.width) {
1576 self.grid[top_start + i] = blank.clone();
1577 }
1578 }
1579
1580 fn styled_blank(&self) -> VCell {
1583 VCell {
1584 ch: ' ',
1585 style: self.current_style.clone(),
1586 }
1587 }
1588
1589 fn fixup_wide_erase_row(&mut self, row_y: u16, start_col: u16, count: u16) {
1593 let w = self.width;
1594 let sc = start_col;
1595 let n = count;
1596 if n == 0 || sc >= w {
1597 return;
1598 }
1599 let row_start = self.idx(0, row_y);
1600 if sc > 0 && self.grid[row_start + usize::from(sc)].ch == WIDE_CONTINUATION {
1602 self.grid[row_start + usize::from(sc - 1)] = VCell::default();
1603 }
1604 let end_col = sc.saturating_add(n);
1606 if end_col < w && self.grid[row_start + usize::from(end_col)].ch == WIDE_CONTINUATION {
1607 self.grid[row_start + usize::from(end_col)] = VCell::default();
1608 }
1609 }
1610
1611 fn erase_display(&mut self, mode: u16) {
1612 let blank = self.styled_blank();
1613 match mode {
1614 0 => {
1615 let count = self.width.saturating_sub(self.cursor_x);
1617 self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, count);
1618 let start = self.idx(self.cursor_x, self.cursor_y);
1619 for cell in &mut self.grid[start..] {
1620 *cell = blank.clone();
1621 }
1622 }
1623 1 => {
1624 let count = self.cursor_x + 1;
1626 self.fixup_wide_erase_row(self.cursor_y, 0, count);
1627 let end = self.idx(self.cursor_x, self.cursor_y) + 1;
1628 for cell in &mut self.grid[..end] {
1629 *cell = blank.clone();
1630 }
1631 }
1632 2 | 3 => {
1633 for cell in &mut self.grid {
1635 *cell = blank.clone();
1636 }
1637 if mode == 3 {
1638 self.scrollback.clear();
1639 }
1640 }
1641 _ => {}
1642 }
1643 }
1644
1645 fn erase_line(&mut self, mode: u16) {
1646 let y = self.cursor_y;
1647 let blank = self.styled_blank();
1648 let row_start = self.idx(0, y);
1649 match mode {
1650 0 => {
1651 let count = self.width.saturating_sub(self.cursor_x);
1653 self.fixup_wide_erase_row(y, self.cursor_x, count);
1654 let start = row_start + usize::from(self.cursor_x);
1655 let end = row_start + usize::from(self.width);
1656 for cell in &mut self.grid[start..end] {
1657 *cell = blank.clone();
1658 }
1659 }
1660 1 => {
1661 let count = self.cursor_x + 1;
1663 self.fixup_wide_erase_row(y, 0, count);
1664 let end = row_start + usize::from(count);
1665 for cell in &mut self.grid[row_start..end] {
1666 *cell = blank.clone();
1667 }
1668 }
1669 2 => {
1670 let end = row_start + usize::from(self.width);
1672 for cell in &mut self.grid[row_start..end] {
1673 *cell = blank.clone();
1674 }
1675 }
1676 _ => {}
1677 }
1678 }
1679
1680 fn reset(&mut self) {
1681 self.grid = vec![VCell::default(); usize::from(self.width) * usize::from(self.height)];
1682 self.cursor_x = 0;
1683 self.cursor_y = 0;
1684 self.cursor_visible = true;
1685 self.current_style = CellStyle::default();
1686 self.scrollback.clear();
1687 self.saved_cursor = None;
1688 self.scroll_top = 0;
1689 self.scroll_bottom = self.height.saturating_sub(1);
1690 self.title.clear();
1691 self.alternate_screen = false;
1692 self.alternate_grid = None;
1693 self.alternate_cursor = None;
1694 self.last_char = None;
1695 self.utf8_len = 0;
1696 self.utf8_expected = 0;
1697 self.tab_stops = Self::default_tab_stops(self.width);
1698 self.insert_mode = false;
1699 self.autowrap = true;
1700 self.charset_slots = [b'B'; 4];
1701 self.active_charset = 0;
1702 self.single_shift = None;
1703 }
1704
1705 fn param(params: &[u16], idx: usize, default: u16) -> u16 {
1706 params
1707 .get(idx)
1708 .copied()
1709 .filter(|&v| v > 0)
1710 .unwrap_or(default)
1711 }
1712}
1713
1714fn ansi_color(idx: u16) -> Color {
1717 match idx {
1718 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(),
1727 }
1728}
1729
1730fn ansi_bright_color(idx: u16) -> Color {
1731 match idx {
1732 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(),
1741 }
1742}
1743
1744fn parse_extended_color(params: &[u16], i: &mut usize) -> Option<Color> {
1746 if *i + 1 >= params.len() {
1747 return None;
1748 }
1749 match params[*i + 1] {
1750 2 => {
1751 if *i + 4 < params.len() {
1753 let r = params[*i + 2] as u8;
1754 let g = params[*i + 3] as u8;
1755 let b = params[*i + 4] as u8;
1756 *i += 4;
1757 Some(Color::new(r, g, b))
1758 } else {
1759 None
1760 }
1761 }
1762 5 => {
1763 if *i + 2 < params.len() {
1765 let idx = params[*i + 2];
1766 *i += 2;
1767 Some(color_256(idx))
1768 } else {
1769 None
1770 }
1771 }
1772 _ => None,
1773 }
1774}
1775
1776fn color_256(idx: u16) -> Color {
1778 match idx {
1779 0..=7 => ansi_color(idx),
1780 8..=15 => ansi_bright_color(idx - 8),
1781 16..=231 => {
1782 let n = idx - 16;
1784 let b = (n % 6) as u8;
1785 let g = ((n / 6) % 6) as u8;
1786 let r = (n / 36) as u8;
1787 let to_rgb = |v: u8| if v == 0 { 0u8 } else { 55 + 40 * v };
1788 Color::new(to_rgb(r), to_rgb(g), to_rgb(b))
1789 }
1790 232..=255 => {
1791 let v = (8 + 10 * (idx - 232)) as u8;
1793 Color::new(v, v, v)
1794 }
1795 _ => Color::default(),
1796 }
1797}
1798
1799#[cfg(test)]
1800mod tests {
1801 use super::*;
1802
1803 fn assert_invariants(vt: &VirtualTerminal) {
1804 assert!(vt.cursor_x <= vt.width);
1806 assert!(vt.cursor_y < vt.height);
1807 assert_eq!(vt.grid.len(), vt.width as usize * vt.height as usize);
1808 assert!(vt.scroll_top <= vt.scroll_bottom);
1809 assert!(vt.scroll_bottom < vt.height);
1810 for line in &vt.scrollback {
1811 assert_eq!(line.len(), vt.width as usize);
1812 }
1813 }
1814
1815 #[test]
1816 fn new_terminal_dimensions() {
1817 let vt = VirtualTerminal::new(80, 24);
1818 assert_eq!(vt.width(), 80);
1819 assert_eq!(vt.height(), 24);
1820 assert_eq!(vt.cursor(), (0, 0));
1821 assert!(vt.cursor_visible());
1822 }
1823
1824 #[test]
1825 #[should_panic(expected = "dimensions must be > 0")]
1826 fn zero_width_panics() {
1827 let _ = VirtualTerminal::new(0, 24);
1828 }
1829
1830 #[test]
1831 #[should_panic(expected = "dimensions must be > 0")]
1832 fn zero_height_panics() {
1833 let _ = VirtualTerminal::new(80, 0);
1834 }
1835
1836 #[test]
1837 fn invariants_hold_for_varied_inputs() {
1838 let inputs: [&[u8]; 6] = [
1839 b"",
1840 b"Hello",
1841 b"ABCDE\r\nFGHIJ",
1842 b"\x1b[2J",
1843 b"\x1b[1;1H\x1b[2;2H",
1844 b"\x1b[?1049hAlt\x1b[?1049l",
1845 ];
1846
1847 for width in 1..=6 {
1848 for height in 1..=4 {
1849 for input in inputs {
1850 let mut vt = VirtualTerminal::new(width, height);
1851 for chunk in input.chunks(3) {
1852 vt.feed(chunk);
1853 assert_invariants(&vt);
1854 }
1855 }
1856 }
1857 }
1858 }
1859
1860 #[test]
1861 fn plain_text_output() {
1862 let mut vt = VirtualTerminal::new(80, 24);
1863 vt.feed(b"Hello, World!");
1864 assert_eq!(vt.char_at(0, 0), Some('H'));
1865 assert_eq!(vt.char_at(12, 0), Some('!'));
1866 assert_eq!(vt.cursor(), (13, 0));
1867 assert_eq!(vt.row_text(0), "Hello, World!");
1868 }
1869
1870 #[test]
1871 fn newline_advances_cursor() {
1872 let mut vt = VirtualTerminal::new(80, 24);
1873 vt.feed(b"Line 1\r\nLine 2");
1874 assert_eq!(vt.row_text(0), "Line 1");
1875 assert_eq!(vt.row_text(1), "Line 2");
1876 assert_eq!(vt.cursor(), (6, 1));
1877 }
1878
1879 #[test]
1880 fn carriage_return() {
1881 let mut vt = VirtualTerminal::new(80, 24);
1882 vt.feed(b"AAAA\rBB");
1883 assert_eq!(vt.row_text(0), "BBAA");
1884 }
1885
1886 #[test]
1887 fn auto_wrap() {
1888 let mut vt = VirtualTerminal::new(5, 3);
1889 vt.feed(b"ABCDEFGH");
1890 assert_eq!(vt.row_text(0), "ABCDE");
1891 assert_eq!(vt.row_text(1), "FGH");
1892 assert_eq!(vt.cursor(), (3, 1));
1893 }
1894
1895 #[test]
1896 fn screen_immediate_wrap_quirk_wraps_on_last_column() {
1897 let mut vt = VirtualTerminal::with_quirks(5, 3, QuirkSet::gnu_screen());
1898 vt.feed(b"ABCDE");
1899 assert_eq!(vt.row_text(0), "ABCDE");
1900 assert_eq!(vt.cursor(), (0, 1));
1901
1902 vt.feed(b"F");
1903 assert_eq!(vt.row_text(1), "F");
1904 assert_eq!(vt.cursor(), (1, 1));
1905 }
1906
1907 #[test]
1908 fn scroll_on_overflow() {
1909 let mut vt = VirtualTerminal::new(10, 3);
1910 vt.feed(b"AAA\r\nBBB\r\nCCC\r\nDDD");
1911 assert_eq!(vt.row_text(0), "BBB");
1913 assert_eq!(vt.row_text(1), "CCC");
1914 assert_eq!(vt.row_text(2), "DDD");
1915 assert_eq!(vt.scrollback_len(), 1);
1916 assert_eq!(vt.scrollback_line(0), Some("AAA".to_string()));
1917 }
1918
1919 #[test]
1920 fn cursor_movement_csi() {
1921 let mut vt = VirtualTerminal::new(80, 24);
1922 vt.feed(b"\x1b[4;6H");
1924 assert_eq!(vt.cursor(), (5, 3));
1925 }
1926
1927 #[test]
1928 fn cursor_up_down_forward_back() {
1929 let mut vt = VirtualTerminal::new(80, 24);
1930 vt.feed(b"\x1b[10;10H"); vt.feed(b"\x1b[3A"); assert_eq!(vt.cursor(), (9, 6));
1933 vt.feed(b"\x1b[2B"); assert_eq!(vt.cursor(), (9, 8));
1935 vt.feed(b"\x1b[5C"); assert_eq!(vt.cursor(), (14, 8));
1937 vt.feed(b"\x1b[3D"); assert_eq!(vt.cursor(), (11, 8));
1939 }
1940
1941 #[test]
1942 fn cursor_clamps_to_bounds() {
1943 let mut vt = VirtualTerminal::new(10, 5);
1944 vt.feed(b"\x1b[100;100H");
1945 assert_eq!(vt.cursor(), (9, 4));
1946 vt.feed(b"\x1b[99A");
1947 assert_eq!(vt.cursor(), (9, 0));
1948 }
1949
1950 #[test]
1951 fn erase_to_end_of_line() {
1952 let mut vt = VirtualTerminal::new(80, 24);
1953 vt.feed(b"ABCDE");
1954 vt.feed(b"\x1b[1;6H"); vt.feed(b"\x1b[K"); assert_eq!(vt.row_text(0), "ABCDE");
1957 }
1958
1959 #[test]
1960 fn erase_entire_line() {
1961 let mut vt = VirtualTerminal::new(80, 24);
1962 vt.feed(b"ABCDE");
1963 vt.feed(b"\x1b[2K"); assert_eq!(vt.row_text(0), "");
1965 }
1966
1967 #[test]
1968 fn erase_display_from_cursor() {
1969 let mut vt = VirtualTerminal::new(10, 3);
1970 vt.feed(b"AAAAAAAAAA");
1971 vt.feed(b"BBBBBBBBBB");
1972 vt.feed(b"CCCCCCCCCC");
1973 vt.feed(b"\x1b[2;5H"); vt.feed(b"\x1b[J"); assert_eq!(vt.row_text(0), "AAAAAAAAAA");
1976 assert_eq!(vt.row_text(1), "BBBB");
1977 assert_eq!(vt.row_text(2), "");
1978 }
1979
1980 #[test]
1981 fn sgr_bold_and_color() {
1982 let mut vt = VirtualTerminal::new(80, 24);
1983 vt.feed(b"\x1b[1;31mHello\x1b[0m World");
1984 let style = vt.style_at(0, 0).unwrap();
1986 assert!(style.bold);
1987 assert_eq!(style.fg, Some(Color::new(170, 0, 0)));
1988 let style2 = vt.style_at(6, 0).unwrap();
1990 assert!(!style2.bold);
1991 assert_eq!(style2.fg, None);
1992 }
1993
1994 #[test]
1995 fn sgr_truecolor() {
1996 let mut vt = VirtualTerminal::new(80, 24);
1997 vt.feed(b"\x1b[38;2;100;200;50mX");
1998 let style = vt.style_at(0, 0).unwrap();
1999 assert_eq!(style.fg, Some(Color::new(100, 200, 50)));
2000 }
2001
2002 #[test]
2003 fn sgr_256_color() {
2004 let mut vt = VirtualTerminal::new(80, 24);
2005 vt.feed(b"\x1b[48;5;196mX"); let style = vt.style_at(0, 0).unwrap();
2007 assert!(style.bg.is_some());
2008 }
2009
2010 #[test]
2011 fn dec_save_restore_cursor() {
2012 let mut vt = VirtualTerminal::new(80, 24);
2013 vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b7"); vt.feed(b"\x1b[1;1H"); assert_eq!(vt.cursor(), (0, 0));
2017 vt.feed(b"\x1b8"); assert_eq!(vt.cursor(), (9, 4));
2019 }
2020
2021 #[test]
2022 fn tmux_nested_cursor_quirk_ignores_save_restore_in_alt_screen() {
2023 let mut vt = VirtualTerminal::with_quirks(80, 24, QuirkSet::tmux_nested());
2024 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));
2030 }
2031
2032 #[test]
2033 fn combined_quirks_apply_independently() {
2034 let quirks = QuirkSet::empty()
2035 .with_screen_immediate_wrap(true)
2036 .with_tmux_nested_cursor(true);
2037 let mut vt = VirtualTerminal::with_quirks(5, 3, quirks);
2038
2039 vt.feed(b"\x1b[?1049h");
2040 vt.feed(b"ABCDE");
2041 assert_eq!(vt.cursor(), (0, 1));
2042
2043 vt.feed(b"\x1b[2;2H\x1b7\x1b[1;1H\x1b8");
2044 assert_eq!(vt.cursor(), (0, 0));
2045 }
2046
2047 #[test]
2048 fn cursor_visibility() {
2049 let mut vt = VirtualTerminal::new(80, 24);
2050 assert!(vt.cursor_visible());
2051 vt.feed(b"\x1b[?25l"); assert!(!vt.cursor_visible());
2053 vt.feed(b"\x1b[?25h"); assert!(vt.cursor_visible());
2055 }
2056
2057 #[test]
2058 fn alternate_screen() {
2059 let mut vt = VirtualTerminal::new(10, 3);
2060 vt.feed(b"Main");
2061 assert_eq!(vt.row_text(0), "Main");
2062 assert!(!vt.is_alternate_screen());
2063
2064 vt.feed(b"\x1b[?1049h"); assert!(vt.is_alternate_screen());
2066 assert_eq!(vt.row_text(0), ""); vt.feed(b"Alt");
2068 assert_eq!(vt.row_text(0), "Alt");
2069
2070 vt.feed(b"\x1b[?1049l"); assert!(!vt.is_alternate_screen());
2072 assert_eq!(vt.row_text(0), "Main"); }
2074
2075 #[test]
2076 fn windows_no_alt_screen_quirk_ignores_alternate_buffer() {
2077 let mut vt = VirtualTerminal::with_quirks(10, 3, QuirkSet::windows_console());
2078 vt.feed(b"Main");
2079 vt.feed(b"\x1b[?1049h"); vt.feed(b"Alt");
2081 vt.feed(b"\x1b[?1049l"); assert!(!vt.is_alternate_screen());
2083 assert_eq!(vt.row_text(0), "MainAlt");
2084 }
2085
2086 #[test]
2087 fn osc_title() {
2088 let mut vt = VirtualTerminal::new(80, 24);
2089 vt.feed(b"\x1b]0;My Title\x07");
2090 assert_eq!(vt.title(), "My Title");
2091 }
2092
2093 #[test]
2094 fn full_reset() {
2095 let mut vt = VirtualTerminal::new(80, 24);
2096 vt.feed(b"Some text\x1b[1;31m");
2097 vt.feed(b"\x1bc"); assert_eq!(vt.cursor(), (0, 0));
2099 assert_eq!(vt.row_text(0), "");
2100 assert!(vt.cursor_visible());
2101 }
2102
2103 #[test]
2104 fn cpr_response_format() {
2105 let mut vt = VirtualTerminal::new(80, 24);
2106 vt.feed(b"\x1b[5;10H");
2107 let response = vt.cpr_response();
2108 assert_eq!(response, b"\x1b[5;10R");
2109 }
2110
2111 #[test]
2112 fn da1_response() {
2113 let vt = VirtualTerminal::new(80, 24);
2114 let response = vt.da1_response();
2115 assert_eq!(response, b"\x1b[?62;22c");
2116 }
2117
2118 #[test]
2119 fn scroll_region() {
2120 let mut vt = VirtualTerminal::new(10, 5);
2121 vt.feed(b"\x1b[2;4r");
2123 vt.feed(b"\x1b[1;1HROW1");
2125 vt.feed(b"\x1b[2;1HROW2");
2126 vt.feed(b"\x1b[3;1HROW3");
2127 vt.feed(b"\x1b[4;1HROW4");
2128 vt.feed(b"\x1b[5;1HROW5");
2129 assert_eq!(vt.row_text(0), "ROW1");
2130 assert_eq!(vt.row_text(4), "ROW5");
2131 }
2132
2133 #[test]
2134 fn tab_advances_to_stop() {
2135 let mut vt = VirtualTerminal::new(80, 24);
2136 vt.feed(b"AB\tC");
2137 assert_eq!(vt.char_at(8, 0), Some('C'));
2138 }
2139
2140 #[test]
2141 fn backspace() {
2142 let mut vt = VirtualTerminal::new(80, 24);
2143 vt.feed(b"ABC\x08X");
2144 assert_eq!(vt.row_text(0), "ABX");
2145 }
2146
2147 #[test]
2148 fn screen_text() {
2149 let mut vt = VirtualTerminal::new(10, 3);
2150 vt.feed(b"AAA\r\nBBB\r\nCCC");
2151 let text = vt.screen_text();
2152 assert_eq!(text, "AAA\nBBB\nCCC");
2153 }
2154
2155 #[test]
2156 fn scrollback_truncation() {
2157 let mut vt = VirtualTerminal::new(10, 2);
2158 vt.set_max_scrollback(3);
2159 for i in 0..5 {
2161 vt.feed_str(&format!("Line{i}\n"));
2162 }
2163 assert!(vt.scrollback_len() <= 3);
2164 }
2165
2166 #[test]
2167 fn out_of_bounds_cell_returns_none() {
2168 let vt = VirtualTerminal::new(10, 5);
2169 assert_eq!(vt.char_at(10, 0), None);
2170 assert_eq!(vt.char_at(0, 5), None);
2171 assert!(vt.style_at(99, 99).is_none());
2172 }
2173
2174 #[test]
2175 fn reverse_index_at_scroll_top() {
2176 let mut vt = VirtualTerminal::new(10, 5);
2177 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1bM"); assert_eq!(vt.cursor(), (0, 1));
2182 }
2183
2184 #[test]
2185 fn cursor_horizontal_absolute() {
2186 let mut vt = VirtualTerminal::new(10, 3);
2187 vt.feed(b"\x1b[10G");
2188 assert_eq!(vt.cursor(), (9, 0));
2189 }
2190
2191 #[test]
2192 fn vertical_position_absolute() {
2193 let mut vt = VirtualTerminal::new(80, 24);
2194 vt.feed(b"\x1b[5d");
2195 assert_eq!(vt.cursor(), (0, 4));
2196 }
2197
2198 #[test]
2199 fn cursor_next_previous_line() {
2200 let mut vt = VirtualTerminal::new(80, 24);
2201 vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b[2E"); assert_eq!(vt.cursor(), (0, 6));
2204 vt.feed(b"\x1b[1F"); assert_eq!(vt.cursor(), (0, 5));
2206 }
2207
2208 #[test]
2209 fn bright_colors() {
2210 let mut vt = VirtualTerminal::new(80, 24);
2211 vt.feed(b"\x1b[91mX"); let style = vt.style_at(0, 0).unwrap();
2213 assert_eq!(style.fg, Some(Color::new(255, 85, 85)));
2214 }
2215
2216 #[test]
2217 fn nel_next_line() {
2218 let mut vt = VirtualTerminal::new(10, 3);
2219 vt.feed(b"ABCDE\x1bEX");
2220 assert_eq!(vt.row_text(0), "ABCDE");
2222 assert_eq!(vt.row_text(1), "X");
2223 assert_eq!(vt.cursor(), (1, 1));
2224 }
2225
2226 #[test]
2227 fn nel_at_bottom_scrolls() {
2228 let mut vt = VirtualTerminal::new(5, 3);
2229 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC");
2230 vt.feed(b"\x1b[3;3H\x1bE"); assert_eq!(vt.row_text(0), "BBBBB");
2232 assert_eq!(vt.row_text(1), "CCCCC");
2233 assert_eq!(vt.row_text(2), "");
2234 assert_eq!(vt.cursor(), (0, 2));
2235 }
2236
2237 #[test]
2238 fn decaln_fills_with_e() {
2239 let mut vt = VirtualTerminal::new(5, 3);
2240 vt.feed(b"ABC\x1b#8");
2241 assert_eq!(vt.row_text(0), "EEEEE");
2242 assert_eq!(vt.row_text(1), "EEEEE");
2243 assert_eq!(vt.row_text(2), "EEEEE");
2244 assert_eq!(vt.cursor(), (0, 0));
2245 }
2246
2247 #[test]
2248 fn decaln_resets_scroll_region() {
2249 let mut vt = VirtualTerminal::new(5, 3);
2250 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");
2255 assert_eq!(vt.row_text(1), "ZZZZZ");
2256 assert_eq!(vt.row_text(2), "");
2257 }
2258
2259 #[test]
2260 fn utf8_basic_multibyte() {
2261 let mut vt = VirtualTerminal::new(10, 3);
2262 vt.feed("Aé B".as_bytes());
2264 assert_eq!(vt.row_text(0), "Aé B");
2265 assert_eq!(vt.cursor(), (4, 0));
2266 }
2267
2268 #[test]
2269 fn wide_char_basic() {
2270 let mut vt = VirtualTerminal::new(10, 3);
2271 vt.feed("A中B".as_bytes());
2273 assert_eq!(vt.row_text(0), "A中B");
2274 assert_eq!(vt.cursor(), (4, 0)); }
2276
2277 #[test]
2278 fn wide_char_wraps_at_last_column() {
2279 let mut vt = VirtualTerminal::new(5, 3);
2280 vt.feed("ABCD中".as_bytes());
2282 assert_eq!(vt.row_text(0), "ABCD");
2283 assert_eq!(vt.row_text(1), "中");
2284 assert_eq!(vt.cursor(), (2, 1));
2285 }
2286
2287 #[test]
2288 fn narrow_overwrites_wide_lead() {
2289 let mut vt = VirtualTerminal::new(10, 3);
2290 vt.feed("中".as_bytes()); vt.feed(b"\x1b[1;1HX"); assert_eq!(vt.row_text(0), "X");
2294 assert_eq!(vt.cursor(), (1, 0));
2295 }
2296
2297 #[test]
2298 fn narrow_overwrites_wide_continuation() {
2299 let mut vt = VirtualTerminal::new(10, 3);
2300 vt.feed("中".as_bytes()); vt.feed(b"\x1b[1;2HX"); assert_eq!(vt.row_text(0), " X");
2304 assert_eq!(vt.cursor(), (2, 0));
2305 }
2306
2307 #[test]
2310 fn default_tab_stops_every_8() {
2311 let vt = VirtualTerminal::new(20, 3);
2312 assert!(!vt.tab_stops[0]);
2314 assert!(vt.tab_stops[8]);
2315 assert!(vt.tab_stops[16]);
2316 assert!(!vt.tab_stops[1]);
2317 assert!(!vt.tab_stops[7]);
2318 }
2319
2320 #[test]
2321 fn hts_sets_custom_tab_stop() {
2322 let mut vt = VirtualTerminal::new(20, 3);
2323 vt.feed(b"\x1b[1;6H\x1bH");
2325 assert!(vt.tab_stops[5]);
2326 vt.feed(b"\x1b[1;1H\t");
2328 assert_eq!(vt.cursor(), (5, 0));
2329 }
2330
2331 #[test]
2332 fn tbc_clears_single_tab_stop() {
2333 let mut vt = VirtualTerminal::new(20, 3);
2334 vt.feed(b"\x1b[1;9H\x1b[0g");
2336 assert!(!vt.tab_stops[8]);
2337 vt.feed(b"\x1b[1;1H\t");
2339 assert_eq!(vt.cursor(), (16, 0));
2340 }
2341
2342 #[test]
2343 fn tbc_clears_all_tab_stops() {
2344 let mut vt = VirtualTerminal::new(20, 3);
2345 vt.feed(b"\x1b[3g");
2347 vt.feed(b"\x1b[1;1H\t");
2349 assert_eq!(vt.cursor(), (19, 0));
2350 }
2351
2352 #[test]
2353 fn cbt_moves_to_previous_tab_stop() {
2354 let mut vt = VirtualTerminal::new(20, 3);
2355 vt.feed(b"\x1b[1;11H\x1b[Z");
2357 assert_eq!(vt.cursor(), (8, 0));
2358 }
2359
2360 #[test]
2361 fn cbt_at_col_zero() {
2362 let mut vt = VirtualTerminal::new(20, 3);
2363 vt.feed(b"\x1b[Z");
2365 assert_eq!(vt.cursor(), (0, 0));
2366 }
2367
2368 #[test]
2369 fn reset_restores_default_tab_stops() {
2370 let mut vt = VirtualTerminal::new(20, 3);
2371 vt.feed(b"\x1b[3g");
2373 assert!(!vt.tab_stops[8]);
2374 vt.feed(b"\x1bc"); assert!(vt.tab_stops[8]);
2376 }
2377
2378 #[test]
2381 fn irm_insert_mode_shifts_right() {
2382 let mut vt = VirtualTerminal::new(10, 3);
2383 vt.feed(b"ABCDE");
2384 vt.feed(b"\x1b[4h\x1b[1;3HXY");
2386 assert_eq!(vt.row_text(0), "ABXYCDE");
2388 }
2389
2390 #[test]
2391 fn irm_replace_mode_default() {
2392 let mut vt = VirtualTerminal::new(10, 3);
2393 vt.feed(b"ABCDE");
2394 vt.feed(b"\x1b[1;3HXY");
2396 assert_eq!(vt.row_text(0), "ABXYE");
2397 }
2398
2399 #[test]
2400 fn irm_disable_returns_to_replace() {
2401 let mut vt = VirtualTerminal::new(10, 3);
2402 vt.feed(b"ABCDE");
2403 vt.feed(b"\x1b[4h\x1b[4l\x1b[1;3HXY");
2405 assert_eq!(vt.row_text(0), "ABXYE");
2407 }
2408
2409 #[test]
2410 fn irm_insert_pushes_off_right_edge() {
2411 let mut vt = VirtualTerminal::new(5, 3);
2412 vt.feed(b"ABCDE");
2413 vt.feed(b"\x1b[4h\x1b[1;1HX");
2415 assert_eq!(vt.row_text(0), "XABCD");
2417 }
2418
2419 #[test]
2422 fn decawm_enabled_wraps_at_edge() {
2423 let mut vt = VirtualTerminal::new(5, 3);
2424 vt.feed(b"ABCDEF");
2426 assert_eq!(vt.row_text(0), "ABCDE");
2427 assert_eq!(vt.row_text(1), "F");
2428 }
2429
2430 #[test]
2431 fn decawm_disabled_no_wrap() {
2432 let mut vt = VirtualTerminal::new(5, 3);
2433 vt.feed(b"\x1b[?7l");
2435 vt.feed(b"ABCDEFGH");
2436 assert_eq!(vt.row_text(0), "ABCDH");
2438 assert_eq!(vt.row_text(1), "");
2439 assert_eq!(vt.cursor(), (4, 0));
2440 }
2441
2442 #[test]
2443 fn decawm_reenable_wraps_again() {
2444 let mut vt = VirtualTerminal::new(5, 3);
2445 vt.feed(b"\x1b[?7l\x1b[?7h");
2447 vt.feed(b"ABCDEF");
2448 assert_eq!(vt.row_text(0), "ABCDE");
2449 assert_eq!(vt.row_text(1), "F");
2450 }
2451
2452 #[test]
2455 fn dec_graphics_g0_designation() {
2456 let mut vt = VirtualTerminal::new(10, 3);
2457 vt.feed(b"\x1b(0qqxx\x1b(B");
2460 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}'); }
2465
2466 #[test]
2467 fn dec_graphics_g1_with_so_si() {
2468 let mut vt = VirtualTerminal::new(10, 3);
2469 vt.feed(b"\x1b)0\x0el\x0fl");
2473 assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'l');
2475 }
2476
2477 #[test]
2478 fn dec_graphics_box_chars() {
2479 let mut vt = VirtualTerminal::new(10, 3);
2480 vt.feed(b"\x1b(0lkjmn\x1b(B");
2482 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}'); }
2488
2489 #[test]
2490 fn charset_reset_restores_ascii() {
2491 let mut vt = VirtualTerminal::new(10, 3);
2492 vt.feed(b"\x1b(0");
2494 vt.feed(b"\x1bc"); vt.feed(b"q");
2496 assert_eq!(vt.char_at(0, 0).unwrap(), 'q'); }
2498
2499 #[test]
2500 fn charset_soft_reset_restores_ascii() {
2501 let mut vt = VirtualTerminal::new(10, 3);
2502 vt.feed(b"\x1b(0");
2504 vt.feed(b"\x1b[!p"); vt.feed(b"q");
2506 assert_eq!(vt.char_at(0, 0).unwrap(), 'q'); }
2508
2509 #[test]
2510 fn so_si_toggle_charset() {
2511 let mut vt = VirtualTerminal::new(10, 3);
2512 vt.feed(b"\x1b)0");
2514 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');
2520 assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(2, 0).unwrap(), 'B');
2522 }
2523
2524 #[test]
2525 fn ascii_passthrough_in_dec_graphics() {
2526 let mut vt = VirtualTerminal::new(10, 3);
2527 vt.feed(b"\x1b(0ABC\x1b(B");
2529 assert_eq!(vt.char_at(0, 0).unwrap(), 'A');
2530 assert_eq!(vt.char_at(1, 0).unwrap(), 'B');
2531 assert_eq!(vt.char_at(2, 0).unwrap(), 'C');
2532 }
2533
2534 #[test]
2537 fn ich_basic_insert() {
2538 let mut vt = VirtualTerminal::new(10, 3);
2539 vt.feed(b"ABCDE");
2540 vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "AB CDE");
2543 assert_eq!(vt.cursor(), (2, 0));
2544 assert_invariants(&vt);
2545 }
2546
2547 #[test]
2548 fn ich_pushes_off_right_edge() {
2549 let mut vt = VirtualTerminal::new(5, 3);
2550 vt.feed(b"ABCDE");
2551 vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "A BC");
2554 assert_invariants(&vt);
2555 }
2556
2557 #[test]
2558 fn ich_at_wide_char_continuation() {
2559 let mut vt = VirtualTerminal::new(10, 3);
2560 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");
2565 assert_invariants(&vt);
2566 }
2567
2568 #[test]
2571 fn dch_basic_delete() {
2572 let mut vt = VirtualTerminal::new(10, 3);
2573 vt.feed(b"ABCDE");
2574 vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[2P"); assert_eq!(vt.row_text(0), "ADE");
2577 assert_eq!(vt.cursor(), (1, 0));
2578 assert_invariants(&vt);
2579 }
2580
2581 #[test]
2582 fn dch_fills_blanks_at_end() {
2583 let mut vt = VirtualTerminal::new(5, 3);
2584 vt.feed(b"ABCDE");
2585 vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b[3P"); assert_eq!(vt.row_text(0), "DE");
2588 assert_invariants(&vt);
2589 }
2590
2591 #[test]
2592 fn dch_at_wide_char_boundary() {
2593 let mut vt = VirtualTerminal::new(10, 3);
2594 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");
2599 assert_invariants(&vt);
2600 }
2601
2602 #[test]
2605 fn ech_basic_erase() {
2606 let mut vt = VirtualTerminal::new(10, 3);
2607 vt.feed(b"ABCDE");
2608 vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[3X"); assert_eq!(vt.row_text(0), "A E");
2611 assert_eq!(vt.cursor(), (1, 0)); assert_invariants(&vt);
2613 }
2614
2615 #[test]
2616 fn ech_does_not_move_cursor() {
2617 let mut vt = VirtualTerminal::new(10, 3);
2618 vt.feed(b"ABCDE");
2619 vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[1X");
2621 assert_eq!(vt.cursor(), (2, 0));
2622 assert_eq!(vt.char_at(2, 0), Some(' '));
2623 assert_eq!(vt.char_at(3, 0), Some('D'));
2624 assert_invariants(&vt);
2625 }
2626
2627 #[test]
2628 fn ech_at_wide_char_continuation() {
2629 let mut vt = VirtualTerminal::new(10, 3);
2630 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");
2635 assert_invariants(&vt);
2636 }
2637
2638 #[test]
2639 fn ech_clamped_to_line_end() {
2640 let mut vt = VirtualTerminal::new(5, 3);
2641 vt.feed(b"ABCDE");
2642 vt.feed(b"\x1b[1;4H"); vt.feed(b"\x1b[99X"); assert_eq!(vt.row_text(0), "ABC");
2645 assert_invariants(&vt);
2646 }
2647
2648 #[test]
2651 fn il_basic_insert_line() {
2652 let mut vt = VirtualTerminal::new(5, 5);
2653 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2654 vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA");
2657 assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB");
2659 assert_eq!(vt.row_text(3), "CCCCC");
2660 assert_eq!(vt.row_text(4), "DDDDD");
2661 }
2663
2664 #[test]
2665 fn il_within_scroll_region() {
2666 let mut vt = VirtualTerminal::new(5, 5);
2667 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2668 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"); }
2677
2678 #[test]
2679 fn il_outside_scroll_region_ignored() {
2680 let mut vt = VirtualTerminal::new(5, 5);
2681 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2682 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA");
2686 assert_eq!(vt.row_text(1), "BBBBB");
2687 assert_invariants(&vt);
2688 }
2689
2690 #[test]
2693 fn dl_basic_delete_line() {
2694 let mut vt = VirtualTerminal::new(5, 5);
2695 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2696 vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(0), "AAAAA");
2699 assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD");
2701 assert_eq!(vt.row_text(3), "EEEEE");
2702 assert_eq!(vt.row_text(4), ""); }
2704
2705 #[test]
2706 fn dl_within_scroll_region() {
2707 let mut vt = VirtualTerminal::new(5, 5);
2708 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2709 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"); }
2718
2719 #[test]
2720 fn dl_outside_scroll_region_ignored() {
2721 let mut vt = VirtualTerminal::new(5, 5);
2722 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2723 vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[5;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(4), "EEEEE");
2727 }
2728
2729 #[test]
2732 fn su_scroll_up_within_region() {
2733 let mut vt = VirtualTerminal::new(5, 5);
2734 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2735 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);
2743 assert_eq!(vt.scrollback_line(0), Some("BBBBB".to_string()));
2744 }
2745
2746 #[test]
2747 fn sd_scroll_down_within_region() {
2748 let mut vt = VirtualTerminal::new(5, 5);
2749 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
2750 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"); }
2758
2759 #[test]
2760 fn su_multiple_lines() {
2761 let mut vt = VirtualTerminal::new(5, 3);
2762 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC");
2763 vt.feed(b"\x1b[2S"); assert_eq!(vt.row_text(0), "CCCCC");
2765 assert_eq!(vt.row_text(1), "");
2766 assert_eq!(vt.row_text(2), "");
2767 assert_eq!(vt.scrollback_len(), 2);
2768 assert_eq!(vt.scrollback_line(0), Some("AAAAA".to_string()));
2769 assert_eq!(vt.scrollback_line(1), Some("BBBBB".to_string()));
2770 }
2771
2772 #[test]
2775 fn rep_basic_repeat() {
2776 let mut vt = VirtualTerminal::new(10, 3);
2777 vt.feed(b"X\x1b[3b"); assert_eq!(vt.row_text(0), "XXXX");
2779 assert_eq!(vt.cursor(), (4, 0));
2780 }
2781
2782 #[test]
2783 fn rep_no_previous_char() {
2784 let mut vt = VirtualTerminal::new(10, 3);
2785 vt.feed(b"\x1b[5b"); assert_eq!(vt.row_text(0), "");
2787 assert_eq!(vt.cursor(), (0, 0));
2788 }
2789
2790 #[test]
2791 fn rep_wraps_across_lines() {
2792 let mut vt = VirtualTerminal::new(5, 3);
2793 vt.feed(b"A\x1b[6b"); assert_eq!(vt.row_text(0), "AAAAA");
2795 assert_eq!(vt.row_text(1), "AA");
2796 assert_eq!(vt.cursor(), (2, 1));
2797 }
2798
2799 #[test]
2802 fn decom_cup_relative_to_scroll_region() {
2803 let mut vt = VirtualTerminal::new(10, 10);
2804 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[1;1H");
2808 assert_eq!(vt.cursor(), (0, 2));
2809 vt.feed(b"\x1b[3;5H");
2811 assert_eq!(vt.cursor(), (4, 4));
2812 }
2813
2814 #[test]
2815 fn decom_clamps_to_scroll_region() {
2816 let mut vt = VirtualTerminal::new(10, 10);
2817 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[99;1H");
2821 assert_eq!(vt.cursor(), (0, 6)); }
2823
2824 #[test]
2825 fn decom_disable_homes_to_origin() {
2826 let mut vt = VirtualTerminal::new(10, 10);
2827 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));
2832 }
2833
2834 #[test]
2835 fn decom_vpa_relative_to_scroll_region() {
2836 let mut vt = VirtualTerminal::new(10, 10);
2837 vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[2d"); assert_eq!(vt.cursor().1, 3);
2841 }
2842
2843 #[test]
2844 fn decom_decstbm_homes_cursor() {
2845 let mut vt = VirtualTerminal::new(10, 10);
2846 vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[5;5H"); vt.feed(b"\x1b[3;7r"); assert_eq!(vt.cursor(), (0, 2)); }
2851
2852 #[test]
2855 fn ss2_translates_one_char_from_g2() {
2856 let mut vt = VirtualTerminal::new(10, 3);
2857 vt.feed(b"\x1b*0");
2859 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');
2864 }
2865
2866 #[test]
2867 fn ss3_translates_one_char_from_g3() {
2868 let mut vt = VirtualTerminal::new(10, 3);
2869 vt.feed(b"\x1b+0");
2871 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');
2876 }
2877
2878 #[test]
2879 fn ss2_only_affects_one_character() {
2880 let mut vt = VirtualTerminal::new(10, 3);
2881 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'); }
2886
2887 #[test]
2890 fn ls2_invokes_g2_into_gl() {
2891 let mut vt = VirtualTerminal::new(10, 3);
2892 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}'); }
2898
2899 #[test]
2900 fn ls3_invokes_g3_into_gl() {
2901 let mut vt = VirtualTerminal::new(10, 3);
2902 vt.feed(b"\x1b+0"); vt.feed(b"\x1bo"); vt.feed(b"n"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{253C}'); }
2907
2908 #[test]
2909 fn ls2_persists_across_characters() {
2910 let mut vt = VirtualTerminal::new(10, 3);
2911 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}'); }
2919
2920 #[test]
2923 fn alt_screen_1047_no_cursor_save() {
2924 let mut vt = VirtualTerminal::new(10, 3);
2925 vt.feed(b"Main");
2926 vt.feed(b"\x1b[1;5H"); let (_cx, _cy) = vt.cursor();
2928 vt.feed(b"\x1b[?1047h"); assert!(vt.is_alternate_screen());
2930 assert_eq!(vt.row_text(0), ""); vt.feed(b"Alt");
2933 vt.feed(b"\x1b[?1047l"); assert!(!vt.is_alternate_screen());
2935 assert_eq!(vt.row_text(0), "Main"); let (_, _) = vt.cursor();
2939 }
2940
2941 #[test]
2942 fn alt_screen_1047_double_enter_ignored() {
2943 let mut vt = VirtualTerminal::new(10, 3);
2944 vt.feed(b"Main");
2945 vt.feed(b"\x1b[?1047h"); vt.feed(b"\x1b[?1047h"); assert!(vt.is_alternate_screen());
2948 assert_eq!(vt.row_text(0), ""); }
2950
2951 #[test]
2954 fn decstbm_invalid_range_ignored() {
2955 let mut vt = VirtualTerminal::new(10, 5);
2956 vt.feed(b"\x1b[4;2r");
2958 assert_eq!(vt.scroll_top, 0);
2959 assert_eq!(vt.scroll_bottom, 4);
2960 }
2961
2962 #[test]
2963 fn decstbm_bottom_at_screen_edge() {
2964 let mut vt = VirtualTerminal::new(10, 5);
2965 vt.feed(b"\x1b[2;5r"); assert_eq!(vt.scroll_top, 1);
2967 assert_eq!(vt.scroll_bottom, 4);
2968 }
2969
2970 #[test]
2971 fn decstbm_homes_cursor_without_decom() {
2972 let mut vt = VirtualTerminal::new(10, 5);
2973 vt.feed(b"\x1b[3;3H"); vt.feed(b"\x1b[2;4r"); assert_eq!(vt.cursor(), (0, 0));
2976 }
2977
2978 #[test]
2981 fn erase_display_mode1_from_start_to_cursor() {
2982 let mut vt = VirtualTerminal::new(10, 3);
2983 vt.feed(b"AAAAAAAAAA");
2984 vt.feed(b"BBBBBBBBBB");
2985 vt.feed(b"CCCCCCCCCC");
2986 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"); }
2992
2993 #[test]
2994 fn erase_display_mode3_clears_scrollback() {
2995 let mut vt = VirtualTerminal::new(5, 2);
2996 vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC"); assert!(vt.scrollback_len() > 0);
2998 vt.feed(b"\x1b[3J"); assert_eq!(vt.scrollback_len(), 0);
3000 }
3001
3002 #[test]
3005 fn erase_line_mode1() {
3006 let mut vt = VirtualTerminal::new(10, 3);
3007 vt.feed(b"ABCDEFGHIJ");
3008 vt.feed(b"\x1b[1;6H"); vt.feed(b"\x1b[1K"); assert_eq!(vt.row_text(0), " GHIJ");
3011 }
3012
3013 #[test]
3016 fn soft_reset_restores_defaults() {
3017 let mut vt = VirtualTerminal::new(10, 5);
3018 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");
3026 assert!(vt.cursor_visible());
3027 assert_eq!(vt.scroll_top, 0);
3028 assert_eq!(vt.scroll_bottom, 4);
3029 vt.feed(b"\x1b[1;1H");
3031 vt.feed(b"ABCDEFGHIJK");
3032 assert_eq!(vt.row_text(0), "ABCDEFGHIJ");
3033 assert_eq!(vt.row_text(1), "K"); }
3035
3036 #[test]
3039 fn erase_line_splits_wide_char_at_boundary() {
3040 let mut vt = VirtualTerminal::new(10, 3);
3041 vt.feed("AB中DE".as_bytes()); vt.feed(b"\x1b[1;4H"); vt.feed(b"\x1b[K"); assert_invariants(&vt);
3045 assert_eq!(vt.char_at(2, 0), Some(' '));
3047 }
3048
3049 #[test]
3050 fn dch_wide_char_continuation_at_boundary() {
3051 let mut vt = VirtualTerminal::new(10, 3);
3052 vt.feed("AB中DE".as_bytes());
3053 vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2P"); assert_eq!(vt.row_text(0), "ABDE");
3056 assert_invariants(&vt);
3057 }
3058
3059 #[test]
3062 fn invariants_after_insert_delete_scroll_sequence() {
3063 let mut vt = VirtualTerminal::new(8, 4);
3064 vt.feed(b"AABBCCDD");
3066 vt.feed(b"EEFFGGHH");
3067 vt.feed(b"IIJJKKLL");
3068 vt.feed(b"MMNNOOPP");
3069 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);
3078 }
3079
3080 #[test]
3081 fn invariants_after_wide_char_operations() {
3082 let mut vt = VirtualTerminal::new(6, 3);
3083 vt.feed("中文字".as_bytes()); vt.feed(b"\x1b[1;1H\x1b[2@"); assert_invariants(&vt);
3087
3088 let mut vt2 = VirtualTerminal::new(6, 3);
3089 vt2.feed("中文字".as_bytes());
3090 vt2.feed(b"\x1b[1;1H\x1b[2P"); assert_invariants(&vt2);
3092
3093 let mut vt3 = VirtualTerminal::new(6, 3);
3094 vt3.feed("中文字".as_bytes());
3095 vt3.feed(b"\x1b[1;3H\x1b[2X"); assert_invariants(&vt3);
3097 }
3098
3099 #[test]
3102 fn put_char_basic_ascii() {
3103 let mut vt = VirtualTerminal::new(80, 24);
3104 vt.put_char('H');
3105 vt.put_char('e');
3106 vt.put_char('l');
3107 vt.put_char('l');
3108 vt.put_char('o');
3109 assert_eq!(vt.row_text(0), "Hello");
3110 assert_eq!(vt.cursor(), (5, 0));
3111 assert_invariants(&vt);
3112 }
3113
3114 #[test]
3115 fn put_char_matches_feed_str() {
3116 let mut vt_feed = VirtualTerminal::new(40, 10);
3117 vt_feed.feed_str("Hello!");
3118
3119 let mut vt_put = VirtualTerminal::new(40, 10);
3120 for ch in "Hello!".chars() {
3121 vt_put.put_char(ch);
3122 }
3123
3124 assert_eq!(vt_feed.screen_text(), vt_put.screen_text());
3125 assert_eq!(vt_feed.cursor(), vt_put.cursor());
3126 }
3127
3128 #[test]
3129 fn put_char_wide_characters() {
3130 let mut vt = VirtualTerminal::new(10, 3);
3131 vt.put_char('中');
3132 vt.put_char('文');
3133 assert_eq!(vt.row_text(0), "中文");
3134 assert_eq!(vt.cursor(), (4, 0)); assert_invariants(&vt);
3136 }
3137
3138 #[test]
3139 fn put_char_autowrap() {
3140 let mut vt = VirtualTerminal::new(5, 3);
3141 for ch in "ABCDE".chars() {
3142 vt.put_char(ch);
3143 }
3144 assert_eq!(vt.row_text(0), "ABCDE");
3146 vt.put_char('F');
3148 assert_eq!(vt.row_text(1), "F");
3149 assert_eq!(vt.cursor(), (1, 1));
3150 assert_invariants(&vt);
3151 }
3152
3153 #[test]
3154 fn put_char_wide_wrap_at_margin() {
3155 let mut vt = VirtualTerminal::new(5, 3);
3158 for ch in "ABCD".chars() {
3159 vt.put_char(ch);
3160 }
3161 vt.put_char('中'); assert_eq!(vt.row_text(0), "ABCD");
3163 assert_eq!(vt.row_text(1), "中");
3164 assert_invariants(&vt);
3165 }
3166
3167 #[test]
3168 fn put_char_zero_width_skipped() {
3169 let mut vt = VirtualTerminal::new(10, 3);
3170 vt.put_char('A');
3171 vt.put_char('\u{0300}'); assert_eq!(vt.cursor(), (1, 0)); assert_eq!(vt.char_at(0, 0), Some('A'));
3174 assert_invariants(&vt);
3175 }
3176
3177 #[test]
3178 fn put_char_preserves_style() {
3179 let mut vt = VirtualTerminal::new(10, 3);
3180 vt.feed(b"\x1b[1m");
3182 vt.put_char('X');
3183 let style = vt.style_at(0, 0).unwrap();
3184 assert!(style.bold);
3185 assert_invariants(&vt);
3186 }
3187
3188 #[test]
3191 fn put_str_basic() {
3192 let mut vt = VirtualTerminal::new(20, 3);
3193 vt.put_str("Hello, world!");
3194 assert_eq!(vt.row_text(0), "Hello, world!");
3195 assert_eq!(vt.cursor(), (13, 0));
3196 assert_invariants(&vt);
3197 }
3198
3199 #[test]
3200 fn put_str_empty() {
3201 let mut vt = VirtualTerminal::new(10, 1);
3202 vt.put_str("");
3203 assert_eq!(vt.cursor(), (0, 0));
3204 assert_eq!(vt.row_text(0), "");
3205 assert_invariants(&vt);
3206 }
3207
3208 #[test]
3209 fn put_str_wraps_at_margin() {
3210 let mut vt = VirtualTerminal::new(5, 3);
3211 vt.put_str("ABCDEFGH");
3212 assert_eq!(vt.row_text(0), "ABCDE");
3213 assert_eq!(vt.row_text(1), "FGH");
3214 assert_eq!(vt.cursor(), (3, 1));
3215 assert_invariants(&vt);
3216 }
3217
3218 #[test]
3219 fn put_str_wide_chars() {
3220 let mut vt = VirtualTerminal::new(10, 1);
3221 vt.put_str("中文");
3222 assert_eq!(vt.row_text(0), "中文");
3223 assert_eq!(vt.cursor(), (4, 0));
3224 assert_invariants(&vt);
3225 }
3226
3227 #[test]
3228 fn put_str_matches_put_char_sequence() {
3229 let text = "Hi 😀!";
3230 let mut vt_str = VirtualTerminal::new(20, 3);
3231 vt_str.put_str(text);
3232
3233 let mut vt_char = VirtualTerminal::new(20, 3);
3234 for ch in text.chars() {
3235 vt_char.put_char(ch);
3236 }
3237
3238 assert_eq!(vt_str.screen_text(), vt_char.screen_text());
3239 assert_eq!(vt_str.cursor(), vt_char.cursor());
3240 }
3241
3242 #[test]
3243 fn put_str_does_not_interpret_escapes() {
3244 let mut vt = VirtualTerminal::new(20, 1);
3245 vt.put_str("\x1b[1mBold");
3247 let text = vt.row_text(0);
3250 assert!(text.contains("[1mBold"), "got: {text:?}");
3251 let style = vt.style_at(0, 0).unwrap();
3253 assert!(!style.bold);
3254 assert_invariants(&vt);
3255 }
3256
3257 #[test]
3260 fn set_cursor_position_basic() {
3261 let mut vt = VirtualTerminal::new(80, 24);
3262 vt.set_cursor_position(10, 5);
3263 assert_eq!(vt.cursor(), (10, 5));
3264 assert_invariants(&vt);
3265 }
3266
3267 #[test]
3268 fn set_cursor_position_clamps_x() {
3269 let mut vt = VirtualTerminal::new(80, 24);
3270 vt.set_cursor_position(200, 5);
3271 assert_eq!(vt.cursor(), (79, 5));
3272 assert_invariants(&vt);
3273 }
3274
3275 #[test]
3276 fn set_cursor_position_clamps_y() {
3277 let mut vt = VirtualTerminal::new(80, 24);
3278 vt.set_cursor_position(10, 100);
3279 assert_eq!(vt.cursor(), (10, 23));
3280 assert_invariants(&vt);
3281 }
3282
3283 #[test]
3284 fn set_cursor_position_origin() {
3285 let mut vt = VirtualTerminal::new(80, 24);
3286 vt.put_str("test");
3287 vt.set_cursor_position(0, 0);
3288 assert_eq!(vt.cursor(), (0, 0));
3289 assert_invariants(&vt);
3290 }
3291
3292 #[test]
3293 fn set_cursor_position_then_put_char() {
3294 let mut vt = VirtualTerminal::new(10, 3);
3295 vt.set_cursor_position(3, 1);
3296 vt.put_char('X');
3297 assert_eq!(vt.char_at(3, 1), Some('X'));
3298 assert_eq!(vt.cursor(), (4, 1));
3299 assert_invariants(&vt);
3300 }
3301
3302 #[test]
3305 fn clear_empties_display() {
3306 let mut vt = VirtualTerminal::new(10, 3);
3307 vt.put_str("Hello");
3308 vt.set_cursor_position(0, 1);
3309 vt.put_str("World");
3310 vt.clear();
3311 assert_eq!(vt.row_text(0), "");
3312 assert_eq!(vt.row_text(1), "");
3313 assert_invariants(&vt);
3314 }
3315
3316 #[test]
3317 fn clear_preserves_cursor_position() {
3318 let mut vt = VirtualTerminal::new(10, 3);
3319 vt.set_cursor_position(5, 2);
3320 vt.clear();
3321 assert_eq!(vt.cursor(), (5, 2));
3322 assert_invariants(&vt);
3323 }
3324
3325 #[test]
3326 fn clear_does_not_affect_scrollback() {
3327 let mut vt = VirtualTerminal::new(5, 2);
3328 vt.put_str("AAAAABBBBBCCCCC");
3330 let sb_before = vt.scrollback_len();
3331 assert!(sb_before > 0);
3332 vt.clear();
3333 assert_eq!(vt.scrollback_len(), sb_before);
3334 assert_invariants(&vt);
3335 }
3336
3337 #[test]
3340 fn clear_scrollback_empties_history() {
3341 let mut vt = VirtualTerminal::new(5, 2);
3342 vt.put_str("AAAAABBBBBCCCCC");
3343 assert!(vt.scrollback_len() > 0);
3344 vt.clear_scrollback();
3345 assert_eq!(vt.scrollback_len(), 0);
3346 assert_invariants(&vt);
3347 }
3348
3349 #[test]
3350 fn clear_scrollback_preserves_display() {
3351 let mut vt = VirtualTerminal::new(10, 2);
3352 vt.put_str("Hello");
3353 vt.set_cursor_position(0, 1);
3354 vt.put_str("World");
3355 vt.clear_scrollback();
3356 assert_eq!(vt.row_text(0), "Hello");
3357 assert_eq!(vt.row_text(1), "World");
3358 assert_invariants(&vt);
3359 }
3360}