Skip to main content

ftui_core/
input_parser.rs

1#![forbid(unsafe_code)]
2
3//! Input parser state machine.
4//!
5//! Decodes terminal input bytes into [`crate::event::Event`] values with DoS protection.
6//!
7//! # Design
8//!
9//! The parser is a state machine that handles:
10//! - ASCII characters and control codes
11//! - UTF-8 multi-byte sequences
12//! - CSI (Control Sequence Introducer) sequences
13//! - SS3 (Single Shift 3) sequences
14//! - OSC (Operating System Command) sequences
15//! - Bracketed paste mode
16//! - Mouse events (SGR protocol)
17//! - Focus events
18//!
19//! # DoS Protection
20//!
21//! The parser enforces length limits on all sequence types to prevent memory exhaustion:
22//! - CSI sequences: 256 bytes max
23//! - OSC sequences: 4KB max
24//! - Paste content: 1MB max
25
26use crate::event::{
27    ClipboardEvent, ClipboardSource, Event, KeyCode, KeyEvent, KeyEventKind, Modifiers,
28    MouseButton, MouseEvent, MouseEventKind, PasteEvent,
29};
30
31/// DoS protection: maximum CSI sequence length.
32const MAX_CSI_LEN: usize = 256;
33
34/// DoS protection: maximum OSC sequence length.
35const MAX_OSC_LEN: usize = 4096;
36
37/// DoS protection: maximum paste content length.
38const MAX_PASTE_LEN: usize = 1024 * 1024; // 1MB
39
40/// Parser state machine states.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42enum ParserState {
43    /// Normal character input.
44    #[default]
45    Ground,
46    /// After ESC (0x1B).
47    Escape,
48    /// After ESC [ (CSI introducer).
49    Csi,
50    /// Collecting CSI parameters.
51    CsiParam,
52    /// Ignoring oversized CSI sequence.
53    CsiIgnore,
54    /// After ESC O (SS3 introducer).
55    Ss3,
56    /// After ESC ] (OSC introducer).
57    Osc,
58    /// Collecting OSC content.
59    OscContent,
60    /// After ESC inside OSC (for ESC \ terminator).
61    OscEscape,
62    /// Ignoring oversized OSC sequence.
63    OscIgnore,
64    /// Collecting UTF-8 multi-byte sequence.
65    Utf8 {
66        /// Bytes collected so far.
67        collected: u8,
68        /// Total bytes expected.
69        expected: u8,
70    },
71}
72
73/// Terminal input parser with DoS protection.
74///
75/// Parse terminal input bytes into events:
76///
77/// ```ignore
78/// let mut parser = InputParser::new();
79/// let events = parser.parse(b"\x1b[A"); // Up arrow
80/// assert_eq!(events.len(), 1);
81/// ```
82#[derive(Debug)]
83pub struct InputParser {
84    /// Current parser state.
85    state: ParserState,
86    /// Buffer for accumulating sequence bytes.
87    buffer: Vec<u8>,
88    /// Buffer for collecting paste content.
89    paste_buffer: Vec<u8>,
90    /// UTF-8 bytes collected so far.
91    utf8_buffer: [u8; 4],
92    /// Whether we're in bracketed paste mode.
93    in_paste: bool,
94    /// Event queued for the next iteration (allows emitting 2 events per byte).
95    pending_event: Option<Event>,
96}
97
98impl Default for InputParser {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl InputParser {
105    /// Create a new input parser.
106    #[must_use]
107    pub fn new() -> Self {
108        Self {
109            state: ParserState::Ground,
110            buffer: Vec::with_capacity(64),
111            paste_buffer: Vec::new(),
112            utf8_buffer: [0; 4],
113            in_paste: false,
114            pending_event: None,
115        }
116    }
117
118    /// Parse input bytes and return any completed events.
119    pub fn parse(&mut self, input: &[u8]) -> Vec<Event> {
120        let mut events = Vec::new();
121        for &byte in input {
122            if let Some(event) = self.process_byte(byte) {
123                events.push(event);
124            }
125            if let Some(pending) = self.pending_event.take() {
126                events.push(pending);
127            }
128        }
129        events
130    }
131
132    /// Process a single byte and optionally return an event.
133    fn process_byte(&mut self, byte: u8) -> Option<Event> {
134        // In paste mode, collect bytes until end sequence
135        if self.in_paste {
136            return self.process_paste_byte(byte);
137        }
138
139        match self.state {
140            ParserState::Ground => self.process_ground(byte),
141            ParserState::Escape => self.process_escape(byte),
142            ParserState::Csi => self.process_csi(byte),
143            ParserState::CsiParam => self.process_csi_param(byte),
144            ParserState::CsiIgnore => self.process_csi_ignore(byte),
145            ParserState::Ss3 => self.process_ss3(byte),
146            ParserState::Osc => self.process_osc(byte),
147            ParserState::OscContent => self.process_osc_content(byte),
148            ParserState::OscEscape => self.process_osc_escape(byte),
149            ParserState::OscIgnore => self.process_osc_ignore(byte),
150            ParserState::Utf8 {
151                collected,
152                expected,
153            } => self.process_utf8(byte, collected, expected),
154        }
155    }
156
157    /// Process byte in ground state.
158    fn process_ground(&mut self, byte: u8) -> Option<Event> {
159        match byte {
160            // ESC - start escape sequence
161            0x1B => {
162                self.state = ParserState::Escape;
163                None
164            }
165            // NUL - Ctrl+Space or Ctrl+@
166            0x00 => Some(Event::Key(KeyEvent::new(KeyCode::Null))),
167            // Backspace alternate (Ctrl+H)
168            0x08 => Some(Event::Key(KeyEvent::new(KeyCode::Backspace))),
169            // Tab (Ctrl+I) - check before generic Ctrl range
170            0x09 => Some(Event::Key(KeyEvent::new(KeyCode::Tab))),
171            // Enter (Ctrl+M) - check before generic Ctrl range
172            0x0D => Some(Event::Key(KeyEvent::new(KeyCode::Enter))),
173            // Other Ctrl+A through Ctrl+Z (0x01-0x1A excluding Tab and Enter)
174            0x01..=0x07 | 0x0A..=0x0C | 0x0E..=0x1A => {
175                let c = (byte + b'a' - 1) as char;
176                Some(Event::Key(
177                    KeyEvent::new(KeyCode::Char(c)).with_modifiers(Modifiers::CTRL),
178                ))
179            }
180            // Backspace (DEL)
181            0x7F => Some(Event::Key(KeyEvent::new(KeyCode::Backspace))),
182            // Printable ASCII
183            0x20..=0x7E => Some(Event::Key(KeyEvent::new(KeyCode::Char(byte as char)))),
184            // UTF-8 lead bytes (valid ranges only)
185            0xC2..=0xDF => {
186                self.utf8_buffer[0] = byte;
187                self.state = ParserState::Utf8 {
188                    collected: 1,
189                    expected: 2,
190                };
191                None
192            }
193            0xE0..=0xEF => {
194                self.utf8_buffer[0] = byte;
195                self.state = ParserState::Utf8 {
196                    collected: 1,
197                    expected: 3,
198                };
199                None
200            }
201            0xF0..=0xF4 => {
202                self.utf8_buffer[0] = byte;
203                self.state = ParserState::Utf8 {
204                    collected: 1,
205                    expected: 4,
206                };
207                None
208            }
209            // Invalid UTF-8 lead bytes (overlong or out of range)
210            0xC0..=0xC1 | 0xF5..=0xFF => Some(Event::Key(KeyEvent::new(KeyCode::Char(
211                std::char::REPLACEMENT_CHARACTER,
212            )))),
213            // Invalid or ignored bytes
214            _ => None,
215        }
216    }
217
218    /// Process byte after ESC.
219    fn process_escape(&mut self, byte: u8) -> Option<Event> {
220        match byte {
221            // CSI introducer
222            b'[' => {
223                self.state = ParserState::Csi;
224                self.buffer.clear();
225                None
226            }
227            // SS3 introducer
228            b'O' => {
229                self.state = ParserState::Ss3;
230                None
231            }
232            // OSC introducer
233            b']' => {
234                self.state = ParserState::Osc;
235                self.buffer.clear();
236                None
237            }
238            // Another ESC - emit Alt+Escape and reset to ground
239            // (or treat as start of new sequence - but ESC ESC is usually Alt+ESC)
240            0x1B => {
241                self.state = ParserState::Ground;
242                Some(Event::Key(
243                    KeyEvent::new(KeyCode::Escape).with_modifiers(Modifiers::ALT),
244                ))
245            }
246            // Alt+letter or Alt+char
247            0x20..=0x7E => {
248                self.state = ParserState::Ground;
249                Some(Event::Key(
250                    KeyEvent::new(KeyCode::Char(byte as char)).with_modifiers(Modifiers::ALT),
251                ))
252            }
253            // Alt+Backspace (DEL)
254            0x7F => {
255                self.state = ParserState::Ground;
256                Some(Event::Key(
257                    KeyEvent::new(KeyCode::Backspace).with_modifiers(Modifiers::ALT),
258                ))
259            }
260            // Invalid - return to ground
261            _ => {
262                self.state = ParserState::Ground;
263                None
264            }
265        }
266    }
267
268    /// Process byte at start of CSI sequence.
269    fn process_csi(&mut self, byte: u8) -> Option<Event> {
270        // Robustness: ESC restarts sequence
271        if byte == 0x1B {
272            self.state = ParserState::Escape;
273            self.buffer.clear();
274            return None;
275        }
276
277        self.buffer.push(byte);
278
279        match byte {
280            // Parameter bytes (0x30-0x3F) and Intermediate bytes (0x20-0x2F)
281            0x20..=0x3F => {
282                self.state = ParserState::CsiParam;
283                None
284            }
285            // Final byte (0x40-0x7E) - parse and return
286            0x40..=0x7E => {
287                self.state = ParserState::Ground;
288                self.parse_csi_sequence()
289            }
290            // Invalid (0x00-0x1F, 0x7F-0xFF)
291            _ => {
292                self.state = ParserState::Ground;
293                self.buffer.clear();
294                None
295            }
296        }
297    }
298
299    /// Process byte while collecting CSI parameters.
300    fn process_csi_param(&mut self, byte: u8) -> Option<Event> {
301        // Robustness: ESC restarts sequence
302        if byte == 0x1B {
303            self.state = ParserState::Escape;
304            self.buffer.clear();
305            return None;
306        }
307
308        // DoS protection
309        if self.buffer.len() >= MAX_CSI_LEN {
310            self.state = ParserState::CsiIgnore;
311            self.buffer.clear();
312            return None;
313        }
314
315        self.buffer.push(byte);
316
317        match byte {
318            // Continue collecting parameters/intermediates
319            0x20..=0x3F => None,
320            // Final byte - parse and return
321            0x40..=0x7E => {
322                self.state = ParserState::Ground;
323                self.parse_csi_sequence()
324            }
325            // Invalid
326            _ => {
327                self.state = ParserState::Ground;
328                self.buffer.clear();
329                None
330            }
331        }
332    }
333
334    /// Ignore bytes until end of CSI sequence.
335    fn process_csi_ignore(&mut self, byte: u8) -> Option<Event> {
336        // Robustness: ESC restarts sequence
337        if byte == 0x1B {
338            self.state = ParserState::Escape;
339            return None;
340        }
341
342        // Final byte (0x40-0x7E) - return to ground
343        // Intermediate bytes outside this range are ignored
344        if (0x40..=0x7E).contains(&byte) {
345            self.state = ParserState::Ground;
346        } else if !(0x20..=0x7E).contains(&byte) {
347            // Invalid character (e.g. newline) - abort sequence
348            self.state = ParserState::Ground;
349        }
350        None
351    }
352
353    /// Parse a complete CSI sequence from the buffer.
354    fn parse_csi_sequence(&mut self) -> Option<Event> {
355        let seq = std::mem::take(&mut self.buffer);
356        if seq.is_empty() {
357            return None;
358        }
359
360        let final_byte = *seq.last()?;
361        let params = &seq[..seq.len() - 1];
362
363        // Check for special sequences first
364        match (params, final_byte) {
365            // Focus events
366            ([], b'I') => return Some(Event::Focus(true)),
367            ([], b'O') => return Some(Event::Focus(false)),
368
369            // Bracketed paste
370            (b"200", b'~') => {
371                self.in_paste = true;
372                self.paste_buffer.clear();
373                self.buffer.clear(); // Ensure tail buffer is clean
374                return None;
375            }
376            (b"201", b'~') => {
377                self.in_paste = false;
378                let content = String::from_utf8_lossy(&self.paste_buffer).into_owned();
379                self.paste_buffer.clear();
380                return Some(Event::Paste(PasteEvent::bracketed(content)));
381            }
382
383            // SGR mouse protocol
384            _ if params.starts_with(b"<") && (final_byte == b'M' || final_byte == b'm') => {
385                return self.parse_sgr_mouse(params, final_byte);
386            }
387
388            _ => {}
389        }
390
391        // Arrow keys and other CSI sequences
392        match final_byte {
393            b'A' => Some(Event::Key(self.key_with_modifiers(KeyCode::Up, params))),
394            b'B' => Some(Event::Key(self.key_with_modifiers(KeyCode::Down, params))),
395            b'C' => Some(Event::Key(self.key_with_modifiers(KeyCode::Right, params))),
396            b'D' => Some(Event::Key(self.key_with_modifiers(KeyCode::Left, params))),
397            b'H' => Some(Event::Key(self.key_with_modifiers(KeyCode::Home, params))),
398            b'F' => Some(Event::Key(self.key_with_modifiers(KeyCode::End, params))),
399            b'Z' => Some(Event::Key(
400                self.key_with_modifiers(KeyCode::BackTab, params),
401            )),
402            b'~' => self.parse_csi_tilde(params),
403            b'u' => self.parse_kitty_keyboard(params),
404            _ => None,
405        }
406    }
407
408    /// Parse CSI sequences ending in ~.
409    fn parse_csi_tilde(&self, params: &[u8]) -> Option<Event> {
410        let num = self.parse_first_param(params)?;
411        let mods = self.parse_modifier_param(params);
412
413        let code = match num {
414            1 => KeyCode::Home,
415            2 => KeyCode::Insert,
416            3 => KeyCode::Delete,
417            4 => KeyCode::End,
418            5 => KeyCode::PageUp,
419            6 => KeyCode::PageDown,
420            15 => KeyCode::F(5),
421            17 => KeyCode::F(6),
422            18 => KeyCode::F(7),
423            19 => KeyCode::F(8),
424            20 => KeyCode::F(9),
425            21 => KeyCode::F(10),
426            23 => KeyCode::F(11),
427            24 => KeyCode::F(12),
428            _ => return None,
429        };
430
431        Some(Event::Key(KeyEvent::new(code).with_modifiers(mods)))
432    }
433
434    /// Parse the first numeric parameter from CSI params.
435    fn parse_first_param(&self, params: &[u8]) -> Option<u32> {
436        let s = std::str::from_utf8(params).ok()?;
437        let first = s.split(';').next()?;
438        first.parse().ok()
439    }
440
441    /// Parse modifier parameter (second param in CSI sequences).
442    fn parse_modifier_param(&self, params: &[u8]) -> Modifiers {
443        let s = match std::str::from_utf8(params) {
444            Ok(s) => s,
445            Err(_) => return Modifiers::NONE,
446        };
447
448        let modifier_value: u32 = s
449            .split(';')
450            .nth(1)
451            .and_then(|s| s.parse().ok())
452            .unwrap_or(1);
453
454        Self::modifiers_from_xterm(modifier_value)
455    }
456
457    /// Parse Kitty keyboard protocol CSI u sequences.
458    ///
459    /// Format: `CSI unicode-key-code:alt-keys ; modifiers:event-type ; text-as-codepoints u`
460    fn parse_kitty_keyboard(&self, params: &[u8]) -> Option<Event> {
461        let s = std::str::from_utf8(params).ok()?;
462        if s.is_empty() {
463            return None;
464        }
465
466        let mut parts = s.split(';');
467        let key_part = parts.next().unwrap_or("");
468        let key_code_str = key_part.split(':').next().unwrap_or("");
469        let key_code: u32 = key_code_str.parse().ok()?;
470
471        let mod_part = parts.next().unwrap_or("");
472        let (modifiers, kind) = Self::kitty_modifiers_and_kind(mod_part);
473
474        let code = Self::kitty_keycode_to_keycode(key_code)?;
475        Some(Event::Key(
476            KeyEvent::new(code)
477                .with_modifiers(modifiers)
478                .with_kind(kind),
479        ))
480    }
481
482    fn kitty_modifiers_and_kind(mod_part: &str) -> (Modifiers, KeyEventKind) {
483        if mod_part.is_empty() {
484            return (Modifiers::NONE, KeyEventKind::Press);
485        }
486
487        let mut parts = mod_part.split(':');
488        let mod_value: u32 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
489        let kind_value: u32 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
490
491        let modifiers = Self::modifiers_from_xterm(mod_value);
492        let kind = match kind_value {
493            2 => KeyEventKind::Repeat,
494            3 => KeyEventKind::Release,
495            _ => KeyEventKind::Press,
496        };
497
498        (modifiers, kind)
499    }
500
501    fn kitty_keycode_to_keycode(key_code: u32) -> Option<KeyCode> {
502        match key_code {
503            // Standard ASCII keys
504            9 => Some(KeyCode::Tab),
505            13 => Some(KeyCode::Enter),
506            27 => Some(KeyCode::Escape),
507            8 | 127 => Some(KeyCode::Backspace),
508            // Kitty keyboard protocol extended keys (CSI u)
509            57_344 => Some(KeyCode::Escape),
510            57_345 => Some(KeyCode::Enter),
511            57_346 => Some(KeyCode::Tab),
512            57_347 => Some(KeyCode::Backspace),
513            57_348 => Some(KeyCode::Insert),
514            57_349 => Some(KeyCode::Delete),
515            57_350 => Some(KeyCode::Left),
516            57_351 => Some(KeyCode::Right),
517            57_352 => Some(KeyCode::Up),
518            57_353 => Some(KeyCode::Down),
519            57_354 => Some(KeyCode::PageUp),
520            57_355 => Some(KeyCode::PageDown),
521            57_356 => Some(KeyCode::Home),
522            57_357 => Some(KeyCode::End),
523            // F1-F24 (57_364-57_387)
524            57_364..=57_387 => {
525                // Safety: range is [57_364, 57_387], so (key_code - 57_364 + 1) is [1, 24]
526                // which fits in u8. We use debug_assert to catch any future range changes.
527                let f_num = key_code - 57_364 + 1;
528                debug_assert!(f_num <= 24, "F-key number {f_num} exceeds F24");
529                Some(KeyCode::F(f_num as u8))
530            }
531            // Reserved/unhandled Kitty keycodes return None
532            57_358..=57_363 | 57_388..=63_743 => None,
533            // Unicode codepoints
534            _ => char::from_u32(key_code).map(KeyCode::Char),
535        }
536    }
537
538    fn modifiers_from_xterm(value: u32) -> Modifiers {
539        // xterm modifier encoding: value = 1 + modifier_bits
540        // Shift=1, Alt=2, Ctrl=4, Super=8
541        let bits = value.saturating_sub(1);
542        let mut mods = Modifiers::NONE;
543        if bits & 1 != 0 {
544            mods |= Modifiers::SHIFT;
545        }
546        if bits & 2 != 0 {
547            mods |= Modifiers::ALT;
548        }
549        if bits & 4 != 0 {
550            mods |= Modifiers::CTRL;
551        }
552        if bits & 8 != 0 {
553            mods |= Modifiers::SUPER;
554        }
555        mods
556    }
557
558    /// Create a key event with modifiers from CSI params.
559    fn key_with_modifiers(&self, code: KeyCode, params: &[u8]) -> KeyEvent {
560        KeyEvent::new(code).with_modifiers(self.parse_modifier_param(params))
561    }
562
563    /// Parse SGR mouse protocol events.
564    fn parse_sgr_mouse(&self, params: &[u8], final_byte: u8) -> Option<Event> {
565        // Format: CSI < button ; x ; y M|m
566        // Skip the leading '<'
567        let params = &params[1..];
568        let s = std::str::from_utf8(params).ok()?;
569        let mut parts = s.split(';');
570
571        let button_code: u16 = parts.next()?.parse().ok()?;
572        let x: u16 = parts.next()?.parse().ok()?;
573        let y: u16 = parts.next()?.parse().ok()?;
574
575        // Decode button and modifiers
576        let (button, mods) = self.decode_mouse_button(button_code);
577
578        let kind = if final_byte == b'M' {
579            if button_code & 64 != 0 {
580                // Scroll event: bit 6 (64) is set
581                // bits 0-1 determine direction: 0=up, 1=down, 2=left, 3=right
582                match button_code & 3 {
583                    0 => MouseEventKind::ScrollUp,
584                    1 => MouseEventKind::ScrollDown,
585                    2 => MouseEventKind::ScrollLeft,
586                    _ => MouseEventKind::ScrollRight,
587                }
588            } else if button_code & 32 != 0 {
589                // Motion event (bit 5 set)
590                // bits 0-1: 0=left, 1=middle, 2=right, 3=no button (moved)
591                if button_code & 3 == 3 {
592                    MouseEventKind::Moved
593                } else {
594                    MouseEventKind::Drag(button)
595                }
596            } else {
597                MouseEventKind::Down(button)
598            }
599        } else {
600            MouseEventKind::Up(button)
601        };
602
603        Some(Event::Mouse(MouseEvent {
604            kind,
605            x: x.saturating_sub(1), // Convert to 0-indexed
606            y: y.saturating_sub(1),
607            modifiers: mods,
608        }))
609    }
610
611    /// Decode mouse button code to button and modifiers.
612    fn decode_mouse_button(&self, code: u16) -> (MouseButton, Modifiers) {
613        let button = match code & 0b11 {
614            0 => MouseButton::Left,
615            1 => MouseButton::Middle,
616            2 => MouseButton::Right,
617            _ => MouseButton::Left,
618        };
619
620        let mut mods = Modifiers::NONE;
621        if code & 4 != 0 {
622            mods |= Modifiers::SHIFT;
623        }
624        if code & 8 != 0 {
625            mods |= Modifiers::ALT;
626        }
627        if code & 16 != 0 {
628            mods |= Modifiers::CTRL;
629        }
630
631        (button, mods)
632    }
633
634    /// Process SS3 (ESC O) sequences.
635    fn process_ss3(&mut self, byte: u8) -> Option<Event> {
636        // Robustness: ESC restarts sequence
637        if byte == 0x1B {
638            self.state = ParserState::Escape;
639            return None;
640        }
641
642        self.state = ParserState::Ground;
643
644        let code = match byte {
645            b'P' => KeyCode::F(1),
646            b'Q' => KeyCode::F(2),
647            b'R' => KeyCode::F(3),
648            b'S' => KeyCode::F(4),
649            b'A' => KeyCode::Up,
650            b'B' => KeyCode::Down,
651            b'C' => KeyCode::Right,
652            b'D' => KeyCode::Left,
653            b'H' => KeyCode::Home,
654            b'F' => KeyCode::End,
655            _ => return None,
656        };
657
658        Some(Event::Key(KeyEvent::new(code)))
659    }
660
661    /// Process OSC start.
662    fn process_osc(&mut self, byte: u8) -> Option<Event> {
663        // Handle ESC as potential ST terminator (ESC \) - don't add to buffer
664        if byte == 0x1B {
665            self.state = ParserState::OscEscape;
666            return None;
667        }
668
669        self.buffer.push(byte);
670
671        match byte {
672            // BEL terminates immediately
673            0x07 => {
674                self.state = ParserState::Ground;
675                self.parse_osc_sequence()
676            }
677            // Continue collecting
678            _ => {
679                self.state = ParserState::OscContent;
680                None
681            }
682        }
683    }
684
685    /// Process OSC content.
686    fn process_osc_content(&mut self, byte: u8) -> Option<Event> {
687        // Handle ESC (0x1B) as potential terminator or reset
688        if byte == 0x1B {
689            self.state = ParserState::OscEscape;
690            return None;
691        }
692
693        // Robustness: Abort on control characters (except BEL) to prevent swallowing logs
694        if byte < 0x20 && byte != 0x07 {
695            self.state = ParserState::Ground;
696            self.buffer.clear();
697            return None;
698        }
699
700        // DoS protection
701        if self.buffer.len() >= MAX_OSC_LEN {
702            self.state = ParserState::OscIgnore;
703            self.buffer.clear();
704            return None;
705        }
706
707        match byte {
708            // BEL terminates
709            0x07 => {
710                self.state = ParserState::Ground;
711                self.parse_osc_sequence()
712            }
713            // Continue collecting
714            _ => {
715                self.buffer.push(byte);
716                None
717            }
718        }
719    }
720
721    /// Process ESC inside OSC (checking for ST terminator).
722    fn process_osc_escape(&mut self, byte: u8) -> Option<Event> {
723        if byte == b'\\' {
724            // ST (String Terminator) found
725            self.state = ParserState::Ground;
726            self.parse_osc_sequence()
727        } else if byte == 0x1B {
728            // ESC ESC - treat second ESC as start of new sequence (restart)
729            self.state = ParserState::Escape;
730            self.buffer.clear();
731            None
732        } else {
733            // ESC followed by something else.
734            // Strict ANSI would say the OSC is cancelled by the ESC.
735            // We treat this as a restart of parsing at the *current* byte,
736            // effectively interpreting the previous ESC as a cancel.
737
738            self.buffer.clear();
739            self.state = ParserState::Escape;
740            self.process_escape(byte)
741        }
742    }
743
744    /// Ignore bytes until end of OSC sequence.
745    fn process_osc_ignore(&mut self, byte: u8) -> Option<Event> {
746        match byte {
747            // BEL terminates
748            0x07 => {
749                self.state = ParserState::Ground;
750                None
751            }
752            // ESC might start terminator or new sequence
753            0x1B => {
754                self.state = ParserState::OscEscape;
755                None
756            }
757            // Abort on control characters to prevent swallowing logs
758            _ if byte < 0x20 => {
759                self.state = ParserState::Ground;
760                None
761            }
762            // Continue ignoring
763            _ => None,
764        }
765    }
766
767    /// Parse a complete OSC sequence.
768    fn parse_osc_sequence(&mut self) -> Option<Event> {
769        let seq = std::mem::take(&mut self.buffer);
770
771        // OSC 52 clipboard response: OSC 52 ; c ; <base64> BEL/ST
772        if seq.starts_with(b"52;") {
773            return self.parse_osc52_clipboard(&seq);
774        }
775
776        // Other OSC sequences (e.g., OSC 8 hyperlinks) are not parsed as events
777        None
778    }
779
780    /// Parse OSC 52 clipboard response.
781    fn parse_osc52_clipboard(&self, seq: &[u8]) -> Option<Event> {
782        // Format: 52;c;<base64> or 52;p;<base64>
783        let content = &seq[3..]; // Skip "52;"
784        if content.is_empty() {
785            return None;
786        }
787
788        // OSC 52 uses clipboard selectors: c=clipboard, p=primary, s=secondary
789        // We map all to Osc52 source type since that's how we received it
790        let source = ClipboardSource::Osc52;
791
792        // Skip "c;" prefix
793        let base64_start = content.iter().position(|&b| b == b';').map(|i| i + 1)?;
794        let base64_data = &content[base64_start..];
795
796        // Decode base64 (simple implementation)
797        let decoded = self.decode_base64(base64_data)?;
798
799        Some(Event::Clipboard(ClipboardEvent::new(
800            String::from_utf8_lossy(&decoded).into_owned(),
801            source,
802        )))
803    }
804
805    /// Simple base64 decoder.
806    fn decode_base64(&self, input: &[u8]) -> Option<Vec<u8>> {
807        const DECODE_TABLE: [i8; 256] = {
808            let mut table = [-1i8; 256];
809            let mut i = 0u8;
810            while i < 26 {
811                table[(b'A' + i) as usize] = i as i8;
812                table[(b'a' + i) as usize] = (i + 26) as i8;
813                i += 1;
814            }
815            let mut i = 0u8;
816            while i < 10 {
817                table[(b'0' + i) as usize] = (i + 52) as i8;
818                i += 1;
819            }
820            table[b'+' as usize] = 62;
821            table[b'/' as usize] = 63;
822            table
823        };
824
825        let mut output = Vec::with_capacity(input.len() * 3 / 4);
826        let mut buffer = 0u32;
827        let mut bits = 0u8;
828
829        for &byte in input {
830            if byte == b'=' {
831                break;
832            }
833            let value = DECODE_TABLE[byte as usize];
834            if value < 0 {
835                continue; // Skip whitespace/invalid
836            }
837            buffer = (buffer << 6) | (value as u32);
838            bits += 6;
839            if bits >= 8 {
840                bits -= 8;
841                output.push((buffer >> bits) as u8);
842                buffer &= (1 << bits) - 1;
843            }
844        }
845
846        Some(output)
847    }
848
849    /// Process UTF-8 continuation bytes.
850    fn process_utf8(&mut self, byte: u8, collected: u8, expected: u8) -> Option<Event> {
851        // Check for valid continuation byte
852        if (byte & 0xC0) != 0x80 {
853            // Invalid - return to ground and re-process the unexpected byte.
854            // Also emit a replacement character for the invalid sequence we just aborted.
855            self.state = ParserState::Ground;
856
857            // Queue the replacement event for the next iteration of the parse loop
858            self.pending_event = self.process_ground(byte);
859
860            return Some(Event::Key(KeyEvent::new(KeyCode::Char(
861                std::char::REPLACEMENT_CHARACTER,
862            ))));
863        }
864
865        self.utf8_buffer[collected as usize] = byte;
866        let new_collected = collected + 1;
867
868        if new_collected == expected {
869            // Complete - decode and emit
870            self.state = ParserState::Ground;
871            let s = std::str::from_utf8(&self.utf8_buffer[..expected as usize]).ok()?;
872            let c = s.chars().next()?;
873            Some(Event::Key(KeyEvent::new(KeyCode::Char(c))))
874        } else {
875            // Need more bytes
876            self.state = ParserState::Utf8 {
877                collected: new_collected,
878                expected,
879            };
880            None
881        }
882    }
883
884    /// Process bytes while in paste mode.
885    fn process_paste_byte(&mut self, byte: u8) -> Option<Event> {
886        const END_SEQ: &[u8] = b"\x1b[201~";
887
888        // Logic:
889        // 1. If we have room in paste_buffer, push it.
890        // 2. If we are full, push to self.buffer (used as a tail tracker) to detect END_SEQ.
891        // 3. Always check if the effective stream ends with END_SEQ.
892
893        if self.paste_buffer.len() < MAX_PASTE_LEN {
894            self.paste_buffer.push(byte);
895
896            // Check for end sequence in paste_buffer
897            if self.paste_buffer.ends_with(END_SEQ) {
898                self.in_paste = false;
899                // Remove the end sequence from content
900                let content_len = self.paste_buffer.len() - END_SEQ.len();
901                let content =
902                    String::from_utf8_lossy(&self.paste_buffer[..content_len]).into_owned();
903                self.paste_buffer.clear();
904                return Some(Event::Paste(PasteEvent::bracketed(content)));
905            }
906        } else {
907            // Buffer is full. DoS protection active.
908            // We stop collecting content, but we MUST track the end sequence.
909            // Use self.buffer as a sliding window for the tail.
910
911            self.buffer.push(byte);
912            if self.buffer.len() > END_SEQ.len() {
913                self.buffer.remove(0);
914            }
915
916            // Check if we found the end sequence.
917            // The sequence might be split between paste_buffer and buffer.
918            // We only need to check the last 6 bytes.
919            // Since `buffer` contains the most recent bytes (up to 6), and `paste_buffer` is full...
920
921            // Construct a view of the last 6 bytes
922            let mut last_bytes = [0u8; 6];
923            let tail_len = self.buffer.len();
924            let paste_len = self.paste_buffer.len();
925
926            if tail_len + paste_len >= 6 {
927                // Fill from buffer (reverse order)
928                for i in 0..tail_len {
929                    last_bytes[6 - tail_len + i] = self.buffer[i];
930                }
931                // Fill remaining from paste_buffer
932                let remaining = 6 - tail_len;
933                if remaining > 0 {
934                    let start = paste_len - remaining;
935                    last_bytes[..remaining]
936                        .copy_from_slice(&self.paste_buffer[start..(remaining + start)]);
937                }
938
939                if last_bytes == END_SEQ {
940                    self.in_paste = false;
941
942                    // We found the end sequence.
943                    // The content is `paste_buffer` MINUS the part of END_SEQ that was in it.
944                    // `remaining` bytes of END_SEQ were in paste_buffer.
945
946                    let content_len = paste_len - remaining;
947                    let content =
948                        String::from_utf8_lossy(&self.paste_buffer[..content_len]).into_owned();
949
950                    self.paste_buffer.clear();
951                    self.buffer.clear();
952
953                    return Some(Event::Paste(PasteEvent::bracketed(content)));
954                }
955            }
956        }
957
958        None
959    }
960}
961
962#[cfg(test)]
963mod tests {
964    use super::*;
965
966    #[test]
967    fn csi_ignore_handles_final_bytes() {
968        let mut parser = InputParser::new();
969
970        // Create a very long CSI sequence terminated by '@' (0x40)
971        // 0x40 is a valid Final Byte (ECMA-48), but our parser currently only checks A-Za-z~
972        let mut seq = vec![0x1B, b'['];
973        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100)); // Trigger CsiIgnore
974        seq.push(b'@'); // Final byte
975
976        let events = parser.parse(&seq);
977        assert_eq!(events.len(), 0);
978
979        // Feed 'a'. If '@' was correctly treated as final byte, 'a' should be parsed as 'a'.
980        // If '@' was ignored (stayed in CsiIgnore), 'a' terminates the sequence and is swallowed.
981        let events = parser.parse(b"a");
982        assert_eq!(events.len(), 1, "Subsequent char 'a' was swallowed");
983        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
984    }
985
986    #[test]
987    fn ascii_characters_parsed() {
988        let mut parser = InputParser::new();
989
990        let events = parser.parse(b"abc");
991        assert_eq!(events.len(), 3);
992        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
993        assert!(matches!(events[1], Event::Key(k) if k.code == KeyCode::Char('b')));
994        assert!(matches!(events[2], Event::Key(k) if k.code == KeyCode::Char('c')));
995    }
996
997    #[test]
998    fn control_characters() {
999        let mut parser = InputParser::new();
1000
1001        // Ctrl+A
1002        let events = parser.parse(&[0x01]);
1003        assert_eq!(events.len(), 1);
1004        assert!(matches!(
1005            events[0],
1006            Event::Key(k) if k.code == KeyCode::Char('a') && k.modifiers.contains(Modifiers::CTRL)
1007        ));
1008
1009        // Backspace
1010        let events = parser.parse(&[0x7F]);
1011        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Backspace));
1012    }
1013
1014    #[test]
1015    fn arrow_keys() {
1016        let mut parser = InputParser::new();
1017
1018        assert!(matches!(
1019            parser.parse(b"\x1b[A").first(),
1020            Some(Event::Key(k)) if k.code == KeyCode::Up
1021        ));
1022        assert!(matches!(
1023            parser.parse(b"\x1b[B").first(),
1024            Some(Event::Key(k)) if k.code == KeyCode::Down
1025        ));
1026        assert!(matches!(
1027            parser.parse(b"\x1b[C").first(),
1028            Some(Event::Key(k)) if k.code == KeyCode::Right
1029        ));
1030        assert!(matches!(
1031            parser.parse(b"\x1b[D").first(),
1032            Some(Event::Key(k)) if k.code == KeyCode::Left
1033        ));
1034    }
1035
1036    #[test]
1037    fn function_keys_ss3() {
1038        let mut parser = InputParser::new();
1039
1040        assert!(matches!(
1041            parser.parse(b"\x1bOP").first(),
1042            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1043        ));
1044        assert!(matches!(
1045            parser.parse(b"\x1bOQ").first(),
1046            Some(Event::Key(k)) if k.code == KeyCode::F(2)
1047        ));
1048        assert!(matches!(
1049            parser.parse(b"\x1bOR").first(),
1050            Some(Event::Key(k)) if k.code == KeyCode::F(3)
1051        ));
1052        assert!(matches!(
1053            parser.parse(b"\x1bOS").first(),
1054            Some(Event::Key(k)) if k.code == KeyCode::F(4)
1055        ));
1056    }
1057
1058    #[test]
1059    fn function_keys_csi() {
1060        let mut parser = InputParser::new();
1061
1062        assert!(matches!(
1063            parser.parse(b"\x1b[15~").first(),
1064            Some(Event::Key(k)) if k.code == KeyCode::F(5)
1065        ));
1066        assert!(matches!(
1067            parser.parse(b"\x1b[17~").first(),
1068            Some(Event::Key(k)) if k.code == KeyCode::F(6)
1069        ));
1070    }
1071
1072    #[test]
1073    fn modifiers_in_csi() {
1074        let mut parser = InputParser::new();
1075
1076        // Shift+Up: CSI 1;2 A
1077        let events = parser.parse(b"\x1b[1;2A");
1078        assert!(matches!(
1079            events.first(),
1080            Some(Event::Key(k)) if k.code == KeyCode::Up && k.modifiers.contains(Modifiers::SHIFT)
1081        ));
1082
1083        // Ctrl+Up: CSI 1;5 A
1084        let events = parser.parse(b"\x1b[1;5A");
1085        assert!(matches!(
1086            events.first(),
1087            Some(Event::Key(k)) if k.code == KeyCode::Up && k.modifiers.contains(Modifiers::CTRL)
1088        ));
1089    }
1090
1091    #[test]
1092    fn modifiers_in_csi_alt_ctrl() {
1093        let mut parser = InputParser::new();
1094
1095        // Alt+Ctrl+Up: CSI 1;7 A (1 + ALT(2) + CTRL(4) = 7)
1096        let events = parser.parse(b"\x1b[1;7A");
1097        assert!(matches!(
1098            events.first(),
1099            Some(Event::Key(k))
1100                if k.code == KeyCode::Up
1101                    && k.modifiers.contains(Modifiers::ALT)
1102                    && k.modifiers.contains(Modifiers::CTRL)
1103        ));
1104    }
1105
1106    #[test]
1107    fn kitty_keyboard_basic_char() {
1108        let mut parser = InputParser::new();
1109
1110        let events = parser.parse(b"\x1b[97u");
1111        assert!(matches!(
1112            events.first(),
1113            Some(Event::Key(k))
1114                if k.code == KeyCode::Char('a')
1115                    && k.modifiers == Modifiers::NONE
1116                    && k.kind == KeyEventKind::Press
1117        ));
1118    }
1119
1120    #[test]
1121    fn kitty_keyboard_with_modifiers_and_kind() {
1122        let mut parser = InputParser::new();
1123
1124        // Ctrl+repeat for 'a' (modifiers=5, event_type=2)
1125        let events = parser.parse(b"\x1b[97;5:2u");
1126        assert!(matches!(
1127            events.first(),
1128            Some(Event::Key(k))
1129                if k.code == KeyCode::Char('a')
1130                    && k.modifiers.contains(Modifiers::CTRL)
1131                    && k.kind == KeyEventKind::Repeat
1132        ));
1133    }
1134
1135    #[test]
1136    fn kitty_keyboard_function_key() {
1137        let mut parser = InputParser::new();
1138
1139        let events = parser.parse(b"\x1b[57364;1u");
1140        assert!(matches!(
1141            events.first(),
1142            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1143        ));
1144    }
1145
1146    #[test]
1147    fn alt_key_escapes() {
1148        let mut parser = InputParser::new();
1149
1150        let events = parser.parse(b"\x1ba");
1151        assert!(matches!(
1152            events.first(),
1153            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.modifiers.contains(Modifiers::ALT)
1154        ));
1155    }
1156
1157    #[test]
1158    fn alt_backspace() {
1159        let mut parser = InputParser::new();
1160
1161        let events = parser.parse(b"\x1b\x7f");
1162        assert!(matches!(
1163            events.first(),
1164            Some(Event::Key(k))
1165                if k.code == KeyCode::Backspace && k.modifiers.contains(Modifiers::ALT)
1166        ));
1167    }
1168
1169    #[test]
1170    fn escape_escape_resets_state() {
1171        let mut parser = InputParser::new();
1172
1173        let events = parser.parse(b"\x1b\x1b");
1174        assert!(matches!(
1175            events.first(),
1176            Some(Event::Key(k)) if k.code == KeyCode::Escape && k.modifiers.contains(Modifiers::ALT)
1177        ));
1178
1179        let events = parser.parse(b"a");
1180        assert!(matches!(
1181            events.first(),
1182            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.modifiers == Modifiers::NONE
1183        ));
1184    }
1185
1186    #[test]
1187    fn focus_events() {
1188        let mut parser = InputParser::new();
1189
1190        assert!(matches!(
1191            parser.parse(b"\x1b[I").first(),
1192            Some(Event::Focus(true))
1193        ));
1194        assert!(matches!(
1195            parser.parse(b"\x1b[O").first(),
1196            Some(Event::Focus(false))
1197        ));
1198    }
1199
1200    #[test]
1201    fn bracketed_paste() {
1202        let mut parser = InputParser::new();
1203
1204        // Start paste mode, paste content, end paste mode
1205        let events = parser.parse(b"\x1b[200~hello world\x1b[201~");
1206        assert_eq!(events.len(), 1);
1207        assert!(matches!(
1208            &events[0],
1209            Event::Paste(p) if p.text == "hello world"
1210        ));
1211    }
1212
1213    #[test]
1214    fn mouse_sgr_protocol() {
1215        let mut parser = InputParser::new();
1216
1217        // Left click at (10, 20)
1218        let events = parser.parse(b"\x1b[<0;10;20M");
1219        assert!(matches!(
1220            events.first(),
1221            Some(Event::Mouse(m)) if m.x == 9 && m.y == 19 // 0-indexed
1222        ));
1223    }
1224
1225    #[test]
1226    fn mouse_sgr_modifiers() {
1227        let mut parser = InputParser::new();
1228
1229        // Shift+Alt+Ctrl + left button (0 + 4 + 8 + 16 = 28)
1230        let events = parser.parse(b"\x1b[<28;3;4M");
1231        assert!(matches!(
1232            events.first(),
1233            Some(Event::Mouse(m))
1234                if m.modifiers.contains(Modifiers::SHIFT)
1235                    && m.modifiers.contains(Modifiers::ALT)
1236                    && m.modifiers.contains(Modifiers::CTRL)
1237        ));
1238    }
1239
1240    #[test]
1241    fn mouse_sgr_scroll_up() {
1242        let mut parser = InputParser::new();
1243
1244        // Scroll up: button code 64
1245        let events = parser.parse(b"\x1b[<64;5;5M");
1246        assert!(matches!(
1247            events.first(),
1248            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollUp)
1249        ));
1250    }
1251
1252    #[test]
1253    fn mouse_sgr_scroll_down() {
1254        let mut parser = InputParser::new();
1255
1256        // Scroll down: button code 65
1257        let events = parser.parse(b"\x1b[<65;5;5M");
1258        assert!(matches!(
1259            events.first(),
1260            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollDown)
1261        ));
1262    }
1263
1264    #[test]
1265    fn mouse_sgr_scroll_left() {
1266        let mut parser = InputParser::new();
1267
1268        // Scroll left: button code 66
1269        let events = parser.parse(b"\x1b[<66;5;5M");
1270        assert!(matches!(
1271            events.first(),
1272            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollLeft)
1273        ));
1274    }
1275
1276    #[test]
1277    fn mouse_sgr_scroll_right() {
1278        let mut parser = InputParser::new();
1279
1280        // Scroll right: button code 67
1281        let events = parser.parse(b"\x1b[<67;5;5M");
1282        assert!(matches!(
1283            events.first(),
1284            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollRight)
1285        ));
1286    }
1287
1288    #[test]
1289    fn mouse_sgr_drag_left() {
1290        let mut parser = InputParser::new();
1291
1292        // Drag with left button: button code 32
1293        let events = parser.parse(b"\x1b[<32;10;20M");
1294        assert!(matches!(
1295            events.first(),
1296            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Drag(MouseButton::Left))
1297        ));
1298    }
1299
1300    #[test]
1301    fn utf8_characters() {
1302        let mut parser = InputParser::new();
1303
1304        // é (U+00E9) = 0xC3 0xA9
1305        let events = parser.parse(&[0xC3, 0xA9]);
1306        assert!(matches!(
1307            events.first(),
1308            Some(Event::Key(k)) if k.code == KeyCode::Char('é')
1309        ));
1310    }
1311
1312    #[test]
1313    fn invalid_utf8_emits_replacement_then_reprocesses_byte() {
1314        let mut parser = InputParser::new();
1315
1316        // 0xE2 expects a 3-byte sequence, 0x28 is invalid continuation.
1317        let events = parser.parse(&[0xE2, 0x28]);
1318        assert_eq!(events.len(), 2);
1319        assert!(matches!(
1320            events[0],
1321            Event::Key(k) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)
1322        ));
1323        assert!(matches!(
1324            events[1],
1325            Event::Key(k) if k.code == KeyCode::Char('(')
1326        ));
1327    }
1328
1329    #[test]
1330    fn dos_protection_csi() {
1331        let mut parser = InputParser::new();
1332
1333        // Create a very long CSI sequence
1334        let mut seq = vec![0x1B, b'['];
1335        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100));
1336        seq.push(b'A');
1337
1338        // DoS protection kicks in and switches to CsiIgnore
1339        // Excess bytes should be ignored, NOT leaked as characters
1340        let events = parser.parse(&seq);
1341        assert_eq!(
1342            events.len(),
1343            0,
1344            "Oversized CSI sequence should produce no events"
1345        );
1346
1347        // The key invariant: parser should be back in ground state and functional
1348        // Verify by parsing a normal sequence after the attack
1349        let events = parser.parse(b"\x1b[A");
1350        assert!(matches!(
1351            events.first(),
1352            Some(Event::Key(k)) if k.code == KeyCode::Up
1353        ));
1354    }
1355
1356    #[test]
1357    fn incomplete_csi_sequence_emits_no_event() {
1358        let mut parser = InputParser::new();
1359        let events = parser.parse(b"\x1b[");
1360        assert!(events.is_empty());
1361    }
1362
1363    #[test]
1364    fn dos_protection_paste() {
1365        let mut parser = InputParser::new();
1366
1367        // Start paste mode
1368        parser.parse(b"\x1b[200~");
1369
1370        // Paste content up to the limit
1371        let content = vec![b'x'; MAX_PASTE_LEN - 100]; // Leave room for end sequence
1372        parser.parse(&content);
1373
1374        // End paste mode
1375        let events = parser.parse(b"\x1b[201~");
1376
1377        // Should have collected content up to limit
1378        assert!(matches!(
1379            events.first(),
1380            Some(Event::Paste(p)) if p.text.len() <= MAX_PASTE_LEN
1381        ));
1382    }
1383
1384    #[test]
1385    fn dos_protection_paste_overflow_terminator() {
1386        let mut parser = InputParser::new();
1387
1388        // Start paste mode
1389        parser.parse(b"\x1b[200~");
1390
1391        // Overflow the buffer by pushing more than MAX_PASTE_LEN bytes.
1392        // DoS protection stops collecting content once buffer is full,
1393        // but continues tracking the end sequence to properly exit paste mode.
1394        let overflow = 100;
1395        let content = vec![b'a'; MAX_PASTE_LEN + overflow];
1396        parser.parse(&content);
1397
1398        // Send terminator - parser MUST detect it and exit paste mode.
1399        // Even though the buffer overflowed, the terminator detection still works.
1400        let events = parser.parse(b"\x1b[201~");
1401
1402        assert_eq!(events.len(), 1, "Should emit paste event");
1403        match &events[0] {
1404            Event::Paste(p) => {
1405                // Content is capped at MAX_PASTE_LEN due to DoS protection.
1406                // Overflow bytes are discarded but terminator is still detected.
1407                assert_eq!(
1408                    p.text.len(),
1409                    MAX_PASTE_LEN,
1410                    "Paste should be capped at MAX_PASTE_LEN bytes"
1411                );
1412                // The content should be all 'a' since we filled with 'a'
1413                assert!(p.text.chars().all(|c| c == 'a'));
1414            }
1415            _ => unreachable!("Expected Paste event"),
1416        }
1417
1418        // Verify we are back in ground state by parsing a key
1419        let events = parser.parse(b"b");
1420        assert_eq!(events.len(), 1);
1421        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('b')));
1422    }
1423
1424    #[test]
1425    fn no_panic_on_invalid_input() {
1426        let mut parser = InputParser::new();
1427
1428        // Random bytes that might trip up the parser
1429        let garbage = [0xFF, 0xFE, 0x00, 0x1B, 0x1B, 0x1B, b'[', 0xFF, b']', 0x00];
1430
1431        // Should not panic
1432        let _ = parser.parse(&garbage);
1433    }
1434
1435    #[test]
1436    fn dos_protection_paste_boundary() {
1437        let mut parser = InputParser::new();
1438        // Start paste mode
1439        parser.parse(b"\x1b[200~");
1440
1441        // Fill buffer exactly to limit
1442        let content = vec![b'x'; MAX_PASTE_LEN];
1443        parser.parse(&content);
1444
1445        // Send end sequence
1446        // Current bug: This will be dropped because buffer is full, trapping parser
1447        let events = parser.parse(b"\x1b[201~");
1448
1449        assert!(
1450            !events.is_empty(),
1451            "Parser trapped in paste mode after hitting limit"
1452        );
1453        assert!(matches!(events[0], Event::Paste(_)));
1454    }
1455
1456    // ── Navigation keys via CSI ~ sequences ──────────────────────────
1457
1458    #[test]
1459    fn csi_tilde_home() {
1460        let mut parser = InputParser::new();
1461        let events = parser.parse(b"\x1b[1~");
1462        assert!(matches!(
1463            events.first(),
1464            Some(Event::Key(k)) if k.code == KeyCode::Home
1465        ));
1466    }
1467
1468    #[test]
1469    fn csi_tilde_insert() {
1470        let mut parser = InputParser::new();
1471        let events = parser.parse(b"\x1b[2~");
1472        assert!(matches!(
1473            events.first(),
1474            Some(Event::Key(k)) if k.code == KeyCode::Insert
1475        ));
1476    }
1477
1478    #[test]
1479    fn csi_tilde_delete() {
1480        let mut parser = InputParser::new();
1481        let events = parser.parse(b"\x1b[3~");
1482        assert!(matches!(
1483            events.first(),
1484            Some(Event::Key(k)) if k.code == KeyCode::Delete
1485        ));
1486    }
1487
1488    #[test]
1489    fn csi_tilde_end() {
1490        let mut parser = InputParser::new();
1491        let events = parser.parse(b"\x1b[4~");
1492        assert!(matches!(
1493            events.first(),
1494            Some(Event::Key(k)) if k.code == KeyCode::End
1495        ));
1496    }
1497
1498    #[test]
1499    fn csi_tilde_page_up() {
1500        let mut parser = InputParser::new();
1501        let events = parser.parse(b"\x1b[5~");
1502        assert!(matches!(
1503            events.first(),
1504            Some(Event::Key(k)) if k.code == KeyCode::PageUp
1505        ));
1506    }
1507
1508    #[test]
1509    fn csi_tilde_page_down() {
1510        let mut parser = InputParser::new();
1511        let events = parser.parse(b"\x1b[6~");
1512        assert!(matches!(
1513            events.first(),
1514            Some(Event::Key(k)) if k.code == KeyCode::PageDown
1515        ));
1516    }
1517
1518    // ── Navigation keys via CSI H/F (xterm-style) ───────────────────
1519
1520    #[test]
1521    fn csi_home_and_end() {
1522        let mut parser = InputParser::new();
1523        assert!(matches!(
1524            parser.parse(b"\x1b[H").first(),
1525            Some(Event::Key(k)) if k.code == KeyCode::Home
1526        ));
1527        assert!(matches!(
1528            parser.parse(b"\x1b[F").first(),
1529            Some(Event::Key(k)) if k.code == KeyCode::End
1530        ));
1531    }
1532
1533    // ── SS3 Home/End ─────────────────────────────────────────────────
1534
1535    #[test]
1536    fn ss3_home_and_end() {
1537        let mut parser = InputParser::new();
1538        assert!(matches!(
1539            parser.parse(b"\x1bOH").first(),
1540            Some(Event::Key(k)) if k.code == KeyCode::Home
1541        ));
1542        assert!(matches!(
1543            parser.parse(b"\x1bOF").first(),
1544            Some(Event::Key(k)) if k.code == KeyCode::End
1545        ));
1546    }
1547
1548    // ── BackTab (Shift+Tab via CSI Z) ────────────────────────────────
1549
1550    #[test]
1551    fn backtab_csi_z() {
1552        let mut parser = InputParser::new();
1553        let events = parser.parse(b"\x1b[Z");
1554        assert!(matches!(
1555            events.first(),
1556            Some(Event::Key(k)) if k.code == KeyCode::BackTab
1557        ));
1558    }
1559
1560    // ── F7-F12 keys via CSI tilde ────────────────────────────────────
1561
1562    #[test]
1563    fn function_keys_f7_to_f12() {
1564        let mut parser = InputParser::new();
1565        assert!(matches!(
1566            parser.parse(b"\x1b[18~").first(),
1567            Some(Event::Key(k)) if k.code == KeyCode::F(7)
1568        ));
1569        assert!(matches!(
1570            parser.parse(b"\x1b[19~").first(),
1571            Some(Event::Key(k)) if k.code == KeyCode::F(8)
1572        ));
1573        assert!(matches!(
1574            parser.parse(b"\x1b[20~").first(),
1575            Some(Event::Key(k)) if k.code == KeyCode::F(9)
1576        ));
1577        assert!(matches!(
1578            parser.parse(b"\x1b[21~").first(),
1579            Some(Event::Key(k)) if k.code == KeyCode::F(10)
1580        ));
1581        assert!(matches!(
1582            parser.parse(b"\x1b[23~").first(),
1583            Some(Event::Key(k)) if k.code == KeyCode::F(11)
1584        ));
1585        assert!(matches!(
1586            parser.parse(b"\x1b[24~").first(),
1587            Some(Event::Key(k)) if k.code == KeyCode::F(12)
1588        ));
1589    }
1590
1591    // ── Modifier combinations on navigation keys ─────────────────────
1592
1593    #[test]
1594    fn ctrl_home_and_alt_end() {
1595        let mut parser = InputParser::new();
1596
1597        // Ctrl+Home: CSI 1;5 H
1598        let events = parser.parse(b"\x1b[1;5H");
1599        assert!(matches!(
1600            events.first(),
1601            Some(Event::Key(k)) if k.code == KeyCode::Home && k.modifiers.contains(Modifiers::CTRL)
1602        ));
1603
1604        // Alt+End: CSI 1;3 F
1605        let events = parser.parse(b"\x1b[1;3F");
1606        assert!(matches!(
1607            events.first(),
1608            Some(Event::Key(k)) if k.code == KeyCode::End && k.modifiers.contains(Modifiers::ALT)
1609        ));
1610    }
1611
1612    #[test]
1613    fn shift_ctrl_arrow() {
1614        let mut parser = InputParser::new();
1615
1616        // Shift+Ctrl+Right: CSI 1;6 C (modifier value 6 = 1 + Shift|Ctrl = 1 + 5)
1617        let events = parser.parse(b"\x1b[1;6C");
1618        assert!(matches!(
1619            events.first(),
1620            Some(Event::Key(k)) if k.code == KeyCode::Right
1621                && k.modifiers.contains(Modifiers::SHIFT)
1622                && k.modifiers.contains(Modifiers::CTRL)
1623        ));
1624    }
1625
1626    #[test]
1627    fn modifiers_on_tilde_keys() {
1628        let mut parser = InputParser::new();
1629
1630        // Ctrl+Delete: CSI 3;5 ~
1631        let events = parser.parse(b"\x1b[3;5~");
1632        assert!(matches!(
1633            events.first(),
1634            Some(Event::Key(k)) if k.code == KeyCode::Delete && k.modifiers.contains(Modifiers::CTRL)
1635        ));
1636
1637        // Shift+PageUp: CSI 5;2 ~
1638        let events = parser.parse(b"\x1b[5;2~");
1639        assert!(matches!(
1640            events.first(),
1641            Some(Event::Key(k)) if k.code == KeyCode::PageUp && k.modifiers.contains(Modifiers::SHIFT)
1642        ));
1643    }
1644
1645    // ── Mouse right/middle click and release ─────────────────────────
1646
1647    #[test]
1648    fn mouse_sgr_right_click() {
1649        let mut parser = InputParser::new();
1650        // Right click: button code 2
1651        let events = parser.parse(b"\x1b[<2;15;10M");
1652        assert!(matches!(
1653            events.first(),
1654            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Right))
1655                && m.x == 14 && m.y == 9
1656        ));
1657    }
1658
1659    #[test]
1660    fn mouse_sgr_middle_click() {
1661        let mut parser = InputParser::new();
1662        // Middle click: button code 1
1663        let events = parser.parse(b"\x1b[<1;5;5M");
1664        assert!(matches!(
1665            events.first(),
1666            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Middle))
1667        ));
1668    }
1669
1670    #[test]
1671    fn mouse_sgr_button_release() {
1672        let mut parser = InputParser::new();
1673        // Left button release: final byte 'm'
1674        let events = parser.parse(b"\x1b[<0;10;20m");
1675        assert!(matches!(
1676            events.first(),
1677            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Up(MouseButton::Left))
1678        ));
1679    }
1680
1681    #[test]
1682    fn mouse_sgr_moved() {
1683        let mut parser = InputParser::new();
1684        // Mouse move (no button): button code 35 (32 | 3, bit 5 set + bits 0-1 = 3)
1685        let events = parser.parse(b"\x1b[<35;10;20M");
1686        assert!(matches!(
1687            events.first(),
1688            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Moved)
1689        ));
1690    }
1691
1692    #[test]
1693    fn mouse_sgr_with_modifiers() {
1694        let mut parser = InputParser::new();
1695        // Shift+Left click: button_code bit 2 set (shift) = 4
1696        let events = parser.parse(b"\x1b[<4;5;5M");
1697        assert!(matches!(
1698            events.first(),
1699            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1700                && m.modifiers.contains(Modifiers::SHIFT)
1701        ));
1702
1703        // Ctrl+Left click: button_code bit 4 set (ctrl) = 16
1704        let events = parser.parse(b"\x1b[<16;5;5M");
1705        assert!(matches!(
1706            events.first(),
1707            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1708                && m.modifiers.contains(Modifiers::CTRL)
1709        ));
1710
1711        // Alt+Left click: button_code bit 3 set (alt) = 8
1712        let events = parser.parse(b"\x1b[<8;5;5M");
1713        assert!(matches!(
1714            events.first(),
1715            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1716                && m.modifiers.contains(Modifiers::ALT)
1717        ));
1718    }
1719
1720    // ── Kitty keyboard release events and special keys ───────────────
1721
1722    #[test]
1723    fn kitty_keyboard_release_event() {
1724        let mut parser = InputParser::new();
1725        // Release event: kind=3
1726        let events = parser.parse(b"\x1b[97;1:3u");
1727        assert!(matches!(
1728            events.first(),
1729            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.kind == KeyEventKind::Release
1730        ));
1731
1732        // Ctrl+release for 'A' (modifiers=5, event_type=3)
1733        let events = parser.parse(b"\x1b[65;5:3u");
1734        assert!(matches!(
1735            events.first(),
1736            Some(Event::Key(k))
1737                if k.code == KeyCode::Char('A')
1738                    && k.modifiers.contains(Modifiers::CTRL)
1739                    && k.kind == KeyEventKind::Release
1740        ));
1741    }
1742
1743    #[test]
1744    fn kitty_keyboard_special_keys() {
1745        let mut parser = InputParser::new();
1746
1747        // Escape: 57344
1748        assert!(matches!(
1749            parser.parse(b"\x1b[57344u").first(),
1750            Some(Event::Key(k)) if k.code == KeyCode::Escape
1751        ));
1752
1753        // Enter: 57345
1754        assert!(matches!(
1755            parser.parse(b"\x1b[57345u").first(),
1756            Some(Event::Key(k)) if k.code == KeyCode::Enter
1757        ));
1758
1759        // Tab: 57346
1760        assert!(matches!(
1761            parser.parse(b"\x1b[57346u").first(),
1762            Some(Event::Key(k)) if k.code == KeyCode::Tab
1763        ));
1764
1765        // Backspace: 57347
1766        assert!(matches!(
1767            parser.parse(b"\x1b[57347u").first(),
1768            Some(Event::Key(k)) if k.code == KeyCode::Backspace
1769        ));
1770
1771        // Insert: 57348
1772        assert!(matches!(
1773            parser.parse(b"\x1b[57348u").first(),
1774            Some(Event::Key(k)) if k.code == KeyCode::Insert
1775        ));
1776
1777        // Delete: 57349
1778        assert!(matches!(
1779            parser.parse(b"\x1b[57349u").first(),
1780            Some(Event::Key(k)) if k.code == KeyCode::Delete
1781        ));
1782    }
1783
1784    #[test]
1785    fn kitty_keyboard_navigation_keys() {
1786        let mut parser = InputParser::new();
1787
1788        // Left: 57350
1789        assert!(matches!(
1790            parser.parse(b"\x1b[57350u").first(),
1791            Some(Event::Key(k)) if k.code == KeyCode::Left
1792        ));
1793        // Right: 57351
1794        assert!(matches!(
1795            parser.parse(b"\x1b[57351u").first(),
1796            Some(Event::Key(k)) if k.code == KeyCode::Right
1797        ));
1798        // Up: 57352
1799        assert!(matches!(
1800            parser.parse(b"\x1b[57352u").first(),
1801            Some(Event::Key(k)) if k.code == KeyCode::Up
1802        ));
1803        // Down: 57353
1804        assert!(matches!(
1805            parser.parse(b"\x1b[57353u").first(),
1806            Some(Event::Key(k)) if k.code == KeyCode::Down
1807        ));
1808        // PageUp: 57354
1809        assert!(matches!(
1810            parser.parse(b"\x1b[57354u").first(),
1811            Some(Event::Key(k)) if k.code == KeyCode::PageUp
1812        ));
1813        // PageDown: 57355
1814        assert!(matches!(
1815            parser.parse(b"\x1b[57355u").first(),
1816            Some(Event::Key(k)) if k.code == KeyCode::PageDown
1817        ));
1818        // Home: 57356
1819        assert!(matches!(
1820            parser.parse(b"\x1b[57356u").first(),
1821            Some(Event::Key(k)) if k.code == KeyCode::Home
1822        ));
1823        // End: 57357
1824        assert!(matches!(
1825            parser.parse(b"\x1b[57357u").first(),
1826            Some(Event::Key(k)) if k.code == KeyCode::End
1827        ));
1828    }
1829
1830    #[test]
1831    fn kitty_keyboard_f_keys() {
1832        let mut parser = InputParser::new();
1833        // F1: 57364
1834        assert!(matches!(
1835            parser.parse(b"\x1b[57364u").first(),
1836            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1837        ));
1838        // F12: 57375
1839        assert!(matches!(
1840            parser.parse(b"\x1b[57375u").first(),
1841            Some(Event::Key(k)) if k.code == KeyCode::F(12)
1842        ));
1843        // F24: 57387
1844        assert!(matches!(
1845            parser.parse(b"\x1b[57387u").first(),
1846            Some(Event::Key(k)) if k.code == KeyCode::F(24)
1847        ));
1848    }
1849
1850    #[test]
1851    fn kitty_keyboard_ascii_as_standard() {
1852        let mut parser = InputParser::new();
1853        // Tab (9), Enter (13), Escape (27), Backspace (127)
1854        assert!(matches!(
1855            parser.parse(b"\x1b[9u").first(),
1856            Some(Event::Key(k)) if k.code == KeyCode::Tab
1857        ));
1858        assert!(matches!(
1859            parser.parse(b"\x1b[13u").first(),
1860            Some(Event::Key(k)) if k.code == KeyCode::Enter
1861        ));
1862        assert!(matches!(
1863            parser.parse(b"\x1b[27u").first(),
1864            Some(Event::Key(k)) if k.code == KeyCode::Escape
1865        ));
1866        assert!(matches!(
1867            parser.parse(b"\x1b[127u").first(),
1868            Some(Event::Key(k)) if k.code == KeyCode::Backspace
1869        ));
1870        // Backspace alternate: 8
1871        assert!(matches!(
1872            parser.parse(b"\x1b[8u").first(),
1873            Some(Event::Key(k)) if k.code == KeyCode::Backspace
1874        ));
1875    }
1876
1877    // ── OSC 52 clipboard ─────────────────────────────────────────────
1878
1879    #[test]
1880    fn osc52_clipboard_bel_terminated() {
1881        let mut parser = InputParser::new();
1882        // OSC 52;c;<base64 "hello"> BEL
1883        // "hello" in base64 is "aGVsbG8="
1884        let events = parser.parse(b"\x1b]52;c;aGVsbG8=\x07");
1885        assert!(matches!(
1886            events.first(),
1887            Some(Event::Clipboard(c)) if c.content == "hello" && c.source == ClipboardSource::Osc52
1888        ));
1889    }
1890
1891    #[test]
1892    fn osc52_clipboard_st_terminated() {
1893        let mut parser = InputParser::new();
1894        // OSC 52;c;<base64 "hello"> ESC \
1895        let events = parser.parse(b"\x1b]52;c;aGVsbG8=\x1b\\");
1896        assert!(matches!(
1897            events.first(),
1898            Some(Event::Clipboard(c)) if c.content == "hello"
1899        ));
1900    }
1901
1902    #[test]
1903    fn osc52_clipboard_primary_selection() {
1904        let mut parser = InputParser::new();
1905        // Primary selection: p instead of c
1906        // "abc" in base64 is "YWJj"
1907        let events = parser.parse(b"\x1b]52;p;YWJj\x07");
1908        assert!(matches!(
1909            events.first(),
1910            Some(Event::Clipboard(c)) if c.content == "abc"
1911        ));
1912    }
1913
1914    // ── Control keys ─────────────────────────────────────────────────
1915
1916    #[test]
1917    fn ctrl_space_is_null() {
1918        let mut parser = InputParser::new();
1919        let events = parser.parse(&[0x00]);
1920        assert!(matches!(
1921            events.first(),
1922            Some(Event::Key(k)) if k.code == KeyCode::Null
1923        ));
1924    }
1925
1926    #[test]
1927    fn all_ctrl_letter_keys() {
1928        let mut parser = InputParser::new();
1929        // Ctrl+A (0x01) through Ctrl+Z (0x1A), skipping Backspace (0x08), Tab (0x09), and Enter (0x0D)
1930        for byte in 0x01..=0x1Au8 {
1931            let events = parser.parse(&[byte]);
1932            assert_eq!(
1933                events.len(),
1934                1,
1935                "Ctrl+{} should produce one event",
1936                (byte + b'a' - 1) as char
1937            );
1938            match byte {
1939                0x08 => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Backspace)),
1940                0x09 => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Tab)),
1941                0x0D => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Enter)),
1942                _ => {
1943                    let expected_char = (byte + b'a' - 1) as char;
1944                    match &events[0] {
1945                        Event::Key(k) => {
1946                            assert_eq!(
1947                                k.code,
1948                                KeyCode::Char(expected_char),
1949                                "Byte 0x{byte:02X} should produce Ctrl+{expected_char}"
1950                            );
1951                            assert!(
1952                                k.modifiers.contains(Modifiers::CTRL),
1953                                "Byte 0x{byte:02X} should have Ctrl modifier"
1954                            );
1955                        }
1956                        other => {
1957                            panic!("Byte 0x{byte:02X}: expected Key event, got {other:?}");
1958                        }
1959                    }
1960                }
1961            }
1962        }
1963    }
1964
1965    // ── UTF-8 multi-byte: 3-byte and 4-byte ─────────────────────────
1966
1967    #[test]
1968    fn utf8_3byte_cjk() {
1969        let mut parser = InputParser::new();
1970        // 中 (U+4E2D) = 0xE4 0xB8 0xAD
1971        let events = parser.parse(&[0xE4, 0xB8, 0xAD]);
1972        assert!(matches!(
1973            events.first(),
1974            Some(Event::Key(k)) if k.code == KeyCode::Char('中')
1975        ));
1976    }
1977
1978    #[test]
1979    fn utf8_4byte_emoji() {
1980        let mut parser = InputParser::new();
1981        // 🦀 (U+1F980) = 0xF0 0x9F 0xA6 0x80
1982        let events = parser.parse(&[0xF0, 0x9F, 0xA6, 0x80]);
1983        assert!(matches!(
1984            events.first(),
1985            Some(Event::Key(k)) if k.code == KeyCode::Char('🦀')
1986        ));
1987    }
1988
1989    // ── Empty input ──────────────────────────────────────────────────
1990
1991    #[test]
1992    fn empty_input_returns_no_events() {
1993        let mut parser = InputParser::new();
1994        let events = parser.parse(b"");
1995        assert!(events.is_empty());
1996    }
1997
1998    // ── Unknown CSI tilde values ─────────────────────────────────────
1999
2000    #[test]
2001    fn unknown_csi_tilde_ignored() {
2002        let mut parser = InputParser::new();
2003        // Code 99 is not a known tilde key
2004        let events = parser.parse(b"\x1b[99~");
2005        assert!(events.is_empty());
2006
2007        // Parser should still work
2008        let events = parser.parse(b"a");
2009        assert!(matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char('a')));
2010    }
2011
2012    // ── Alt+various characters ───────────────────────────────────────
2013
2014    #[test]
2015    fn alt_special_chars() {
2016        let mut parser = InputParser::new();
2017
2018        // Alt+space
2019        let events = parser.parse(b"\x1b ");
2020        assert!(matches!(
2021            events.first(),
2022            Some(Event::Key(k)) if k.code == KeyCode::Char(' ') && k.modifiers.contains(Modifiers::ALT)
2023        ));
2024
2025        // Alt+digit
2026        let events = parser.parse(b"\x1b5");
2027        assert!(matches!(
2028            events.first(),
2029            Some(Event::Key(k)) if k.code == KeyCode::Char('5') && k.modifiers.contains(Modifiers::ALT)
2030        ));
2031
2032        // Alt+bracket
2033        let events = parser.parse(b"\x1b}");
2034        assert!(matches!(
2035            events.first(),
2036            Some(Event::Key(k)) if k.code == KeyCode::Char('}') && k.modifiers.contains(Modifiers::ALT)
2037        ));
2038    }
2039
2040    // ── SS3 arrow keys ───────────────────────────────────────────────
2041
2042    #[test]
2043    fn ss3_arrow_keys() {
2044        let mut parser = InputParser::new();
2045        assert!(matches!(
2046            parser.parse(b"\x1bOA").first(),
2047            Some(Event::Key(k)) if k.code == KeyCode::Up
2048        ));
2049        assert!(matches!(
2050            parser.parse(b"\x1bOB").first(),
2051            Some(Event::Key(k)) if k.code == KeyCode::Down
2052        ));
2053        assert!(matches!(
2054            parser.parse(b"\x1bOC").first(),
2055            Some(Event::Key(k)) if k.code == KeyCode::Right
2056        ));
2057        assert!(matches!(
2058            parser.parse(b"\x1bOD").first(),
2059            Some(Event::Key(k)) if k.code == KeyCode::Left
2060        ));
2061    }
2062
2063    // ── Xterm modifier encoding ──────────────────────────────────────
2064
2065    #[test]
2066    fn xterm_modifier_encoding() {
2067        // Verify modifiers_from_xterm decoding (value = 1 + modifier_bits)
2068        assert_eq!(InputParser::modifiers_from_xterm(1), Modifiers::NONE);
2069        assert_eq!(InputParser::modifiers_from_xterm(2), Modifiers::SHIFT);
2070        assert_eq!(InputParser::modifiers_from_xterm(3), Modifiers::ALT);
2071        assert_eq!(
2072            InputParser::modifiers_from_xterm(4),
2073            Modifiers::SHIFT | Modifiers::ALT
2074        );
2075        assert_eq!(InputParser::modifiers_from_xterm(5), Modifiers::CTRL);
2076        assert_eq!(
2077            InputParser::modifiers_from_xterm(6),
2078            Modifiers::SHIFT | Modifiers::CTRL
2079        );
2080        assert_eq!(InputParser::modifiers_from_xterm(9), Modifiers::SUPER);
2081    }
2082
2083    // ── SS3 interrupted by ESC ───────────────────────────────────────
2084
2085    #[test]
2086    fn ss3_interrupted_by_esc() {
2087        let mut parser = InputParser::new();
2088        // ESC O ESC should restart into Escape state
2089        let events = parser.parse(b"\x1bO\x1b[A");
2090        // Should get Up arrow from the new ESC [ A sequence
2091        assert!(matches!(
2092            events.first(),
2093            Some(Event::Key(k)) if k.code == KeyCode::Up
2094        ));
2095    }
2096
2097    // ── Kitty keyboard: unhandled keycodes ───────────────────────────
2098
2099    #[test]
2100    fn kitty_keyboard_reserved_keycode_ignored() {
2101        let mut parser = InputParser::new();
2102        // Reserved range 57358..=57363 returns None
2103        let events = parser.parse(b"\x1b[57360u");
2104        assert!(events.is_empty());
2105
2106        // Parser still works
2107        let events = parser.parse(b"x");
2108        assert!(matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char('x')));
2109    }
2110    #[test]
2111    fn utf8_invalid_sequence_emits_replacement() {
2112        let mut parser = InputParser::new();
2113
2114        // 0xE0 is a start of 3-byte sequence.
2115        // 0x41 ('A') is not a valid continuation byte.
2116        // Should emit Replacement Character then 'A'.
2117        let events = parser.parse(&[0xE0, 0x41]);
2118        assert_eq!(events.len(), 2);
2119
2120        match &events[0] {
2121            Event::Key(k) => assert_eq!(k.code, KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
2122            _ => panic!("Expected replacement character"),
2123        }
2124
2125        match &events[1] {
2126            Event::Key(k) => assert_eq!(k.code, KeyCode::Char('A')),
2127            _ => panic!("Expected character 'A'"),
2128        }
2129    }
2130
2131    #[test]
2132    fn utf8_invalid_lead_emits_replacement() {
2133        let mut parser = InputParser::new();
2134
2135        // 0xC0 is an invalid UTF-8 lead byte (overlong sequence).
2136        let events = parser.parse(&[0xC0, b'a']);
2137        assert!(
2138            matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
2139            "Expected replacement for invalid lead"
2140        );
2141        assert!(
2142            events
2143                .iter()
2144                .any(|e| matches!(e, Event::Key(k) if k.code == KeyCode::Char('a'))),
2145            "Expected subsequent ASCII to be preserved"
2146        );
2147
2148        // 0xF5 is an out-of-range UTF-8 lead byte.
2149        let events = parser.parse(&[0xF5, b'b']);
2150        assert!(
2151            matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
2152            "Expected replacement for out-of-range lead"
2153        );
2154        assert!(
2155            events
2156                .iter()
2157                .any(|e| matches!(e, Event::Key(k) if k.code == KeyCode::Char('b'))),
2158            "Expected subsequent ASCII to be preserved"
2159        );
2160    }
2161}
2162
2163#[cfg(test)]
2164mod proptest_fuzz {
2165    use super::*;
2166    use proptest::prelude::*;
2167
2168    // ── Strategy helpers ────────────────────────────────────────────────
2169    // Avoid turbofish inside proptest! macro (Rust 2024 edition compat).
2170
2171    fn arb_byte() -> impl Strategy<Value = u8> {
2172        any::<u8>()
2173    }
2174
2175    fn arb_byte_vec(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
2176        prop::collection::vec(arb_byte(), 0..=max_len)
2177    }
2178
2179    /// Generate a well-formed CSI sequence: ESC [ <params> <final byte>.
2180    fn csi_sequence() -> impl Strategy<Value = Vec<u8>> {
2181        let params = prop::collection::vec(0x30u8..=0x3F, 0..=20);
2182        let final_byte = 0x40u8..=0x7E;
2183        (params, final_byte).prop_map(|(p, f)| {
2184            let mut buf = vec![0x1B, b'['];
2185            buf.extend_from_slice(&p);
2186            buf.push(f);
2187            buf
2188        })
2189    }
2190
2191    /// Generate an OSC sequence: ESC ] <content> ST.
2192    fn osc_sequence() -> impl Strategy<Value = Vec<u8>> {
2193        let content = prop::collection::vec(0x20u8..=0x7E, 0..=64);
2194        let terminator = prop_oneof![
2195            Just(vec![0x1B, b'\\']), // ESC backslash
2196            Just(vec![0x07]),        // BEL
2197        ];
2198        (content, terminator).prop_map(|(c, t)| {
2199            let mut buf = vec![0x1B, b']'];
2200            buf.extend_from_slice(&c);
2201            buf.extend_from_slice(&t);
2202            buf
2203        })
2204    }
2205
2206    /// Generate an SS3 sequence: ESC O <final byte>.
2207    fn ss3_sequence() -> impl Strategy<Value = Vec<u8>> {
2208        (0x40u8..=0x7E).prop_map(|f| vec![0x1B, b'O', f])
2209    }
2210
2211    /// Generate a bracketed paste: ESC[200~ <content> ESC[201~.
2212    fn paste_sequence() -> impl Strategy<Value = Vec<u8>> {
2213        prop::collection::vec(0x20u8..=0x7E, 0..=128).prop_map(|content| {
2214            let mut buf = vec![0x1B, b'[', b'2', b'0', b'0', b'~'];
2215            buf.extend_from_slice(&content);
2216            buf.extend_from_slice(b"\x1b[201~");
2217            buf
2218        })
2219    }
2220
2221    /// Generate structured adversarial input: mix of valid sequences and random bytes.
2222    fn mixed_adversarial() -> impl Strategy<Value = Vec<u8>> {
2223        let fragment = prop_oneof![
2224            csi_sequence(),
2225            osc_sequence(),
2226            ss3_sequence(),
2227            paste_sequence(),
2228            arb_byte_vec(16),                            // random bytes
2229            Just(vec![0x1B]),                            // bare ESC
2230            Just(vec![0x1B, b'[']),                      // unterminated CSI
2231            Just(vec![0x1B, b']']),                      // unterminated OSC
2232            prop::collection::vec(0x80u8..=0xFF, 1..=4), // high bytes
2233        ];
2234        prop::collection::vec(fragment, 1..=8)
2235            .prop_map(|frags| frags.into_iter().flatten().collect())
2236    }
2237
2238    // ── Property tests ─────────────────────────────────────────────────
2239
2240    proptest! {
2241        /// Random bytes must never panic.
2242        #[test]
2243        fn random_bytes_never_panic(input in arb_byte_vec(512)) {
2244            let mut parser = InputParser::new();
2245            let _ = parser.parse(&input);
2246        }
2247
2248        /// After parsing any input, the parser must be reusable for normal keys.
2249        #[test]
2250        fn parser_recovers_after_garbage(input in arb_byte_vec(256)) {
2251            let mut parser = InputParser::new();
2252            let _ = parser.parse(&input);
2253
2254            // Feed a clean known sequence (letter 'z') after the garbage.
2255            let events = parser.parse(b"z");
2256            // Parser must not panic. We can't assert exact events because
2257            // the parser may still be mid-sequence, but it must not panic.
2258            let _ = events;
2259        }
2260
2261        /// Structured mixed input (valid sequences + garbage) must never panic.
2262        #[test]
2263        fn mixed_sequences_never_panic(input in mixed_adversarial()) {
2264            let mut parser = InputParser::new();
2265            let _ = parser.parse(&input);
2266        }
2267
2268        /// All generated events must be valid (non-panicking Debug).
2269        #[test]
2270        fn events_are_well_formed(input in arb_byte_vec(256)) {
2271            let mut parser = InputParser::new();
2272            let events = parser.parse(&input);
2273            for event in &events {
2274                // Exercise Debug impl — catches inconsistent internal state.
2275                let _ = format!("{event:?}");
2276            }
2277        }
2278
2279        /// CSI sequences never produce more events than bytes fed.
2280        #[test]
2281        fn csi_event_count_bounded(seq in csi_sequence()) {
2282            let mut parser = InputParser::new();
2283            let events = parser.parse(&seq);
2284            prop_assert!(events.len() <= seq.len(),
2285                "Got {} events from {} bytes", events.len(), seq.len());
2286        }
2287
2288        /// OSC sequences never produce more events than bytes fed.
2289        #[test]
2290        fn osc_event_count_bounded(seq in osc_sequence()) {
2291            let mut parser = InputParser::new();
2292            let events = parser.parse(&seq);
2293            prop_assert!(events.len() <= seq.len(),
2294                "Got {} events from {} bytes", events.len(), seq.len());
2295        }
2296
2297        /// Paste content is always bounded by MAX_PASTE_LEN.
2298        #[test]
2299        fn paste_content_bounded(content in prop::collection::vec(arb_byte(), 0..=2048)) {
2300            let mut parser = InputParser::new();
2301            let mut input = vec![0x1B, b'[', b'2', b'0', b'0', b'~'];
2302            input.extend_from_slice(&content);
2303            input.extend_from_slice(b"\x1b[201~");
2304
2305            let events = parser.parse(&input);
2306            for event in &events {
2307                if let Event::Paste(p) = event {
2308                    prop_assert!(p.text.len() <= MAX_PASTE_LEN,
2309                        "Paste text {} exceeds limit {}", p.text.len(), MAX_PASTE_LEN);
2310                }
2311            }
2312        }
2313
2314        /// Feeding input byte-by-byte yields same events as feeding all at once.
2315        #[test]
2316        fn incremental_matches_bulk(input in arb_byte_vec(128)) {
2317            let mut bulk_parser = InputParser::new();
2318            let bulk_events = bulk_parser.parse(&input);
2319
2320            let mut incr_parser = InputParser::new();
2321            let mut incr_events = Vec::new();
2322            for byte in &input {
2323                incr_events.extend(incr_parser.parse(std::slice::from_ref(byte)));
2324            }
2325
2326            let bulk_dbg: Vec<String> = bulk_events.iter().map(|e| format!("{e:?}")).collect();
2327            let incr_dbg: Vec<String> = incr_events.iter().map(|e| format!("{e:?}")).collect();
2328            prop_assert_eq!(bulk_dbg, incr_dbg,
2329                "Bulk vs incremental mismatch for input {:?}", input);
2330        }
2331
2332        /// Repeated parsing of the same input must always produce the same result
2333        /// (parser is deterministic after reset).
2334        #[test]
2335        fn deterministic_output(input in arb_byte_vec(128)) {
2336            let mut parser1 = InputParser::new();
2337            let events1 = parser1.parse(&input);
2338
2339            let mut parser2 = InputParser::new();
2340            let events2 = parser2.parse(&input);
2341
2342            let dbg1: Vec<String> = events1.iter().map(|e| format!("{e:?}")).collect();
2343            let dbg2: Vec<String> = events2.iter().map(|e| format!("{e:?}")).collect();
2344            prop_assert_eq!(dbg1, dbg2);
2345        }
2346    }
2347
2348    // ── Targeted invariant tests (outside proptest! macro) ─────────────
2349
2350    /// After a long garbage run, parser handles a simple key within bounded time.
2351    #[test]
2352    fn no_quadratic_blowup() {
2353        let mut parser = InputParser::new();
2354
2355        // 64KB of random-ish bytes (repeating pattern).
2356        let garbage: Vec<u8> = (0..65536).map(|i| (i % 256) as u8).collect();
2357        let _ = parser.parse(&garbage);
2358
2359        // Follow with a clean key — must not take pathological time.
2360        let events = parser.parse(b"a");
2361        let _ = events; // primarily asserting no hang/panic
2362    }
2363
2364    /// Oversized CSI sequence triggers DoS protection without panic.
2365    #[test]
2366    fn oversized_csi_transitions_to_ignore() {
2367        let mut parser = InputParser::new();
2368
2369        // CSI followed by MAX_CSI_LEN+100 parameter bytes then a final byte.
2370        let mut input = vec![0x1B, b'['];
2371        input.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100));
2372        input.push(b'm');
2373
2374        let _ = parser.parse(&input);
2375
2376        // Parser must still be usable.
2377        let events = parser.parse(b"x");
2378        assert_eq!(events.len(), 1);
2379        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('x')));
2380    }
2381
2382    /// Oversized OSC sequence triggers DoS protection without panic.
2383    #[test]
2384    fn oversized_osc_transitions_to_ignore() {
2385        let mut parser = InputParser::new();
2386
2387        // OSC followed by MAX_OSC_LEN+100 content bytes then ST.
2388        let mut input = vec![0x1B, b']'];
2389        input.extend(std::iter::repeat_n(b'a', MAX_OSC_LEN + 100));
2390        input.push(0x07); // BEL terminator
2391
2392        let _ = parser.parse(&input);
2393
2394        // Parser must still be usable.
2395        let events = parser.parse(b"y");
2396        assert_eq!(events.len(), 1);
2397        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('y')));
2398    }
2399
2400    /// Rapid ESC toggling doesn't corrupt state.
2401    #[test]
2402    fn rapid_esc_toggle() {
2403        let mut parser = InputParser::new();
2404
2405        // 1000 bare ESCs in a row.
2406        let input: Vec<u8> = vec![0x1B; 1000];
2407        let _ = parser.parse(&input);
2408
2409        // Must recover for a normal key.
2410        let events = parser.parse(b"k");
2411        assert!(!events.is_empty());
2412    }
2413
2414    /// Interleaved paste start sequences without end.
2415    #[test]
2416    fn unterminated_paste_recovery() {
2417        let mut parser = InputParser::new();
2418
2419        // Start paste, but never end it — feed lots of data.
2420        let mut input = b"\x1b[200~".to_vec();
2421        input.extend(std::iter::repeat_n(b'x', 2048));
2422
2423        let _ = parser.parse(&input);
2424
2425        // Now end the paste.
2426        let events = parser.parse(b"\x1b[201~");
2427        assert!(
2428            !events.is_empty(),
2429            "Parser should emit paste event on terminator"
2430        );
2431    }
2432
2433    /// UTF-8 boundary: all possible lead bytes followed by truncation.
2434    #[test]
2435    fn truncated_utf8_lead_bytes() {
2436        let mut parser = InputParser::new();
2437
2438        // Two-byte lead (0xC0..0xDF), three-byte (0xE0..0xEF), four-byte (0xF0..0xF7)
2439        for lead in [0xC2, 0xE0, 0xF0] {
2440            let _ = parser.parse(&[lead]);
2441            // Feed a normal ASCII after the truncated lead.
2442            let events = parser.parse(b"a");
2443            // Must not panic; 'a' should eventually appear.
2444            let _ = events;
2445        }
2446    }
2447
2448    /// Null bytes mixed with valid input.
2449    #[test]
2450    fn null_bytes_interleaved() {
2451        let mut parser = InputParser::new();
2452
2453        let input = b"\x00A\x00\x1b[A\x00B\x00";
2454        let events = parser.parse(input);
2455        // Should get events for 'A', Up arrow, and 'B' (nulls handled gracefully).
2456        assert!(
2457            events.len() >= 2,
2458            "Expected at least 2 events, got {}",
2459            events.len()
2460        );
2461    }
2462
2463    // ── Additional fuzz invariant tests (bd-10i.11.3) ─────────────────
2464
2465    /// Generate an OSC 52 clipboard sequence with arbitrary base64 payload.
2466    fn osc52_sequence() -> impl Strategy<Value = Vec<u8>> {
2467        let selector = prop_oneof![Just(b'c'), Just(b'p'), Just(b's')];
2468        // Generate valid base64 characters with occasional invalid ones
2469        let payload = prop::collection::vec(
2470            prop_oneof![
2471                0x41u8..=0x5A, // A-Z
2472                0x61u8..=0x7A, // a-z
2473                0x30u8..=0x39, // 0-9
2474                Just(b'+'),
2475                Just(b'/'),
2476                Just(b'='),
2477            ],
2478            0..=128,
2479        );
2480        let terminator = prop_oneof![
2481            Just(vec![0x1B, b'\\']), // ESC backslash (ST)
2482            Just(vec![0x07]),        // BEL
2483        ];
2484        (selector, payload, terminator).prop_map(|(sel, pay, term)| {
2485            let mut buf = vec![0x1B, b']', b'5', b'2', b';', sel, b';'];
2486            buf.extend_from_slice(&pay);
2487            buf.extend_from_slice(&term);
2488            buf
2489        })
2490    }
2491
2492    /// Generate an SGR mouse sequence.
2493    fn sgr_mouse_sequence() -> impl Strategy<Value = Vec<u8>> {
2494        let button_code = 0u16..128;
2495        let x = 1u16..300;
2496        let y = 1u16..100;
2497        let final_byte = prop_oneof![Just(b'M'), Just(b'm')];
2498        (button_code, x, y, final_byte)
2499            .prop_map(|(btn, x, y, fb)| format!("\x1b[<{btn};{x};{y}{}", fb as char).into_bytes())
2500    }
2501
2502    /// Generate Kitty keyboard protocol sequences.
2503    fn kitty_keyboard_sequence() -> impl Strategy<Value = Vec<u8>> {
2504        let keycode = prop_oneof![
2505            0x20u32..0x7F,       // ASCII range
2506            0x57344u32..0x57400, // Kitty special keys
2507            0x100u32..0x200,     // Extended range
2508        ];
2509        let modifier = 1u32..16;
2510        let kind = prop_oneof![Just(1u32), Just(2u32), Just(3u32)]; // press/repeat/release
2511        (keycode, prop::option::of(modifier), prop::option::of(kind)).prop_map(
2512            |(kc, mods, kind)| match (mods, kind) {
2513                (Some(m), Some(k)) => format!("\x1b[{kc};{m}:{k}u").into_bytes(),
2514                (Some(m), None) => format!("\x1b[{kc};{m}u").into_bytes(),
2515                _ => format!("\x1b[{kc}u").into_bytes(),
2516            },
2517        )
2518    }
2519
2520    proptest! {
2521        // --- OSC 52 clipboard tests ---
2522
2523        /// OSC 52 clipboard sequences never panic.
2524        #[test]
2525        fn osc52_never_panics(seq in osc52_sequence()) {
2526            let mut parser = InputParser::new();
2527            let events = parser.parse(&seq);
2528            // If parsed, should be a Clipboard event
2529            for event in &events {
2530                if let Event::Clipboard(c) = event {
2531                    prop_assert!(!c.content.is_empty() || c.content.is_empty(),
2532                        "Clipboard event must have a content field");
2533                }
2534            }
2535        }
2536
2537        /// OSC 52 with corrupt base64 doesn't panic.
2538        #[test]
2539        fn osc52_corrupt_base64_safe(payload in arb_byte_vec(128)) {
2540            let mut parser = InputParser::new();
2541            let mut input = b"\x1b]52;c;".to_vec();
2542            input.extend_from_slice(&payload);
2543            input.push(0x07); // BEL terminator
2544            let _ = parser.parse(&input);
2545        }
2546
2547        // --- SGR mouse tests ---
2548
2549        /// All SGR mouse sequences parse without panicking.
2550        #[test]
2551        fn sgr_mouse_never_panics(seq in sgr_mouse_sequence()) {
2552            let mut parser = InputParser::new();
2553            let events = parser.parse(&seq);
2554            for event in &events {
2555                // Verify events are well-formed (exercises Debug impl)
2556                let _ = format!("{event:?}");
2557            }
2558        }
2559
2560        /// SGR mouse with extreme coordinates doesn't overflow.
2561        #[test]
2562        fn sgr_mouse_extreme_coords(
2563            btn in 0u16..128,
2564            x in 0u16..=65535,
2565            y in 0u16..=65535,
2566        ) {
2567            let mut parser = InputParser::new();
2568            let input = format!("\x1b[<{btn};{x};{y}M").into_bytes();
2569            let events = parser.parse(&input);
2570            for event in &events {
2571                if let Event::Mouse(m) = event {
2572                    prop_assert!(m.x <= x, "Mouse x {} > input x {}", m.x, x);
2573                    prop_assert!(m.y <= y, "Mouse y {} > input y {}", m.y, y);
2574                }
2575            }
2576        }
2577
2578        // --- Kitty keyboard protocol tests ---
2579
2580        /// Kitty keyboard sequences never panic.
2581        #[test]
2582        fn kitty_keyboard_never_panics(seq in kitty_keyboard_sequence()) {
2583            let mut parser = InputParser::new();
2584            let _ = parser.parse(&seq);
2585        }
2586
2587        // --- State boundary tests ---
2588
2589        /// Truncated CSI followed by new valid sequence works correctly.
2590        #[test]
2591        fn truncated_csi_then_valid(
2592            params in prop::collection::vec(0x30u8..=0x3F, 1..=10),
2593            valid_char in 0x20u8..0x7F,
2594        ) {
2595            let mut parser = InputParser::new();
2596
2597            // Send truncated CSI (no final byte)
2598            let mut partial = vec![0x1B, b'['];
2599            partial.extend_from_slice(&params);
2600            let _ = parser.parse(&partial);
2601
2602            // Now send a fresh ESC sequence that should reset state
2603            let events = parser.parse(&[0x1B, b'[', b'A']); // Up arrow
2604            // Parser should eventually emit events (possibly including
2605            // interpretation of partial as complete)
2606            let _ = events;
2607
2608            // Verify recovery with a simple key
2609            let events = parser.parse(&[valid_char]);
2610            let _ = events;
2611        }
2612
2613        /// Truncated OSC followed by new valid sequence works.
2614        #[test]
2615        fn truncated_osc_then_valid(
2616            content in prop::collection::vec(0x20u8..=0x7E, 1..=32),
2617        ) {
2618            let mut parser = InputParser::new();
2619
2620            // Send unterminated OSC
2621            let mut partial = vec![0x1B, b']'];
2622            partial.extend_from_slice(&content);
2623            let _ = parser.parse(&partial);
2624
2625            // Send a new ESC to interrupt, then a valid key
2626            let events = parser.parse(b"\x1bz");
2627            let _ = events;
2628        }
2629
2630        // --- Near-limit tests ---
2631
2632        /// CSI sequence just under MAX_CSI_LEN produces events.
2633        #[test]
2634        fn csi_near_limit_produces_event(
2635            fill_byte in 0x30u8..=0x39, // digit parameter bytes
2636        ) {
2637            let mut parser = InputParser::new();
2638
2639            let mut input = vec![0x1B, b'['];
2640            // Fill to just under limit
2641            input.extend(std::iter::repeat_n(fill_byte, MAX_CSI_LEN - 1));
2642            input.push(b'm'); // final byte (SGR)
2643
2644            let events = parser.parse(&input);
2645            // Should NOT have been ignored (under limit)
2646            // The sequence is valid structurally even if params are nonsensical
2647            let _ = events;
2648
2649            // Parser should still work
2650            let events = parser.parse(b"a");
2651            prop_assert!(!events.is_empty(), "Parser stuck after near-limit CSI");
2652        }
2653
2654        /// OSC sequence just under MAX_OSC_LEN still processes.
2655        #[test]
2656        fn osc_near_limit_processes(
2657            fill_byte in 0x20u8..=0x7E,
2658        ) {
2659            let mut parser = InputParser::new();
2660
2661            let mut input = vec![0x1B, b']'];
2662            input.extend(std::iter::repeat_n(fill_byte, MAX_OSC_LEN - 1));
2663            input.push(0x07); // BEL terminator
2664
2665            let _ = parser.parse(&input);
2666
2667            // Parser should still work
2668            let events = parser.parse(b"b");
2669            prop_assert!(!events.is_empty(), "Parser stuck after near-limit OSC");
2670        }
2671
2672        // --- Consecutive paste tests ---
2673
2674        /// Multiple back-to-back paste sequences all emit events.
2675        #[test]
2676        fn consecutive_pastes_emit_events(count in 2usize..=5) {
2677            let mut parser = InputParser::new();
2678            let mut input = Vec::new();
2679
2680            for i in 0..count {
2681                input.extend_from_slice(b"\x1b[200~");
2682                input.extend_from_slice(format!("paste_{i}").as_bytes());
2683                input.extend_from_slice(b"\x1b[201~");
2684            }
2685
2686            let events = parser.parse(&input);
2687            let paste_events: Vec<_> = events.iter()
2688                .filter(|e| matches!(e, Event::Paste(_)))
2689                .collect();
2690
2691            prop_assert_eq!(paste_events.len(), count,
2692                "Expected {} paste events, got {}", count, paste_events.len());
2693        }
2694
2695        /// Paste with invalid UTF-8 bytes doesn't panic.
2696        #[test]
2697        fn paste_with_invalid_utf8(content in arb_byte_vec(256)) {
2698            let mut parser = InputParser::new();
2699            let mut input = b"\x1b[200~".to_vec();
2700            input.extend_from_slice(&content);
2701            input.extend_from_slice(b"\x1b[201~");
2702
2703            let events = parser.parse(&input);
2704            for event in &events {
2705                if let Event::Paste(p) = event {
2706                    // Text should be valid UTF-8 (lossy conversion happens internally)
2707                    prop_assert!(p.text.is_char_boundary(0), "Paste text is not valid UTF-8");
2708                }
2709            }
2710        }
2711
2712        // --- Recovery invariants ---
2713
2714        /// After any arbitrary input, feeding ESC then a known key recovers.
2715        #[test]
2716        fn recovery_via_esc_reset(garbage in arb_byte_vec(256)) {
2717            let mut parser = InputParser::new();
2718            let _ = parser.parse(&garbage);
2719
2720            // Terminate any pending OSC (BEL works from any OSC sub-state),
2721            // then ESC to flush any other intermediate state.
2722            let _ = parser.parse(b"\x07\x1b\\\x1b");
2723            let _ = parser.parse(b"\x1b");
2724
2725            // Now feed a clean character.
2726            let _ = parser.parse(b"z");
2727
2728            // Feed one more clean character to verify.
2729            let events = parser.parse(b"q");
2730            // After terminating all pending sequences and feeding clean input,
2731            // the parser must produce events.
2732            prop_assert!(!events.is_empty(),
2733                "Parser did not recover after garbage + reset");
2734        }
2735    }
2736}