From 28f2e6ce66a804e4b8269a02587b2b6500710421 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 21 Jul 2026 15:31:15 -0400 Subject: [PATCH] fix(vterm): parse VT index operations Dispatch ESC D, ESC E, and ESC M as typed full-screen IND, NEL, and RI operations while preserving the line-oriented parser contract. Extend both the every-split parser matrix and parser-to-screen integration test so missing dispatch fails behaviorally rather than only at compile time. --- src/ansi.rs | 18 +++++++++++++----- src/terminal/screen.rs | 3 ++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ansi.rs b/src/ansi.rs index f7a74b7..f57d58c 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -924,11 +924,16 @@ impl AnsiParser { } b'P' => self.state = State::DcsEntry, b'X' | b'^' | b'_' => self.state = State::SosPmApcString, - b'7' | b'8' | b'H' | b'=' | b'>' if self.profile == AnsiParserProfile::FullScreen => { + b'7' | b'8' | b'D' | b'E' | b'H' | b'M' | b'=' | b'>' + if self.profile == AnsiParserProfile::FullScreen => + { let event = match b { b'7' => AnsiEvent::SaveCursor, b'8' => AnsiEvent::RestoreCursor, + b'D' => AnsiEvent::Index, + b'E' => AnsiEvent::NextLine, b'H' => AnsiEvent::SetTabStop, + b'M' => AnsiEvent::ReverseIndex, b'=' => AnsiEvent::SetMode { mode: TerminalMode::ApplicationKeypad, enabled: true, @@ -2315,14 +2320,17 @@ mod tests { #[test] fn full_screen_emits_typed_operation_set_across_every_split() { - let bytes = b"\x07\t\n\x1bH\x1b[2A\x1b[3B\x1b[4C\x1b[5D\x1b[2E\x1b[2F\ - \x1b[7G\x1b[8d\x1b[2;3H\x1b[J\x1b[1K\x1b[2X\x1b[3@\x1b[4P\ - \x1b[2L\x1b[2M\x1b[3S\x1b[2T\x1b[2;20r\x1b[s\x1b[u\x1b[3g\ - \x1b[?1;6;7;25;1000;1002;1003;1004;1006;2004;2026h\ + let bytes = b"\x07\t\n\x1bD\x1bE\x1bM\x1bH\x1b[2A\x1b[3B\x1b[4C\x1b[5D\ + \x1b[2E\x1b[2F\x1b[7G\x1b[8d\x1b[2;3H\x1b[J\x1b[1K\x1b[2X\ + \x1b[3@\x1b[4P\x1b[2L\x1b[2M\x1b[3S\x1b[2T\x1b[2;20r\x1b[s\ + \x1b[u\x1b[3g\x1b[?1;6;7;25;1000;1002;1003;1004;1006;2004;2026h\ \x1b[?47h\x1b[?1047h\x1b[?1049h\x1b[c\x1b[>c\x1b[5n\x1b[6n"; let mut whole = AnsiParser::with_profile(AnsiParserProfile::FullScreen); let expected = whole.feed(bytes); assert!(expected.contains(&AnsiEvent::Bell)); + assert!(expected.contains(&AnsiEvent::Index)); + assert!(expected.contains(&AnsiEvent::NextLine)); + assert!(expected.contains(&AnsiEvent::ReverseIndex)); assert!(expected.contains(&AnsiEvent::CursorPosition { row: 2, col: 3 })); assert!(expected.contains(&AnsiEvent::SetScrollingRegion { top: 2, diff --git a/src/terminal/screen.rs b/src/terminal/screen.rs index cfc7f3b..af9e6cf 100644 --- a/src/terminal/screen.rs +++ b/src/terminal/screen.rs @@ -1587,12 +1587,13 @@ mod tests { #[test] fn parser_split_points_produce_identical_screen() { - let bytes = b"ab\x1b[2;3Hc\x1b[31mD\x1b[2J\x1b[Hdone"; + let bytes = b"ab\x1b[2;3Hc\x1b[31mD\x1b[2J\x1b[Hdone\x1bE\x1bM\x1bD"; let mut whole_parser = AnsiParser::with_profile(AnsiParserProfile::FullScreen); let mut whole = screen(3, 8); for event in whole_parser.feed(bytes) { whole.apply_event(event); } + assert_eq!(whole.snapshot().cursor, Some(CellCoord::new(1, 0))); for split in 0..=bytes.len() { let mut parser = AnsiParser::with_profile(AnsiParserProfile::FullScreen); let mut split_screen = screen(3, 8);