1use smallvec::SmallVec;
14
15pub type CsiParams = SmallVec<[u16; 4]>;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Action {
25 Print(char),
27 Newline,
29 CarriageReturn,
31 Tab,
33 Backspace,
35 Bell,
37 CursorUp(u16),
39 CursorDown(u16),
41 CursorRight(u16),
43 CursorLeft(u16),
45 CursorNextLine(u16),
47 CursorPrevLine(u16),
49 CursorColumn(u16),
51 CursorRow(u16),
53 SetScrollRegion { top: u16, bottom: u16 },
58 ScrollUp(u16),
60 ScrollDown(u16),
62 InsertLines(u16),
64 DeleteLines(u16),
66 InsertChars(u16),
68 DeleteChars(u16),
70 CursorPosition { row: u16, col: u16 },
72 EraseInDisplay(u8),
74 EraseInLine(u8),
76 Sgr(CsiParams),
81 DecSet(CsiParams),
83 DecRst(CsiParams),
85 AnsiSet(CsiParams),
87 AnsiRst(CsiParams),
89 SaveCursor,
91 RestoreCursor,
93 Index,
95 ReverseIndex,
97 NextLine,
99 FullReset,
101 SetTitle(String),
103 HyperlinkStart(String),
105 HyperlinkEnd,
107 SetTabStop,
109 ClearTabStop(u16),
111 BackTab(u16),
113 ApplicationKeypad,
115 NormalKeypad,
117 EraseChars(u16),
119 ScreenAlignment,
121 RepeatChar(u16),
123 SetCursorShape(u8),
129 SoftReset,
135 EraseScrollback,
137 FocusIn,
139 FocusOut,
141 PasteStart,
143 PasteEnd,
145 DeviceAttributes,
147 DeviceAttributesSecondary,
149 DeviceStatusReport,
151 CursorPositionReport,
153 DesignateCharset { slot: u8, charset: u8 },
158 SingleShift2,
160 SingleShift3,
162 MouseEvent {
170 button: u16,
171 col: u16,
172 row: u16,
173 pressed: bool,
174 },
175 Escape(Vec<u8>),
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180enum State {
181 Ground,
182 Esc,
183 EscHash,
185 EscCharset {
188 slot: u8,
189 },
190 Csi,
191 Osc,
192 OscEsc,
193 Utf8 {
196 bytes_remaining: u8,
197 },
198}
199
200const ESC_BUF_RETAIN_CAP_MAX: usize = 16 * 1024;
205
206#[derive(Debug, Clone)]
208pub struct Parser {
209 state: State,
210 buf: Vec<u8>,
211 utf8_buf: [u8; 4],
213 utf8_len: u8,
215}
216
217impl Default for Parser {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl Parser {
224 #[must_use]
226 pub fn new() -> Self {
227 Self {
228 state: State::Ground,
229 buf: Vec::new(),
230 utf8_buf: [0; 4],
231 utf8_len: 0,
232 }
233 }
234
235 #[must_use]
237 pub fn feed(&mut self, bytes: &[u8]) -> Vec<Action> {
238 let mut out = Vec::new();
239 self.feed_into(bytes, &mut out);
240 out
241 }
242
243 pub fn feed_into(&mut self, bytes: &[u8], out: &mut Vec<Action>) {
249 for &b in bytes {
250 if let Some(action) = self.advance(b) {
251 out.push(action);
252 }
253 }
254 }
255
256 pub fn advance(&mut self, b: u8) -> Option<Action> {
260 match self.state {
261 State::Ground => self.advance_ground(b),
262 State::Esc => self.advance_esc(b),
263 State::EscHash => self.advance_esc_hash(b),
264 State::EscCharset { slot } => self.advance_esc_charset(b, slot),
265 State::Csi => self.advance_csi(b),
266 State::Osc => self.advance_osc(b),
267 State::OscEsc => self.advance_osc_esc(b),
268 State::Utf8 { bytes_remaining } => self.advance_utf8(b, bytes_remaining),
269 }
270 }
271
272 fn advance_ground(&mut self, b: u8) -> Option<Action> {
273 match b {
274 b'\n' | 0x0B | 0x0C => Some(Action::Newline), b'\r' => Some(Action::CarriageReturn),
276 b'\t' => Some(Action::Tab),
277 0x08 => Some(Action::Backspace),
278 0x07 => Some(Action::Bell),
279 0x1b => {
280 self.state = State::Esc;
281 self.buf.clear();
282 self.buf.push(0x1b);
283 None
284 }
285 0x20..=0x7E => Some(Action::Print(b as char)),
286 0xC2..=0xDF => {
288 self.utf8_buf[0] = b;
290 self.utf8_len = 1;
291 self.state = State::Utf8 { bytes_remaining: 1 };
292 None
293 }
294 0xE0..=0xEF => {
295 self.utf8_buf[0] = b;
297 self.utf8_len = 1;
298 self.state = State::Utf8 { bytes_remaining: 2 };
299 None
300 }
301 0xF0..=0xF4 => {
302 self.utf8_buf[0] = b;
304 self.utf8_len = 1;
305 self.state = State::Utf8 { bytes_remaining: 3 };
306 None
307 }
308 _ => None, }
311 }
312
313 fn advance_utf8(&mut self, b: u8, bytes_remaining: u8) -> Option<Action> {
315 if (0x80..=0xBF).contains(&b) {
317 let idx = self.utf8_len as usize;
318 if idx < 4 {
319 self.utf8_buf[idx] = b;
320 self.utf8_len += 1;
321 }
322 if bytes_remaining == 1 {
323 self.state = State::Ground;
325 let len = self.utf8_len as usize;
326 let ch = core::str::from_utf8(&self.utf8_buf[..len])
327 .ok()
328 .and_then(|s| s.chars().next());
329 self.utf8_len = 0;
330 ch.map(Action::Print)
331 } else {
332 self.state = State::Utf8 {
333 bytes_remaining: bytes_remaining - 1,
334 };
335 None
336 }
337 } else {
338 self.state = State::Ground;
342 self.utf8_len = 0;
343 self.advance_ground(b)
344 }
345 }
346
347 fn advance_esc(&mut self, b: u8) -> Option<Action> {
348 self.buf.push(b);
349 match b {
350 b'[' => {
351 self.state = State::Csi;
352 None
353 }
354 b']' => {
355 self.state = State::Osc;
356 None
357 }
358 b'7' => {
360 self.state = State::Ground;
361 self.buf.clear();
362 Some(Action::SaveCursor)
363 }
364 b'8' => {
366 self.state = State::Ground;
367 self.buf.clear();
368 Some(Action::RestoreCursor)
369 }
370 b'D' => {
372 self.state = State::Ground;
373 self.buf.clear();
374 Some(Action::Index)
375 }
376 b'M' => {
378 self.state = State::Ground;
379 self.buf.clear();
380 Some(Action::ReverseIndex)
381 }
382 b'E' => {
384 self.state = State::Ground;
385 self.buf.clear();
386 Some(Action::NextLine)
387 }
388 b'c' => {
390 self.state = State::Ground;
391 self.buf.clear();
392 Some(Action::FullReset)
393 }
394 b'H' => {
396 self.state = State::Ground;
397 self.buf.clear();
398 Some(Action::SetTabStop)
399 }
400 b'=' => {
402 self.state = State::Ground;
403 self.buf.clear();
404 Some(Action::ApplicationKeypad)
405 }
406 b'>' => {
408 self.state = State::Ground;
409 self.buf.clear();
410 Some(Action::NormalKeypad)
411 }
412 b'#' => {
414 self.state = State::EscHash;
415 None
416 }
417 b'(' => {
419 self.state = State::EscCharset { slot: 0 };
420 None
421 }
422 b')' => {
423 self.state = State::EscCharset { slot: 1 };
424 None
425 }
426 b'*' => {
427 self.state = State::EscCharset { slot: 2 };
428 None
429 }
430 b'+' => {
431 self.state = State::EscCharset { slot: 3 };
432 None
433 }
434 b'N' => {
436 self.state = State::Ground;
437 self.buf.clear();
438 Some(Action::SingleShift2)
439 }
440 b'O' => {
442 self.state = State::Ground;
443 self.buf.clear();
444 Some(Action::SingleShift3)
445 }
446 _ => {
447 self.state = State::Ground;
448 Some(Action::Escape(self.take_buf()))
449 }
450 }
451 }
452
453 fn advance_esc_hash(&mut self, b: u8) -> Option<Action> {
454 self.buf.push(b);
455 self.state = State::Ground;
456 match b {
457 b'8' => {
459 self.buf.clear();
460 Some(Action::ScreenAlignment)
461 }
462 _ => Some(Action::Escape(self.take_buf())),
463 }
464 }
465
466 fn advance_esc_charset(&mut self, b: u8, slot: u8) -> Option<Action> {
467 self.buf.push(b);
468 self.state = State::Ground;
469 self.buf.clear();
471 Some(Action::DesignateCharset { slot, charset: b })
472 }
473
474 fn advance_csi(&mut self, b: u8) -> Option<Action> {
475 self.buf.push(b);
476 if (0x40..=0x7E).contains(&b) {
478 self.state = State::Ground;
479 let seq = self.take_buf();
480 return Some(Self::decode_csi(&seq).unwrap_or(Action::Escape(seq)));
481 }
482 None
483 }
484
485 fn advance_osc(&mut self, b: u8) -> Option<Action> {
486 self.buf.push(b);
487 match b {
488 0x07 => {
489 self.state = State::Ground;
491 let seq = self.take_buf();
492 Some(Self::decode_osc(&seq).unwrap_or(Action::Escape(seq)))
493 }
494 0x1b => {
495 self.state = State::OscEsc;
497 None
498 }
499 _ => None,
500 }
501 }
502
503 fn advance_osc_esc(&mut self, b: u8) -> Option<Action> {
504 self.buf.push(b);
505 if b == b'\\' {
506 self.state = State::Ground;
508 let seq = self.take_buf();
509 return Some(Self::decode_osc(&seq).unwrap_or(Action::Escape(seq)));
510 }
511 self.state = State::Osc;
513 None
514 }
515
516 fn take_buf(&mut self) -> Vec<u8> {
517 let retained_cap = self.buf.capacity().min(ESC_BUF_RETAIN_CAP_MAX);
521 core::mem::replace(&mut self.buf, Vec::with_capacity(retained_cap))
522 }
523
524 fn decode_csi(seq: &[u8]) -> Option<Action> {
525 if seq.len() < 3 || seq[0] != 0x1b || seq[1] != b'[' {
526 return None;
527 }
528 let final_byte = *seq.last()?;
529 let param_bytes = &seq[2..seq.len().saturating_sub(1)];
530
531 if param_bytes.first() == Some(&b'?') {
533 let params = Self::parse_csi_params(¶m_bytes[1..])?;
534 return match final_byte {
535 b'h' => Some(Action::DecSet(params)),
536 b'l' => Some(Action::DecRst(params)),
537 _ => None,
538 };
539 }
540
541 if param_bytes.first() == Some(&b'>') {
543 return match final_byte {
544 b'c' => Some(Action::DeviceAttributesSecondary),
546 _ => None,
547 };
548 }
549
550 if param_bytes.first() == Some(&b'<') {
553 if final_byte == b'M' || final_byte == b'm' {
554 let params = Self::parse_csi_params(¶m_bytes[1..])?;
555 if params.len() == 3 {
556 let button = params[0];
557 let col = params[1].saturating_sub(1);
559 let row = params[2].saturating_sub(1);
560 let pressed = final_byte == b'M';
561 return Some(Action::MouseEvent {
562 button,
563 col,
564 row,
565 pressed,
566 });
567 }
568 }
569 return None;
570 }
571
572 let intermediate_start = param_bytes
575 .iter()
576 .position(|&b| (0x20..=0x2F).contains(&b))
577 .unwrap_or(param_bytes.len());
578 let (numeric_bytes, intermediates) = param_bytes.split_at(intermediate_start);
579
580 match (intermediates, final_byte) {
582 (b" ", b'q') => {
584 let params = Self::parse_csi_params(numeric_bytes)?;
585 let shape = params.first().copied().unwrap_or(0);
586 return Some(Action::SetCursorShape(shape.min(6) as u8));
587 }
588 (b"!", b'p') => return Some(Action::SoftReset),
590 _ if !intermediates.is_empty() => return None,
592 _ => {}
593 }
594
595 let params = Self::parse_csi_params(numeric_bytes)?;
597
598 match final_byte {
599 b'A' => Some(Action::CursorUp(Self::csi_count_or_one(
600 params.first().copied(),
601 ))),
602 b'B' => Some(Action::CursorDown(Self::csi_count_or_one(
603 params.first().copied(),
604 ))),
605 b'C' => Some(Action::CursorRight(Self::csi_count_or_one(
606 params.first().copied(),
607 ))),
608 b'D' => Some(Action::CursorLeft(Self::csi_count_or_one(
609 params.first().copied(),
610 ))),
611 b'E' => Some(Action::CursorNextLine(Self::csi_count_or_one(
612 params.first().copied(),
613 ))),
614 b'F' => Some(Action::CursorPrevLine(Self::csi_count_or_one(
615 params.first().copied(),
616 ))),
617 b'G' => Some(Action::CursorColumn(
618 Self::csi_count_or_one(params.first().copied()).saturating_sub(1),
619 )),
620 b'H' | b'f' => {
621 let row = params
623 .first()
624 .copied()
625 .unwrap_or(1)
626 .max(1)
627 .saturating_sub(1);
628 let col = params.get(1).copied().unwrap_or(1).max(1).saturating_sub(1);
629 Some(Action::CursorPosition { row, col })
630 }
631 b'J' => {
632 let mode = params.first().copied().unwrap_or(0);
633 match mode {
634 0..=2 => Some(Action::EraseInDisplay(mode as u8)),
635 3 => Some(Action::EraseScrollback),
636 _ => None,
637 }
638 }
639 b'K' => {
640 let mode = params.first().copied().unwrap_or(0);
641 if mode <= 2 {
642 Some(Action::EraseInLine(mode as u8))
643 } else {
644 None
645 }
646 }
647 b'd' => Some(Action::CursorRow(
648 Self::csi_count_or_one(params.first().copied()).saturating_sub(1),
649 )),
650 b'L' => Some(Action::InsertLines(Self::csi_count_or_one(
651 params.first().copied(),
652 ))),
653 b'M' => Some(Action::DeleteLines(Self::csi_count_or_one(
654 params.first().copied(),
655 ))),
656 b'@' => Some(Action::InsertChars(Self::csi_count_or_one(
657 params.first().copied(),
658 ))),
659 b'P' => Some(Action::DeleteChars(Self::csi_count_or_one(
660 params.first().copied(),
661 ))),
662 b'S' => Some(Action::ScrollUp(Self::csi_count_or_one(
663 params.first().copied(),
664 ))),
665 b'T' => Some(Action::ScrollDown(Self::csi_count_or_one(
666 params.first().copied(),
667 ))),
668 b'r' => {
669 let top = params
670 .first()
671 .copied()
672 .unwrap_or(0)
673 .max(1)
674 .saturating_sub(1);
675 let bottom = params.get(1).copied().unwrap_or(0);
676 Some(Action::SetScrollRegion { top, bottom })
677 }
678 b'm' => Some(Action::Sgr(params)),
679 b'g' => {
681 let mode = params.first().copied().unwrap_or(0);
682 Some(Action::ClearTabStop(mode))
683 }
684 b'Z' => Some(Action::BackTab(Self::csi_count_or_one(
686 params.first().copied(),
687 ))),
688 b'X' => Some(Action::EraseChars(Self::csi_count_or_one(
690 params.first().copied(),
691 ))),
692 b'b' => Some(Action::RepeatChar(Self::csi_count_or_one(
694 params.first().copied(),
695 ))),
696 b's' => {
698 if params.is_empty() || (params.len() == 1 && params[0] == 0) {
699 Some(Action::SaveCursor)
700 } else {
701 None
703 }
704 }
705 b'u' => {
707 if params.is_empty() || (params.len() == 1 && params[0] == 0) {
708 Some(Action::RestoreCursor)
709 } else {
710 None
711 }
712 }
713 b'I' => Some(Action::FocusIn),
715 b'O' => Some(Action::FocusOut),
716 b'~' => {
718 let param = params.first().copied().unwrap_or(0);
719 match param {
720 200 => Some(Action::PasteStart),
721 201 => Some(Action::PasteEnd),
722 _ => None,
723 }
724 }
725 b'c' => {
727 let p = params.first().copied().unwrap_or(0);
728 if p == 0 {
729 Some(Action::DeviceAttributes)
730 } else {
731 None
732 }
733 }
734 b'n' => {
736 let p = params.first().copied().unwrap_or(0);
737 match p {
738 5 => Some(Action::DeviceStatusReport),
739 6 => Some(Action::CursorPositionReport),
740 _ => None,
741 }
742 }
743 b'h' => Some(Action::AnsiSet(params)),
745 b'l' => Some(Action::AnsiRst(params)),
747 _ => None,
748 }
749 }
750
751 fn decode_osc(seq: &[u8]) -> Option<Action> {
752 if seq.len() < 4 || seq[0] != 0x1b || seq[1] != b']' {
753 return None;
754 }
755
756 let content = if *seq.last()? == 0x07 {
758 &seq[2..seq.len().saturating_sub(1)]
759 } else if seq.len() >= 4 && seq[seq.len() - 2] == 0x1b && seq[seq.len() - 1] == b'\\' {
760 &seq[2..seq.len().saturating_sub(2)]
761 } else {
762 return None;
763 };
764
765 let first_semi = content.iter().position(|&b| b == b';')?;
766 let cmd = core::str::from_utf8(&content[..first_semi]).ok()?;
767 let cmd: u16 = cmd.parse().ok()?;
768 let rest = &content[first_semi + 1..];
769
770 match cmd {
771 0 | 2 => {
772 let title = String::from_utf8_lossy(rest).to_string();
773 Some(Action::SetTitle(title))
774 }
775 8 => {
776 let second_semi = rest.iter().position(|&b| b == b';')?;
778 let uri = &rest[second_semi + 1..];
779 if uri.is_empty() {
780 Some(Action::HyperlinkEnd)
781 } else {
782 Some(Action::HyperlinkStart(
783 String::from_utf8_lossy(uri).to_string(),
784 ))
785 }
786 }
787 _ => None,
788 }
789 }
790
791 fn parse_csi_params(params: &[u8]) -> Option<CsiParams> {
792 if params.is_empty() {
793 return Some(CsiParams::new());
794 }
795 let s = core::str::from_utf8(params).ok()?;
796 let mut out = CsiParams::new();
797 for part in s.split(';') {
798 if part.is_empty() {
799 out.push(0);
800 continue;
801 }
802 let value = part.parse::<u32>().ok()?;
803 out.push(value.min(u16::MAX as u32) as u16);
804 }
805 Some(out)
806 }
807
808 fn csi_count_or_one(value: Option<u16>) -> u16 {
809 value.unwrap_or(1).max(1)
810 }
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816 use smallvec::smallvec;
817
818 #[test]
821 fn printable_ascii_emits_print() {
822 let mut p = Parser::new();
823 let actions = p.feed(b"hi");
824 assert_eq!(actions, vec![Action::Print('h'), Action::Print('i')]);
825 }
826
827 #[test]
828 fn c0_controls_emit_actions() {
829 let mut p = Parser::new();
830 let actions = p.feed(b"\t\r\n");
831 assert_eq!(
832 actions,
833 vec![Action::Tab, Action::CarriageReturn, Action::Newline]
834 );
835 }
836
837 #[test]
838 fn vt_and_ff_treated_as_newline() {
839 let mut p = Parser::new();
840 assert_eq!(p.feed(b"\x0b"), vec![Action::Newline]);
842 assert_eq!(p.feed(b"\x0c"), vec![Action::Newline]);
843 }
844
845 #[test]
848 fn utf8_two_byte_character() {
849 let mut p = Parser::new();
850 let actions = p.feed("é".as_bytes());
852 assert_eq!(actions, vec![Action::Print('é')]);
853 }
854
855 #[test]
856 fn utf8_three_byte_character() {
857 let mut p = Parser::new();
858 let actions = p.feed("中".as_bytes());
860 assert_eq!(actions, vec![Action::Print('中')]);
861 }
862
863 #[test]
864 fn utf8_four_byte_character() {
865 let mut p = Parser::new();
866 let actions = p.feed("🎉".as_bytes());
868 assert_eq!(actions, vec![Action::Print('🎉')]);
869 }
870
871 #[test]
872 fn utf8_mixed_with_ascii() {
873 let mut p = Parser::new();
874 let actions = p.feed("aé中🎉b".as_bytes());
875 assert_eq!(
876 actions,
877 vec![
878 Action::Print('a'),
879 Action::Print('é'),
880 Action::Print('中'),
881 Action::Print('🎉'),
882 Action::Print('b'),
883 ]
884 );
885 }
886
887 #[test]
888 fn utf8_split_across_feeds() {
889 let mut p = Parser::new();
890 assert_eq!(p.feed(&[0xC3]), Vec::<Action>::new());
892 assert_eq!(p.feed(&[0xA9]), vec![Action::Print('é')]);
893 }
894
895 #[test]
896 fn utf8_split_four_byte_across_feeds() {
897 let mut p = Parser::new();
898 assert!(p.feed(&[0xF0]).is_empty());
900 assert!(p.feed(&[0x9F]).is_empty());
901 assert!(p.feed(&[0x8E]).is_empty());
902 assert_eq!(p.feed(&[0x89]), vec![Action::Print('🎉')]);
903 }
904
905 #[test]
906 fn utf8_invalid_continuation_aborts_and_reprocesses() {
907 let mut p = Parser::new();
908 let actions = p.feed(&[0xC3, b'a']);
910 assert_eq!(actions, vec![Action::Print('a')]);
912 }
913
914 #[test]
915 fn utf8_overlong_leading_bytes_are_ignored() {
916 let mut p = Parser::new();
917 assert!(p.feed(&[0xC0]).is_empty());
919 assert!(p.feed(&[0xC1]).is_empty());
920 }
921
922 #[test]
923 fn utf8_invalid_leading_bytes_above_f4_ignored() {
924 let mut p = Parser::new();
925 assert!(p.feed(&[0xF5]).is_empty());
927 assert!(p.feed(&[0xFF]).is_empty());
928 }
929
930 #[test]
931 fn utf8_interrupted_by_escape() {
932 let mut p = Parser::new();
933 let actions = p.feed(&[0xC3, 0x1b, b'c']);
935 assert_eq!(actions, vec![Action::FullReset]);
938 }
939
940 #[test]
941 fn utf8_japanese_text() {
942 let mut p = Parser::new();
943 let actions = p.feed("こんにちは".as_bytes());
944 assert_eq!(
945 actions,
946 vec![
947 Action::Print('こ'),
948 Action::Print('ん'),
949 Action::Print('に'),
950 Action::Print('ち'),
951 Action::Print('は'),
952 ]
953 );
954 }
955
956 #[test]
959 fn decset_cursor_hide() {
960 let mut p = Parser::new();
961 let actions = p.feed(b"\x1b[?25l");
962 assert_eq!(actions, vec![Action::DecRst(smallvec![25])]);
963 }
964
965 #[test]
966 fn decset_cursor_show() {
967 let mut p = Parser::new();
968 let actions = p.feed(b"\x1b[?25h");
969 assert_eq!(actions, vec![Action::DecSet(smallvec![25])]);
970 }
971
972 #[test]
973 fn decset_multiple_modes() {
974 let mut p = Parser::new();
975 let actions = p.feed(b"\x1b[?1049;2004;1006h");
977 assert_eq!(actions, vec![Action::DecSet(smallvec![1049, 2004, 1006])]);
978 }
979
980 #[test]
981 fn decrst_multiple_modes() {
982 let mut p = Parser::new();
983 let actions = p.feed(b"\x1b[?1049;2004l");
984 assert_eq!(actions, vec![Action::DecRst(smallvec![1049, 2004])]);
985 }
986
987 #[test]
988 fn decset_sync_output() {
989 let mut p = Parser::new();
990 assert_eq!(
991 p.feed(b"\x1b[?2026h"),
992 vec![Action::DecSet(smallvec![2026])]
993 );
994 assert_eq!(
995 p.feed(b"\x1b[?2026l"),
996 vec![Action::DecRst(smallvec![2026])]
997 );
998 }
999
1000 #[test]
1001 fn decset_autowrap() {
1002 let mut p = Parser::new();
1003 assert_eq!(p.feed(b"\x1b[?7h"), vec![Action::DecSet(smallvec![7])]);
1004 assert_eq!(p.feed(b"\x1b[?7l"), vec![Action::DecRst(smallvec![7])]);
1005 }
1006
1007 #[test]
1010 fn ansi_set_insert_mode() {
1011 let mut p = Parser::new();
1012 assert_eq!(p.feed(b"\x1b[4h"), vec![Action::AnsiSet(smallvec![4])]);
1014 assert_eq!(p.feed(b"\x1b[4l"), vec![Action::AnsiRst(smallvec![4])]);
1016 }
1017
1018 #[test]
1019 fn ansi_set_newline_mode() {
1020 let mut p = Parser::new();
1021 assert_eq!(p.feed(b"\x1b[20h"), vec![Action::AnsiSet(smallvec![20])]);
1022 assert_eq!(p.feed(b"\x1b[20l"), vec![Action::AnsiRst(smallvec![20])]);
1023 }
1024
1025 #[test]
1028 fn esc_7_saves_cursor() {
1029 let mut p = Parser::new();
1030 assert_eq!(p.feed(b"\x1b7"), vec![Action::SaveCursor]);
1031 }
1032
1033 #[test]
1034 fn esc_8_restores_cursor() {
1035 let mut p = Parser::new();
1036 assert_eq!(p.feed(b"\x1b8"), vec![Action::RestoreCursor]);
1037 }
1038
1039 #[test]
1040 fn save_restore_roundtrip_sequence() {
1041 let mut p = Parser::new();
1042 let actions = p.feed(b"\x1b7\x1b[5;10H\x1b8");
1043 assert_eq!(
1044 actions,
1045 vec![
1046 Action::SaveCursor,
1047 Action::CursorPosition { row: 4, col: 9 },
1048 Action::RestoreCursor,
1049 ]
1050 );
1051 }
1052
1053 #[test]
1056 fn esc_d_is_index() {
1057 let mut p = Parser::new();
1058 assert_eq!(p.feed(b"\x1bD"), vec![Action::Index]);
1059 }
1060
1061 #[test]
1062 fn esc_m_is_reverse_index() {
1063 let mut p = Parser::new();
1064 assert_eq!(p.feed(b"\x1bM"), vec![Action::ReverseIndex]);
1065 }
1066
1067 #[test]
1068 fn esc_e_is_next_line() {
1069 let mut p = Parser::new();
1070 assert_eq!(p.feed(b"\x1bE"), vec![Action::NextLine]);
1071 }
1072
1073 #[test]
1074 fn esc_c_is_full_reset() {
1075 let mut p = Parser::new();
1076 assert_eq!(p.feed(b"\x1bc"), vec![Action::FullReset]);
1077 }
1078
1079 #[test]
1082 fn csi_sgr_is_decoded() {
1083 let mut p = Parser::new();
1084 assert_eq!(p.feed(b"\x1b[31m"), vec![Action::Sgr(smallvec![31])]);
1085 assert_eq!(p.feed(b"\x1b[m"), vec![Action::Sgr(smallvec![])]);
1086 }
1087
1088 #[test]
1089 fn csi_cup_is_decoded_to_cursor_position() {
1090 let mut p = Parser::new();
1091 let actions = p.feed(b"\x1b[5;10H");
1092 assert_eq!(
1093 actions,
1094 vec![Action::CursorPosition { row: 4, col: 9 }],
1095 "CUP should decode as 0-indexed cursor position"
1096 );
1097
1098 let actions = p.feed(b"\x1b[0;0H");
1099 assert_eq!(
1100 actions,
1101 vec![Action::CursorPosition { row: 0, col: 0 }],
1102 "CUP zero params should default to 1;1"
1103 );
1104 }
1105
1106 #[test]
1107 fn csi_ed_and_el_are_decoded() {
1108 let mut p = Parser::new();
1109 assert_eq!(p.feed(b"\x1b[2J"), vec![Action::EraseInDisplay(2)]);
1110 assert_eq!(p.feed(b"\x1b[K"), vec![Action::EraseInLine(0)]);
1111 }
1112
1113 #[test]
1114 fn csi_cursor_relative_moves_are_decoded() {
1115 let mut p = Parser::new();
1116 assert_eq!(
1117 p.feed(b"\x1b[2A\x1b[B\x1b[3C\x1b[0D"),
1118 vec![
1119 Action::CursorUp(2),
1120 Action::CursorDown(1),
1121 Action::CursorRight(3),
1122 Action::CursorLeft(1),
1123 ]
1124 );
1125 }
1126
1127 #[test]
1128 fn csi_cha_is_decoded_to_absolute_column() {
1129 let mut p = Parser::new();
1130 assert_eq!(p.feed(b"\x1b[5G"), vec![Action::CursorColumn(4)]);
1131 assert_eq!(p.feed(b"\x1b[0G"), vec![Action::CursorColumn(0)]);
1132 }
1133
1134 #[test]
1135 fn csi_cnl_cpl_and_vpa_are_decoded() {
1136 let mut p = Parser::new();
1137 assert_eq!(
1138 p.feed(b"\x1b[2E\x1b[F\x1b[3d\x1b[0d\x1b[d"),
1139 vec![
1140 Action::CursorNextLine(2),
1141 Action::CursorPrevLine(1),
1142 Action::CursorRow(2),
1143 Action::CursorRow(0),
1144 Action::CursorRow(0),
1145 ]
1146 );
1147 }
1148
1149 #[test]
1150 fn csi_scroll_region_and_insert_delete_are_decoded() {
1151 let mut p = Parser::new();
1152 assert_eq!(
1153 p.feed(b"\x1b[2;4r\x1b[r\x1b[2S\x1b[T\x1b[3L\x1b[M\x1b[4@\x1b[P"),
1154 vec![
1155 Action::SetScrollRegion { top: 1, bottom: 4 },
1156 Action::SetScrollRegion { top: 0, bottom: 0 },
1157 Action::ScrollUp(2),
1158 Action::ScrollDown(1),
1159 Action::InsertLines(3),
1160 Action::DeleteLines(1),
1161 Action::InsertChars(4),
1162 Action::DeleteChars(1),
1163 ]
1164 );
1165 }
1166
1167 #[test]
1168 fn osc_sequence_bel_terminated_is_captured() {
1169 let mut p = Parser::new();
1170 assert_eq!(
1171 p.feed(b"\x1b]0;title\x07"),
1172 vec![Action::SetTitle("title".to_string())]
1173 );
1174 assert_eq!(
1175 p.feed(b"\x1b]2;hi\x1b\\"),
1176 vec![Action::SetTitle("hi".to_string())]
1177 );
1178 }
1179
1180 #[test]
1181 fn osc8_hyperlink_is_decoded() {
1182 let mut p = Parser::new();
1183 assert_eq!(
1184 p.feed(b"\x1b]8;;https://example.com\x07"),
1185 vec![Action::HyperlinkStart("https://example.com".to_string())]
1186 );
1187 assert_eq!(p.feed(b"\x1b]8;;\x07"), vec![Action::HyperlinkEnd]);
1188 assert_eq!(
1189 p.feed(b"\x1b]8;;https://a.test\x1b\\"),
1190 vec![Action::HyperlinkStart("https://a.test".to_string())]
1191 );
1192 assert_eq!(p.feed(b"\x1b]8;;\x1b\\"), vec![Action::HyperlinkEnd]);
1193 }
1194
1195 #[test]
1198 fn mixed_utf8_csi_osc_sequence() {
1199 let mut p = Parser::new();
1200 let mut input = Vec::new();
1202 input.extend_from_slice("日本語".as_bytes());
1203 input.extend_from_slice(b"\x1b[31m");
1204 input.extend_from_slice(b"\x1b[5;1H");
1205 let actions = p.feed(&input);
1206 assert_eq!(
1207 actions,
1208 vec![
1209 Action::Print('日'),
1210 Action::Print('本'),
1211 Action::Print('語'),
1212 Action::Sgr(smallvec![31]),
1213 Action::CursorPosition { row: 4, col: 0 },
1214 ]
1215 );
1216 }
1217
1218 #[test]
1219 fn typical_terminal_setup_sequence() {
1220 let mut p = Parser::new();
1221 let actions = p.feed(b"\x1b[?1049h\x1b[?2004h\x1b[?1006h\x1b[?25l");
1223 assert_eq!(
1224 actions,
1225 vec![
1226 Action::DecSet(smallvec![1049]),
1227 Action::DecSet(smallvec![2004]),
1228 Action::DecSet(smallvec![1006]),
1229 Action::DecRst(smallvec![25]),
1230 ]
1231 );
1232 }
1233
1234 #[test]
1235 fn typical_terminal_teardown_sequence() {
1236 let mut p = Parser::new();
1237 let actions = p.feed(b"\x1b[?25h\x1b[?1006l\x1b[?2004l\x1b[?1049l");
1239 assert_eq!(
1240 actions,
1241 vec![
1242 Action::DecSet(smallvec![25]),
1243 Action::DecRst(smallvec![1006]),
1244 Action::DecRst(smallvec![2004]),
1245 Action::DecRst(smallvec![1049]),
1246 ]
1247 );
1248 }
1249
1250 #[test]
1253 fn esc_h_is_set_tab_stop() {
1254 let mut p = Parser::new();
1255 assert_eq!(p.feed(b"\x1bH"), vec![Action::SetTabStop]);
1256 }
1257
1258 #[test]
1259 fn csi_g_is_clear_tab_stop_at_cursor() {
1260 let mut p = Parser::new();
1261 assert_eq!(p.feed(b"\x1b[g"), vec![Action::ClearTabStop(0)]);
1262 assert_eq!(p.feed(b"\x1b[0g"), vec![Action::ClearTabStop(0)]);
1263 }
1264
1265 #[test]
1266 fn csi_3g_is_clear_all_tab_stops() {
1267 let mut p = Parser::new();
1268 assert_eq!(p.feed(b"\x1b[3g"), vec![Action::ClearTabStop(3)]);
1269 }
1270
1271 #[test]
1272 fn csi_z_is_back_tab() {
1273 let mut p = Parser::new();
1274 assert_eq!(p.feed(b"\x1b[Z"), vec![Action::BackTab(1)]);
1275 assert_eq!(p.feed(b"\x1b[3Z"), vec![Action::BackTab(3)]);
1276 }
1277
1278 #[test]
1281 fn esc_eq_is_application_keypad() {
1282 let mut p = Parser::new();
1283 assert_eq!(p.feed(b"\x1b="), vec![Action::ApplicationKeypad]);
1284 }
1285
1286 #[test]
1287 fn esc_gt_is_normal_keypad() {
1288 let mut p = Parser::new();
1289 assert_eq!(p.feed(b"\x1b>"), vec![Action::NormalKeypad]);
1290 }
1291
1292 #[test]
1295 fn csi_x_is_erase_chars() {
1296 let mut p = Parser::new();
1297 assert_eq!(p.feed(b"\x1b[X"), vec![Action::EraseChars(1)]);
1298 assert_eq!(p.feed(b"\x1b[5X"), vec![Action::EraseChars(5)]);
1299 }
1300
1301 #[test]
1304 fn tab_stop_setup_and_clear_sequence() {
1305 let mut p = Parser::new();
1306 let actions = p.feed(b"\x1b[5G\x1bH\x1b[13G\x1bH\x1b[3g");
1308 assert_eq!(
1309 actions,
1310 vec![
1311 Action::CursorColumn(4),
1312 Action::SetTabStop,
1313 Action::CursorColumn(12),
1314 Action::SetTabStop,
1315 Action::ClearTabStop(3),
1316 ]
1317 );
1318 }
1319
1320 #[test]
1321 fn esc_hash_8_is_screen_alignment() {
1322 let mut p = Parser::new();
1323 assert_eq!(p.feed(b"\x1b#8"), vec![Action::ScreenAlignment]);
1324 }
1325
1326 #[test]
1327 fn esc_hash_unknown_is_escape() {
1328 let mut p = Parser::new();
1329 let actions = p.feed(b"\x1b#3");
1330 assert_eq!(actions.len(), 1);
1331 assert!(matches!(actions[0], Action::Escape(_)));
1332 }
1333
1334 #[test]
1335 fn csi_b_is_repeat_char() {
1336 let mut p = Parser::new();
1337 assert_eq!(p.feed(b"\x1b[5b"), vec![Action::RepeatChar(5)]);
1338 }
1339
1340 #[test]
1341 fn csi_b_default_is_one() {
1342 let mut p = Parser::new();
1343 assert_eq!(p.feed(b"\x1b[b"), vec![Action::RepeatChar(1)]);
1344 }
1345
1346 #[test]
1349 fn csi_sp_q_is_set_cursor_shape() {
1350 let mut p = Parser::new();
1351 assert_eq!(p.feed(b"\x1b[2 q"), vec![Action::SetCursorShape(2)]);
1353 }
1354
1355 #[test]
1356 fn csi_sp_q_all_shapes() {
1357 let mut p = Parser::new();
1358 for shape in 0u8..=6 {
1361 let seq = format!("\x1b[{shape} q");
1362 let actions = p.feed(seq.as_bytes());
1363 assert_eq!(
1364 actions,
1365 vec![Action::SetCursorShape(shape)],
1366 "DECSCUSR shape {shape}"
1367 );
1368 }
1369 }
1370
1371 #[test]
1372 fn csi_sp_q_default_is_zero() {
1373 let mut p = Parser::new();
1374 assert_eq!(p.feed(b"\x1b[ q"), vec![Action::SetCursorShape(0)]);
1376 }
1377
1378 #[test]
1379 fn csi_sp_q_clamps_to_six() {
1380 let mut p = Parser::new();
1381 assert_eq!(p.feed(b"\x1b[99 q"), vec![Action::SetCursorShape(6)]);
1383 }
1384
1385 #[test]
1388 fn csi_bang_p_is_soft_reset() {
1389 let mut p = Parser::new();
1390 assert_eq!(p.feed(b"\x1b[!p"), vec![Action::SoftReset]);
1391 }
1392
1393 #[test]
1394 fn vim_cursor_shape_sequence() {
1395 let mut p = Parser::new();
1396 let actions = p.feed(b"\x1b[6 q\x1b[2 q");
1398 assert_eq!(
1399 actions,
1400 vec![
1401 Action::SetCursorShape(6), Action::SetCursorShape(2), ]
1404 );
1405 }
1406
1407 #[test]
1408 fn soft_reset_in_typical_sequence() {
1409 let mut p = Parser::new();
1410 let actions = p.feed(b"\x1b[!p\x1b[?7h\x1b[?25h");
1412 assert_eq!(
1413 actions,
1414 vec![
1415 Action::SoftReset,
1416 Action::DecSet(smallvec![7]), Action::DecSet(smallvec![25]), ]
1419 );
1420 }
1421
1422 #[test]
1425 fn csi_3_j_is_erase_scrollback() {
1426 let mut p = Parser::new();
1427 assert_eq!(p.feed(b"\x1b[3J"), vec![Action::EraseScrollback]);
1428 }
1429
1430 #[test]
1431 fn csi_j_mode_0_1_2_still_work() {
1432 let mut p = Parser::new();
1433 assert_eq!(p.feed(b"\x1b[J"), vec![Action::EraseInDisplay(0)]);
1434 assert_eq!(p.feed(b"\x1b[1J"), vec![Action::EraseInDisplay(1)]);
1435 assert_eq!(p.feed(b"\x1b[2J"), vec![Action::EraseInDisplay(2)]);
1436 }
1437
1438 #[test]
1441 fn csi_s_is_save_cursor() {
1442 let mut p = Parser::new();
1443 assert_eq!(p.feed(b"\x1b[s"), vec![Action::SaveCursor]);
1444 }
1445
1446 #[test]
1447 fn csi_u_is_restore_cursor() {
1448 let mut p = Parser::new();
1449 assert_eq!(p.feed(b"\x1b[u"), vec![Action::RestoreCursor]);
1450 }
1451
1452 #[test]
1453 fn csi_s_with_params_is_none() {
1454 let mut p = Parser::new();
1455 let result = p.feed(b"\x1b[1;2s");
1458 assert_eq!(result, vec![Action::Escape(b"\x1b[1;2s".to_vec())]);
1459 }
1460
1461 #[test]
1464 fn csi_i_is_focus_in() {
1465 let mut p = Parser::new();
1466 assert_eq!(p.feed(b"\x1b[I"), vec![Action::FocusIn]);
1467 }
1468
1469 #[test]
1470 fn csi_o_is_focus_out() {
1471 let mut p = Parser::new();
1472 assert_eq!(p.feed(b"\x1b[O"), vec![Action::FocusOut]);
1473 }
1474
1475 #[test]
1478 fn csi_200_tilde_is_paste_start() {
1479 let mut p = Parser::new();
1480 assert_eq!(p.feed(b"\x1b[200~"), vec![Action::PasteStart]);
1481 }
1482
1483 #[test]
1484 fn csi_201_tilde_is_paste_end() {
1485 let mut p = Parser::new();
1486 assert_eq!(p.feed(b"\x1b[201~"), vec![Action::PasteEnd]);
1487 }
1488
1489 #[test]
1490 fn csi_unknown_tilde_is_none() {
1491 let mut p = Parser::new();
1492 let result = p.feed(b"\x1b[99~");
1495 assert_eq!(result, vec![Action::Escape(b"\x1b[99~".to_vec())]);
1496 }
1497
1498 #[test]
1501 fn csi_c_is_device_attributes() {
1502 let mut p = Parser::new();
1503 assert_eq!(p.feed(b"\x1b[c"), vec![Action::DeviceAttributes]);
1504 }
1505
1506 #[test]
1507 fn csi_0_c_is_device_attributes() {
1508 let mut p = Parser::new();
1509 assert_eq!(p.feed(b"\x1b[0c"), vec![Action::DeviceAttributes]);
1510 }
1511
1512 #[test]
1513 fn csi_1_c_is_not_device_attributes() {
1514 let mut p = Parser::new();
1515 let actions = p.feed(b"\x1b[1c");
1517 assert!(!actions.contains(&Action::DeviceAttributes));
1518 }
1519
1520 #[test]
1523 fn csi_gt_c_is_da2() {
1524 let mut p = Parser::new();
1525 assert_eq!(p.feed(b"\x1b[>c"), vec![Action::DeviceAttributesSecondary]);
1526 }
1527
1528 #[test]
1529 fn csi_gt_0_c_is_da2() {
1530 let mut p = Parser::new();
1531 assert_eq!(p.feed(b"\x1b[>0c"), vec![Action::DeviceAttributesSecondary]);
1532 }
1533
1534 #[test]
1537 fn csi_5n_is_device_status_report() {
1538 let mut p = Parser::new();
1539 assert_eq!(p.feed(b"\x1b[5n"), vec![Action::DeviceStatusReport]);
1540 }
1541
1542 #[test]
1543 fn csi_6n_is_cursor_position_report() {
1544 let mut p = Parser::new();
1545 assert_eq!(p.feed(b"\x1b[6n"), vec![Action::CursorPositionReport]);
1546 }
1547
1548 #[test]
1549 fn csi_0n_is_not_dsr() {
1550 let mut p = Parser::new();
1551 let actions = p.feed(b"\x1b[0n");
1552 assert!(!actions.contains(&Action::DeviceStatusReport));
1553 assert!(!actions.contains(&Action::CursorPositionReport));
1554 }
1555
1556 #[test]
1559 fn esc_paren_b_is_g0_ascii() {
1560 let mut p = Parser::new();
1561 assert_eq!(
1562 p.feed(b"\x1b(B"),
1563 vec![Action::DesignateCharset {
1564 slot: 0,
1565 charset: b'B'
1566 }]
1567 );
1568 }
1569
1570 #[test]
1571 fn esc_paren_0_is_g0_dec_graphics() {
1572 let mut p = Parser::new();
1573 assert_eq!(
1574 p.feed(b"\x1b(0"),
1575 vec![Action::DesignateCharset {
1576 slot: 0,
1577 charset: b'0'
1578 }]
1579 );
1580 }
1581
1582 #[test]
1583 fn esc_rparen_b_is_g1_ascii() {
1584 let mut p = Parser::new();
1585 assert_eq!(
1586 p.feed(b"\x1b)B"),
1587 vec![Action::DesignateCharset {
1588 slot: 1,
1589 charset: b'B'
1590 }]
1591 );
1592 }
1593
1594 #[test]
1595 fn esc_star_0_is_g2_dec_graphics() {
1596 let mut p = Parser::new();
1597 assert_eq!(
1598 p.feed(b"\x1b*0"),
1599 vec![Action::DesignateCharset {
1600 slot: 2,
1601 charset: b'0'
1602 }]
1603 );
1604 }
1605
1606 #[test]
1607 fn esc_plus_b_is_g3_ascii() {
1608 let mut p = Parser::new();
1609 assert_eq!(
1610 p.feed(b"\x1b+B"),
1611 vec![Action::DesignateCharset {
1612 slot: 3,
1613 charset: b'B'
1614 }]
1615 );
1616 }
1617
1618 #[test]
1621 fn esc_n_is_single_shift_2() {
1622 let mut p = Parser::new();
1623 assert_eq!(p.feed(b"\x1bN"), vec![Action::SingleShift2]);
1624 }
1625
1626 #[test]
1627 fn esc_o_is_single_shift_3() {
1628 let mut p = Parser::new();
1629 assert_eq!(p.feed(b"\x1bO"), vec![Action::SingleShift3]);
1630 }
1631
1632 #[test]
1635 fn sgr_mouse_left_press() {
1636 let mut p = Parser::new();
1637 assert_eq!(
1639 p.feed(b"\x1b[<0;10;5M"),
1640 vec![Action::MouseEvent {
1641 button: 0,
1642 col: 9,
1643 row: 4,
1644 pressed: true,
1645 }]
1646 );
1647 }
1648
1649 #[test]
1650 fn sgr_mouse_left_release() {
1651 let mut p = Parser::new();
1652 assert_eq!(
1654 p.feed(b"\x1b[<0;10;5m"),
1655 vec![Action::MouseEvent {
1656 button: 0,
1657 col: 9,
1658 row: 4,
1659 pressed: false,
1660 }]
1661 );
1662 }
1663
1664 #[test]
1665 fn sgr_mouse_right_press() {
1666 let mut p = Parser::new();
1667 assert_eq!(
1669 p.feed(b"\x1b[<2;1;1M"),
1670 vec![Action::MouseEvent {
1671 button: 2,
1672 col: 0,
1673 row: 0,
1674 pressed: true,
1675 }]
1676 );
1677 }
1678
1679 #[test]
1680 fn sgr_mouse_middle_press() {
1681 let mut p = Parser::new();
1682 assert_eq!(
1683 p.feed(b"\x1b[<1;50;25M"),
1684 vec![Action::MouseEvent {
1685 button: 1,
1686 col: 49,
1687 row: 24,
1688 pressed: true,
1689 }]
1690 );
1691 }
1692
1693 #[test]
1694 fn sgr_mouse_scroll_up() {
1695 let mut p = Parser::new();
1696 assert_eq!(
1698 p.feed(b"\x1b[<64;5;3M"),
1699 vec![Action::MouseEvent {
1700 button: 64,
1701 col: 4,
1702 row: 2,
1703 pressed: true,
1704 }]
1705 );
1706 }
1707
1708 #[test]
1709 fn sgr_mouse_scroll_down() {
1710 let mut p = Parser::new();
1711 assert_eq!(
1713 p.feed(b"\x1b[<65;5;3M"),
1714 vec![Action::MouseEvent {
1715 button: 65,
1716 col: 4,
1717 row: 2,
1718 pressed: true,
1719 }]
1720 );
1721 }
1722
1723 #[test]
1724 fn sgr_mouse_ctrl_left_press() {
1725 let mut p = Parser::new();
1726 assert_eq!(
1728 p.feed(b"\x1b[<16;1;1M"),
1729 vec![Action::MouseEvent {
1730 button: 16,
1731 col: 0,
1732 row: 0,
1733 pressed: true,
1734 }]
1735 );
1736 }
1737
1738 #[test]
1739 fn sgr_mouse_motion_while_pressed() {
1740 let mut p = Parser::new();
1741 assert_eq!(
1743 p.feed(b"\x1b[<32;20;10M"),
1744 vec![Action::MouseEvent {
1745 button: 32,
1746 col: 19,
1747 row: 9,
1748 pressed: true,
1749 }]
1750 );
1751 }
1752
1753 #[test]
1754 fn sgr_mouse_incomplete_params_falls_through() {
1755 let mut p = Parser::new();
1756 let actions = p.feed(b"\x1b[<0;10M");
1758 assert!(
1759 !matches!(actions.first(), Some(Action::MouseEvent { .. })),
1760 "incomplete SGR mouse should not produce MouseEvent"
1761 );
1762 }
1763
1764 #[test]
1765 fn sgr_mouse_large_coords() {
1766 let mut p = Parser::new();
1767 assert_eq!(
1769 p.feed(b"\x1b[<0;300;100M"),
1770 vec![Action::MouseEvent {
1771 button: 0,
1772 col: 299,
1773 row: 99,
1774 pressed: true,
1775 }]
1776 );
1777 }
1778
1779 #[test]
1782 fn feed_into_is_equivalent_to_feed() {
1783 let inputs: &[&[u8]] = &[
1784 b"Hello, World!",
1785 b"\x1b[31;1mBold Red\x1b[m",
1786 b"\x1b[?1049h\x1b[?2004h",
1787 b"\x1b[10;20H",
1788 b"\x1b]0;title\x07",
1789 b"\xc3\xa9\xc3\xa0",
1790 ];
1791 for &input in inputs {
1792 let mut p1 = Parser::new();
1793 let mut p2 = Parser::new();
1794 let expected = p1.feed(input);
1795 let mut actual = Vec::new();
1796 p2.feed_into(input, &mut actual);
1797 assert_eq!(expected, actual, "mismatch for input {:?}", input);
1798 }
1799 }
1800
1801 #[test]
1802 fn feed_into_reuses_capacity() {
1803 let mut p = Parser::new();
1804 let mut out = Vec::new();
1805 p.feed_into(b"\x1b[31m", &mut out);
1806 assert!(!out.is_empty());
1807 let cap = out.capacity();
1808 out.clear();
1809 p.feed_into(b"\x1b[32m", &mut out);
1810 assert!(out.capacity() >= cap);
1812 }
1813
1814 #[test]
1815 fn take_buf_caps_retained_internal_capacity() {
1816 let mut p = Parser::new();
1817 p.buf = Vec::with_capacity(ESC_BUF_RETAIN_CAP_MAX * 4);
1818 p.buf.extend_from_slice(b"\x1b[31m");
1819
1820 let taken = p.take_buf();
1821 assert!(!taken.is_empty());
1822 assert!(
1823 p.buf.capacity() <= ESC_BUF_RETAIN_CAP_MAX,
1824 "retained capacity must be capped to avoid oversized churn"
1825 );
1826 }
1827}