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// Import tracing macros (no-op when tracing feature is disabled).
32#[cfg(feature = "tracing")]
33use crate::logging::{debug, debug_span, trace};
34#[cfg(not(feature = "tracing"))]
35use crate::{debug, debug_span, trace};
36
37/// DoS protection: maximum CSI sequence length.
38const MAX_CSI_LEN: usize = 256;
39
40/// DoS protection: maximum OSC sequence length.
41const MAX_OSC_LEN: usize = 102_400;
42
43/// DoS protection: maximum paste content length.
44const MAX_PASTE_LEN: usize = 1024 * 1024; // 1MB
45/// Upper bound for event vector preallocation hints.
46///
47/// Keep this bounded so callers passing very large slices do not cause
48/// disproportionate reserve spikes.
49const MAX_EVENT_RESERVE_HINT: usize = 8 * 1024 + 1;
50
51/// Parser state machine states.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53enum ParserState {
54    /// Normal character input.
55    #[default]
56    Ground,
57    /// After ESC (0x1B).
58    Escape,
59    /// After ESC [ (CSI introducer).
60    Csi,
61    /// Collecting CSI parameters.
62    CsiParam,
63    /// Ignoring oversized CSI sequence.
64    CsiIgnore,
65    /// After ESC O (SS3 introducer).
66    Ss3,
67    /// After ESC ] (OSC introducer).
68    Osc,
69    /// Collecting OSC content.
70    OscContent,
71    /// After ESC inside OSC (for ESC \ terminator).
72    OscEscape,
73    /// Ignoring oversized OSC sequence.
74    OscIgnore,
75    /// Inside a DCS (`ESC P …`) control string, which we consume and discard
76    /// until its String Terminator. DCS carries terminal query *responses*
77    /// (XTGETTCAP capability reports, DECRQSS status strings), not key input;
78    /// decoding its bytes as keys would inject garbage (e.g. an XTGETTCAP reply
79    /// that leaks into the input stream on a slow link).
80    DcsIgnore,
81    /// After an ESC inside a DCS string — checking for the `ESC \` (ST)
82    /// terminator.
83    DcsEscape,
84    /// Collecting UTF-8 multi-byte sequence.
85    Utf8 {
86        /// Bytes collected so far.
87        collected: u8,
88        /// Total bytes expected.
89        expected: u8,
90    },
91    /// Collecting X10 mouse coordinates (3 bytes).
92    MouseX10 { collected: u8, buffer: [u8; 3] },
93}
94
95/// Terminal input parser with DoS protection.
96///
97/// Parse terminal input bytes into events:
98///
99/// ```ignore
100/// let mut parser = InputParser::new();
101/// let events = parser.parse(b"\x1b[A"); // Up arrow
102/// assert_eq!(events.len(), 1);
103/// ```
104#[derive(Debug)]
105pub struct InputParser {
106    /// Current parser state.
107    state: ParserState,
108    /// Buffer for accumulating sequence bytes.
109    buffer: Vec<u8>,
110    /// Buffer for collecting paste content.
111    paste_buffer: Vec<u8>,
112    /// UTF-8 bytes collected so far.
113    utf8_buffer: [u8; 4],
114    /// Whether we're in bracketed paste mode.
115    in_paste: bool,
116    /// Event queued for the next iteration (allows emitting 2 events per byte).
117    pending_event: Option<Event>,
118    /// Whether to expect X10-encoded mouse events (`CSI M cb cx cy`).
119    ///
120    /// In practice some terminals/muxes can fall back to raw X10 packets even
121    /// after SGR negotiation. This flag should track whether mouse capture is
122    /// active for the current session.
123    ///
124    /// Defaults to `false`. When false, bare `CSI M` is treated as an unknown
125    /// CSI sequence (silently ignored) rather than entering X10 decode mode.
126    expect_x10_mouse: bool,
127    /// Whether to accept legacy xterm/rxvt mouse packets (`CSI Cb;Cx;Cy M`).
128    ///
129    /// Some terminals/muxes may ignore SGR mode requests and continue emitting
130    /// legacy numeric mouse packets. This flag enables that fallback parser
131    /// while keeping raw X10 byte-triplet decoding separately gated by
132    /// `expect_x10_mouse`.
133    allow_legacy_mouse: bool,
134}
135
136impl Default for InputParser {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl InputParser {
143    #[inline]
144    fn event_reserve_hint(input_len: usize) -> usize {
145        input_len.saturating_add(1).min(MAX_EVENT_RESERVE_HINT)
146    }
147
148    /// Create a new input parser.
149    #[must_use]
150    pub fn new() -> Self {
151        Self {
152            state: ParserState::Ground,
153            buffer: Vec::with_capacity(64),
154            paste_buffer: Vec::new(),
155            utf8_buffer: [0; 4],
156            in_paste: false,
157            pending_event: None,
158            expect_x10_mouse: false,
159            allow_legacy_mouse: false,
160        }
161    }
162
163    /// Enable or disable X10 mouse event parsing.
164    ///
165    /// When enabled, bare `CSI M` triggers X10 coordinate collection
166    /// (3 raw bytes). This should generally follow mouse-capture state.
167    pub fn set_expect_x10_mouse(&mut self, enabled: bool) {
168        self.expect_x10_mouse = enabled;
169    }
170
171    /// Enable or disable legacy numeric mouse fallback parsing.
172    ///
173    /// When enabled, parse `CSI Cb;Cx;Cy M` as mouse input. This is useful when
174    /// mouse capture is active but the terminal does not honor SGR 1006 mode.
175    ///
176    /// Default: `false`.
177    pub fn set_allow_legacy_mouse(&mut self, enabled: bool) {
178        self.allow_legacy_mouse = enabled;
179    }
180
181    /// Whether the parser is currently waiting on additional bytes for a
182    /// timeout-resolved sequence (bare ESC or partial UTF-8).
183    #[must_use]
184    pub const fn has_pending_timeout_state(&self) -> bool {
185        matches!(self.state, ParserState::Escape | ParserState::Utf8 { .. })
186    }
187
188    /// Handle a timeout in the input stream.
189    ///
190    /// If the parser is waiting for more bytes to complete an ambiguous sequence
191    /// (specifically a bare ESC), a timeout indicates the sequence has ended.
192    pub fn timeout(&mut self) -> Option<Event> {
193        match self.state {
194            ParserState::Escape => {
195                self.state = ParserState::Ground;
196                Some(Event::Key(KeyEvent::new(KeyCode::Escape)))
197            }
198            ParserState::Utf8 { .. } => {
199                // Incomplete UTF-8 sequence at timeout -> replacement char
200                self.state = ParserState::Ground;
201                self.utf8_buffer = [0; 4];
202                Some(Event::Key(KeyEvent::new(KeyCode::Char(
203                    std::char::REPLACEMENT_CHARACTER,
204                ))))
205            }
206            _ => None,
207        }
208    }
209
210    /// Parse input bytes and return any completed events.
211    pub fn parse(&mut self, input: &[u8]) -> Vec<Event> {
212        let mut events = Vec::with_capacity(Self::event_reserve_hint(input.len()));
213        self.parse_with(input, |event| events.push(event));
214        events
215    }
216
217    /// Parse input bytes and emit each completed event through `emit`.
218    pub fn parse_with<F>(&mut self, input: &[u8], mut emit: F)
219    where
220        F: FnMut(Event),
221    {
222        let span = debug_span!("event.normalize", raw_byte_count = input.len());
223        let _guard = span.enter();
224        trace!("raw input bytes: {} bytes", input.len());
225
226        for &byte in input {
227            if let Some(event) = self.process_byte(byte) {
228                debug!(event_type = event.event_type_label(), "normalized event");
229                emit(event);
230            }
231            if let Some(pending) = self.pending_event.take() {
232                debug!(event_type = pending.event_type_label(), "normalized event");
233                emit(pending);
234            }
235        }
236    }
237
238    /// Parse input bytes and append completed events to `events`.
239    ///
240    /// This variant lets callers reuse a scratch buffer across parses to avoid
241    /// repeated allocations on hot input paths.
242    pub fn parse_into(&mut self, input: &[u8], events: &mut Vec<Event>) {
243        let needed = Self::event_reserve_hint(input.len());
244        let available = events.capacity().saturating_sub(events.len());
245        if available < needed {
246            events.reserve(needed - available);
247        }
248        self.parse_with(input, |event| events.push(event));
249    }
250
251    /// Process a single byte and optionally return an event.
252    fn process_byte(&mut self, byte: u8) -> Option<Event> {
253        // In paste mode, collect bytes until end sequence
254        if self.in_paste {
255            return self.process_paste_byte(byte);
256        }
257
258        match self.state {
259            ParserState::Ground => self.process_ground(byte),
260            ParserState::Escape => self.process_escape(byte),
261            ParserState::Csi => self.process_csi(byte),
262            ParserState::CsiParam => self.process_csi_param(byte),
263            ParserState::CsiIgnore => self.process_csi_ignore(byte),
264            ParserState::Ss3 => self.process_ss3(byte),
265            ParserState::Osc => self.process_osc(byte),
266            ParserState::OscContent => self.process_osc_content(byte),
267            ParserState::OscEscape => self.process_osc_escape(byte),
268            ParserState::OscIgnore => self.process_osc_ignore(byte),
269            ParserState::DcsIgnore => self.process_dcs_ignore(byte),
270            ParserState::DcsEscape => self.process_dcs_escape(byte),
271            ParserState::Utf8 {
272                collected,
273                expected,
274            } => self.process_utf8(byte, collected, expected),
275            ParserState::MouseX10 { .. } => self.process_mouse_x10(byte),
276        }
277    }
278
279    /// Process byte in ground state.
280    fn process_ground(&mut self, byte: u8) -> Option<Event> {
281        match byte {
282            // ESC - start escape sequence
283            0x1B => {
284                self.state = ParserState::Escape;
285                None
286            }
287            // C1 CSI (S8C1T): start CSI sequence without ESC prefix.
288            0x9B => {
289                self.state = ParserState::Csi;
290                self.buffer.clear();
291                None
292            }
293            // C1 SS3: start SS3 sequence without ESC prefix.
294            0x8F => {
295                self.state = ParserState::Ss3;
296                None
297            }
298            // C1 OSC: start OSC sequence without ESC prefix.
299            0x9D => {
300                self.state = ParserState::Osc;
301                self.buffer.clear();
302                None
303            }
304            // NUL - Ctrl+Space or Ctrl+@
305            0x00 => Some(Event::Key(KeyEvent::new(KeyCode::Null))),
306            // Backspace alternate (Ctrl+H)
307            0x08 => Some(Event::Key(KeyEvent::new(KeyCode::Backspace))),
308            // Tab (Ctrl+I) - check before generic Ctrl range
309            0x09 => Some(Event::Key(KeyEvent::new(KeyCode::Tab))),
310            // Enter (Ctrl+M) - check before generic Ctrl range
311            0x0D => Some(Event::Key(KeyEvent::new(KeyCode::Enter))),
312            // Other Ctrl+A through Ctrl+Z (0x01-0x1A excluding Tab and Enter)
313            0x01..=0x07 | 0x0A..=0x0C | 0x0E..=0x1A => {
314                let c = (byte + b'a' - 1) as char;
315                Some(Event::Key(
316                    KeyEvent::new(KeyCode::Char(c)).with_modifiers(Modifiers::CTRL),
317                ))
318            }
319            // Ctrl+\, Ctrl+], Ctrl+^, Ctrl+_ (0x1C-0x1F)
320            0x1C => Some(Event::Key(
321                KeyEvent::new(KeyCode::Char('\\')).with_modifiers(Modifiers::CTRL),
322            )),
323            0x1D => Some(Event::Key(
324                KeyEvent::new(KeyCode::Char(']')).with_modifiers(Modifiers::CTRL),
325            )),
326            0x1E => Some(Event::Key(
327                KeyEvent::new(KeyCode::Char('^')).with_modifiers(Modifiers::CTRL),
328            )),
329            0x1F => Some(Event::Key(
330                KeyEvent::new(KeyCode::Char('_')).with_modifiers(Modifiers::CTRL),
331            )),
332            // Backspace (DEL)
333            0x7F => Some(Event::Key(KeyEvent::new(KeyCode::Backspace))),
334            // Printable ASCII
335            0x20..=0x7E => Some(Event::Key(KeyEvent::new(KeyCode::Char(byte as char)))),
336            // UTF-8 lead bytes (valid ranges only)
337            0xC2..=0xDF => {
338                self.utf8_buffer[0] = byte;
339                self.state = ParserState::Utf8 {
340                    collected: 1,
341                    expected: 2,
342                };
343                None
344            }
345            0xE0..=0xEF => {
346                self.utf8_buffer[0] = byte;
347                self.state = ParserState::Utf8 {
348                    collected: 1,
349                    expected: 3,
350                };
351                None
352            }
353            0xF0..=0xF4 => {
354                self.utf8_buffer[0] = byte;
355                self.state = ParserState::Utf8 {
356                    collected: 1,
357                    expected: 4,
358                };
359                None
360            }
361            // Invalid UTF-8 lead bytes (overlong or out of range)
362            0xC0..=0xC1 | 0xF5..=0xFF => Some(Event::Key(KeyEvent::new(KeyCode::Char(
363                std::char::REPLACEMENT_CHARACTER,
364            )))),
365            // Invalid or ignored bytes
366            _ => None,
367        }
368    }
369
370    /// Process byte after ESC.
371    fn process_escape(&mut self, byte: u8) -> Option<Event> {
372        match byte {
373            // CSI introducer
374            b'[' => {
375                self.state = ParserState::Csi;
376                self.buffer.clear();
377                None
378            }
379            // SS3 introducer
380            b'O' => {
381                self.state = ParserState::Ss3;
382                None
383            }
384            // OSC introducer
385            b']' => {
386                self.state = ParserState::Osc;
387                self.buffer.clear();
388                None
389            }
390            // DCS introducer (ESC P). DCS is how terminals return string-valued
391            // query responses — XTGETTCAP capability reports and DECRQSS status
392            // strings — so we consume and discard the whole `ESC P … ST` string
393            // rather than decoding it as keys. Without this, a leaked XTGETTCAP
394            // reply (`ESC P 1+r524742=8/8/8 ESC \`) would decode as `Alt+P`
395            // followed by its payload as literal keystrokes
396            // (`1 + r 5 2 4 7 4 2 = 8 / 8 / 8`, `Alt+\`).
397            //
398            // This shadows the legacy `Alt+Shift+P` encoding (which also sends
399            // `ESC P` under metaSendsEscape) — an unavoidable, standard ambiguity
400            // (DCS wins, exactly as `ESC [`/`ESC ]`/`ESC O` already shadow
401            // `Alt+[`/`Alt+]`/`Alt+Shift+O`). We deliberately do NOT intercept
402            // the sibling C1 string introducers SOS (`ESC X`), PM (`ESC ^`) or
403            // APC (`ESC _`): terminals essentially never send those as responses,
404            // so shadowing them would needlessly swallow `Alt+Shift+X`/`Alt+^`/
405            // `Alt+_` keypresses for no benefit. (8-bit C1 DCS `0x90` is likewise
406            // not handled — modern UTF-8 terminals use the 7-bit form above.)
407            b'P' => {
408                self.state = ParserState::DcsIgnore;
409                self.buffer.clear();
410                None
411            }
412            // Another ESC - emit Alt+Escape and reset to ground
413            // (or treat as start of new sequence - but ESC ESC is usually Alt+ESC)
414            0x1B => {
415                self.state = ParserState::Ground;
416                Some(Event::Key(
417                    KeyEvent::new(KeyCode::Escape).with_modifiers(Modifiers::ALT),
418                ))
419            }
420            // Control characters (Ctrl+Key) -> Alt+Ctrl+Key
421            0x00..=0x1F => {
422                self.state = ParserState::Ground;
423                // Delegate to process_ground to decode the control key (e.g. 0x01 -> Ctrl+A)
424                // then add the ALT modifier.
425                if let Some(mut event) = self.process_ground(byte) {
426                    if let Event::Key(ref mut key) = event {
427                        key.modifiers |= Modifiers::ALT;
428                    }
429                    Some(event)
430                } else {
431                    None
432                }
433            }
434            // Alt+letter or Alt+char
435            0x20..=0x7E => {
436                self.state = ParserState::Ground;
437                Some(Event::Key(
438                    KeyEvent::new(KeyCode::Char(byte as char)).with_modifiers(Modifiers::ALT),
439                ))
440            }
441            // Alt+Backspace (DEL)
442            0x7F => {
443                self.state = ParserState::Ground;
444                Some(Event::Key(
445                    KeyEvent::new(KeyCode::Backspace).with_modifiers(Modifiers::ALT),
446                ))
447            }
448            // Invalid - return to ground
449            _ => {
450                self.state = ParserState::Ground;
451                None
452            }
453        }
454    }
455
456    /// Process byte at start of CSI sequence.
457    fn process_csi(&mut self, byte: u8) -> Option<Event> {
458        // Robustness: ESC restarts sequence
459        if byte == 0x1B {
460            self.state = ParserState::Escape;
461            self.buffer.clear();
462            return None;
463        }
464
465        self.buffer.push(byte);
466
467        match byte {
468            // Parameter bytes (0x30-0x3F) and Intermediate bytes (0x20-0x2F)
469            0x20..=0x3F => {
470                self.state = ParserState::CsiParam;
471                None
472            }
473            // Final byte (0x40-0x7E) - parse and return
474            0x40..=0x7E => {
475                // X10 mouse trigger: bare `CSI M` enters raw X10 coordinate
476                // collection only when the runtime currently expects possible
477                // X10 fallback traffic.
478                if self.expect_x10_mouse && byte == b'M' && self.buffer.len() == 1 {
479                    self.state = ParserState::MouseX10 {
480                        collected: 0,
481                        buffer: [0; 3],
482                    };
483                    self.buffer.clear();
484                    return None;
485                }
486
487                self.state = ParserState::Ground;
488                self.parse_csi_sequence()
489            }
490            // Invalid (0x00-0x1F, 0x7F-0xFF)
491            _ => {
492                self.state = ParserState::Ground;
493                self.buffer.clear();
494                None
495            }
496        }
497    }
498
499    /// Process byte while collecting CSI parameters.
500    fn process_csi_param(&mut self, byte: u8) -> Option<Event> {
501        // Robustness: ESC restarts sequence
502        if byte == 0x1B {
503            self.state = ParserState::Escape;
504            self.buffer.clear();
505            return None;
506        }
507
508        // DoS protection
509        if self.buffer.len() >= MAX_CSI_LEN {
510            self.state = ParserState::CsiIgnore;
511            self.buffer.clear();
512            return None;
513        }
514
515        self.buffer.push(byte);
516
517        match byte {
518            // Continue collecting parameters/intermediates
519            0x20..=0x3F => None,
520            // Final byte - parse and return
521            0x40..=0x7E => {
522                self.state = ParserState::Ground;
523                self.parse_csi_sequence()
524            }
525            // Invalid
526            _ => {
527                self.state = ParserState::Ground;
528                self.buffer.clear();
529                self.process_ground(byte)
530            }
531        }
532    }
533
534    /// Ignore bytes until end of CSI sequence.
535    fn process_csi_ignore(&mut self, byte: u8) -> Option<Event> {
536        // Robustness: ESC restarts sequence
537        if byte == 0x1B {
538            self.state = ParserState::Escape;
539            return None;
540        }
541
542        // Final byte (0x40-0x7E) - return to ground
543        if (0x40..=0x7E).contains(&byte) {
544            self.state = ParserState::Ground;
545            None
546        } else if (0x20..=0x3F).contains(&byte) {
547            // Parameter/Intermediate bytes - continue ignoring
548            None
549        } else {
550            // Invalid character (e.g. newline) - abort sequence and reprocess
551            self.state = ParserState::Ground;
552            self.process_ground(byte)
553        }
554    }
555
556    /// Parse a complete CSI sequence from the buffer.
557    fn parse_csi_sequence(&mut self) -> Option<Event> {
558        let seq = std::mem::take(&mut self.buffer);
559        if seq.is_empty() {
560            return None;
561        }
562
563        let final_byte = *seq.last()?;
564        let params = &seq[..seq.len() - 1];
565
566        // Check for special sequences first
567        match (params, final_byte) {
568            // Focus events
569            ([], b'I') => return Some(Event::Focus(true)),
570            ([], b'O') => return Some(Event::Focus(false)),
571
572            // Bracketed paste
573            (b"200", b'~') => {
574                self.in_paste = true;
575                self.paste_buffer.clear();
576                self.buffer.clear(); // Ensure tail buffer is clean
577                return None;
578            }
579            (b"201", b'~') => {
580                self.in_paste = false;
581                let content = String::from_utf8_lossy(&self.paste_buffer).into_owned();
582                self.paste_buffer.clear();
583                return Some(Event::Paste(PasteEvent::bracketed(content)));
584            }
585
586            // SGR mouse protocol
587            _ if params.starts_with(b"<") && (final_byte == b'M' || final_byte == b'm') => {
588                return self.parse_sgr_mouse(params, final_byte);
589            }
590            // Legacy mouse protocol fallback (xterm/rxvt 1015):
591            // CSI Cb ; Cx ; Cy M
592            //
593            // Gate this behind explicit mouse fallback toggles so we don't
594            // reinterpret generic CSI ... M sequences as mouse input when
595            // mouse capture is off.
596            _ if (self.allow_legacy_mouse || self.expect_x10_mouse) && final_byte == b'M' => {
597                if let Some(event) = self.parse_legacy_mouse(params) {
598                    return Some(event);
599                }
600            }
601
602            _ => {}
603        }
604
605        // Arrow keys and other CSI sequences
606        match final_byte {
607            b'A' => Some(Event::Key(self.key_with_modifiers(KeyCode::Up, params))),
608            b'B' => Some(Event::Key(self.key_with_modifiers(KeyCode::Down, params))),
609            b'C' => Some(Event::Key(self.key_with_modifiers(KeyCode::Right, params))),
610            b'D' => Some(Event::Key(self.key_with_modifiers(KeyCode::Left, params))),
611            b'H' => Some(Event::Key(self.key_with_modifiers(KeyCode::Home, params))),
612            b'F' => Some(Event::Key(self.key_with_modifiers(KeyCode::End, params))),
613            b'P' => Some(Event::Key(self.key_with_modifiers(KeyCode::F(1), params))),
614            b'Q' => Some(Event::Key(self.key_with_modifiers(KeyCode::F(2), params))),
615            b'R' => Some(Event::Key(self.key_with_modifiers(KeyCode::F(3), params))),
616            b'S' => Some(Event::Key(self.key_with_modifiers(KeyCode::F(4), params))),
617            b'Z' => Some(Event::Key(
618                self.key_with_modifiers(KeyCode::BackTab, params),
619            )),
620            b'~' => self.parse_csi_tilde(params),
621            b'u' => self.parse_kitty_keyboard(params),
622            _ => None,
623        }
624    }
625
626    /// Parse CSI sequences ending in ~.
627    fn parse_csi_tilde(&self, params: &[u8]) -> Option<Event> {
628        let num = self.parse_first_param(params)?;
629        let mods = self.parse_modifier_param(params);
630
631        let code = match num {
632            1 => KeyCode::Home,
633            2 => KeyCode::Insert,
634            3 => KeyCode::Delete,
635            4 => KeyCode::End,
636            5 => KeyCode::PageUp,
637            6 => KeyCode::PageDown,
638            15 => KeyCode::F(5),
639            17 => KeyCode::F(6),
640            18 => KeyCode::F(7),
641            19 => KeyCode::F(8),
642            20 => KeyCode::F(9),
643            21 => KeyCode::F(10),
644            23 => KeyCode::F(11),
645            24 => KeyCode::F(12),
646            _ => return None,
647        };
648
649        Some(Event::Key(KeyEvent::new(code).with_modifiers(mods)))
650    }
651
652    /// Parse the first numeric parameter from CSI params.
653    fn parse_first_param(&self, params: &[u8]) -> Option<u32> {
654        let s = std::str::from_utf8(params).ok()?;
655        let first = s.split(';').next()?;
656        first.parse().ok()
657    }
658
659    /// Parse modifier parameter (second param in CSI sequences).
660    fn parse_modifier_param(&self, params: &[u8]) -> Modifiers {
661        let s = match std::str::from_utf8(params) {
662            Ok(s) => s,
663            Err(_) => return Modifiers::NONE,
664        };
665
666        let modifier_value: u32 = s
667            .split(';')
668            .nth(1)
669            .and_then(|s| s.parse().ok())
670            .unwrap_or(1);
671
672        Self::modifiers_from_xterm(modifier_value)
673    }
674
675    /// Parse Kitty keyboard protocol CSI u sequences.
676    ///
677    /// Format: `CSI unicode-key-code:alt-keys ; modifiers:event-type ; text-as-codepoints u`
678    fn parse_kitty_keyboard(&self, params: &[u8]) -> Option<Event> {
679        let s = std::str::from_utf8(params).ok()?;
680        if s.is_empty() {
681            return None;
682        }
683
684        let mut parts = s.split(';');
685        let key_part = parts.next().unwrap_or("");
686        let key_code_str = key_part.split(':').next().unwrap_or("");
687        let key_code: u32 = key_code_str.parse().ok()?;
688
689        let mod_part = parts.next().unwrap_or("");
690        let (modifiers, kind) = Self::kitty_modifiers_and_kind(mod_part);
691
692        let code = Self::kitty_keycode_to_keycode(key_code)?;
693        Some(Event::Key(
694            KeyEvent::new(code)
695                .with_modifiers(modifiers)
696                .with_kind(kind),
697        ))
698    }
699
700    fn kitty_modifiers_and_kind(mod_part: &str) -> (Modifiers, KeyEventKind) {
701        if mod_part.is_empty() {
702            return (Modifiers::NONE, KeyEventKind::Press);
703        }
704
705        let mut parts = mod_part.split(':');
706        let mod_value: u32 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
707        let kind_value: u32 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
708
709        let modifiers = Self::modifiers_from_xterm(mod_value);
710        let kind = match kind_value {
711            2 => KeyEventKind::Repeat,
712            3 => KeyEventKind::Release,
713            _ => KeyEventKind::Press,
714        };
715
716        (modifiers, kind)
717    }
718
719    fn kitty_keycode_to_keycode(key_code: u32) -> Option<KeyCode> {
720        match key_code {
721            // Standard ASCII keys
722            9 => Some(KeyCode::Tab),
723            13 => Some(KeyCode::Enter),
724            27 => Some(KeyCode::Escape),
725            8 | 127 => Some(KeyCode::Backspace),
726            // Kitty keyboard protocol extended keys (CSI u)
727            57_344 => Some(KeyCode::Escape),
728            57_345 => Some(KeyCode::Enter),
729            57_346 => Some(KeyCode::Tab),
730            57_347 => Some(KeyCode::Backspace),
731            57_348 => Some(KeyCode::Insert),
732            57_349 => Some(KeyCode::Delete),
733            57_350 => Some(KeyCode::Left),
734            57_351 => Some(KeyCode::Right),
735            57_352 => Some(KeyCode::Up),
736            57_353 => Some(KeyCode::Down),
737            57_354 => Some(KeyCode::PageUp),
738            57_355 => Some(KeyCode::PageDown),
739            57_356 => Some(KeyCode::Home),
740            57_357 => Some(KeyCode::End),
741            // F1-F24 (57_364-57_387)
742            57_364..=57_387 => {
743                // Safety: range is [57_364, 57_387], so (key_code - 57_364 + 1) is [1, 24]
744                // which fits in u8. We use debug_assert to catch any future range changes.
745                let f_num = key_code - 57_364 + 1;
746                debug_assert!(f_num <= 24, "F-key number {f_num} exceeds F24");
747                Some(KeyCode::F(f_num as u8))
748            }
749            // Reserved/unhandled Kitty keycodes return None
750            57_358..=57_363 | 57_388..=63_743 => None,
751            // Unicode codepoints
752            _ => char::from_u32(key_code).map(KeyCode::Char),
753        }
754    }
755
756    fn modifiers_from_xterm(value: u32) -> Modifiers {
757        // xterm modifier encoding: value = 1 + modifier_bits
758        // Shift=1, Alt=2, Ctrl=4, Super=8
759        let bits = value.saturating_sub(1);
760        let mut mods = Modifiers::NONE;
761        if bits & 1 != 0 {
762            mods |= Modifiers::SHIFT;
763        }
764        if bits & 2 != 0 {
765            mods |= Modifiers::ALT;
766        }
767        if bits & 4 != 0 {
768            mods |= Modifiers::CTRL;
769        }
770        if bits & 8 != 0 {
771            mods |= Modifiers::SUPER;
772        }
773        mods
774    }
775
776    /// Create a key event with modifiers from CSI params.
777    fn key_with_modifiers(&self, code: KeyCode, params: &[u8]) -> KeyEvent {
778        KeyEvent::new(code).with_modifiers(self.parse_modifier_param(params))
779    }
780
781    /// Parse SGR mouse protocol events.
782    fn parse_sgr_mouse(&self, params: &[u8], final_byte: u8) -> Option<Event> {
783        // Format: CSI < button ; x ; y M|m
784        // Skip the leading '<'
785        let params = &params[1..];
786        let s = std::str::from_utf8(params).ok()?;
787        let mut parts = s.split(';');
788
789        // Accept numeric prefixes in each token so sequences with sub-params
790        // (e.g. `10:0`) still decode to their base coordinate/button values.
791        let button_code_u32 = Self::parse_u32_prefix(parts.next()?)?;
792        let button_code = button_code_u32.min(u16::MAX as u32) as u16;
793        let x_raw = Self::parse_i32_prefix(parts.next()?)?;
794        let y_raw = Self::parse_i32_prefix(parts.next()?)?;
795
796        // Decode button and modifiers
797        let (button, mods) = self.decode_mouse_button(button_code);
798
799        let kind = if final_byte == b'M' {
800            if button_code & 64 != 0 {
801                // Scroll event: bit 6 (64) is set
802                // bits 0-1 determine direction: 0=up, 1=down, 2=left, 3=right
803                match button_code & 3 {
804                    0 => MouseEventKind::ScrollUp,
805                    1 => MouseEventKind::ScrollDown,
806                    2 => MouseEventKind::ScrollLeft,
807                    _ => MouseEventKind::ScrollRight,
808                }
809            } else if button_code & 32 != 0 {
810                // Motion event (bit 5 set)
811                // bits 0-1: 0=left, 1=middle, 2=right, 3=no button (moved)
812                if button_code & 3 == 3 {
813                    MouseEventKind::Moved
814                } else {
815                    MouseEventKind::Drag(button)
816                }
817            } else if (button_code & 3) == 3 {
818                // Compatibility: some terminals emit release as uppercase 'M'
819                // with button code 3 instead of lowercase 'm'.
820                MouseEventKind::Up(MouseButton::Left)
821            } else {
822                MouseEventKind::Down(button)
823            }
824        } else {
825            MouseEventKind::Up(button)
826        };
827
828        Some(Event::Mouse(MouseEvent {
829            kind,
830            x: Self::normalize_sgr_coord(x_raw),
831            y: Self::normalize_sgr_coord(y_raw),
832            modifiers: mods,
833        }))
834    }
835
836    #[inline]
837    fn parse_u32_prefix(token: &str) -> Option<u32> {
838        let bytes = token.as_bytes();
839        let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
840        if digits == 0 {
841            return None;
842        }
843        token[..digits].parse().ok()
844    }
845
846    #[inline]
847    fn parse_i32_prefix(token: &str) -> Option<i32> {
848        let bytes = token.as_bytes();
849        if bytes.is_empty() {
850            return None;
851        }
852        let start = if bytes[0] == b'-' || bytes[0] == b'+' {
853            1
854        } else {
855            0
856        };
857        let digits = bytes[start..]
858            .iter()
859            .take_while(|b| b.is_ascii_digit())
860            .count();
861        if digits == 0 {
862            return None;
863        }
864        token[..start + digits].parse().ok()
865    }
866
867    #[inline]
868    fn normalize_sgr_coord(raw: i32) -> u16 {
869        if raw <= 1 {
870            return 0;
871        }
872        let zero_indexed = raw - 1;
873        zero_indexed.min(i32::from(u16::MAX)) as u16
874    }
875
876    /// Parse legacy xterm/rxvt 1015 mouse events: `CSI Cb;Cx;Cy M`.
877    ///
878    /// This acts as a compatibility fallback for terminals that don't emit SGR
879    /// mouse (`CSI < ... M/m`) despite mouse capture being enabled.
880    fn parse_legacy_mouse(&self, params: &[u8]) -> Option<Event> {
881        if params.is_empty() || params.starts_with(b"<") {
882            return None;
883        }
884
885        let s = std::str::from_utf8(params).ok()?;
886        let mut parts = s.split(';');
887        let button_code: u16 = parts.next()?.parse().ok()?;
888        let x: u16 = parts.next()?.parse().ok()?;
889        let y: u16 = parts.next()?.parse().ok()?;
890        // Reject if shape doesn't match exactly Cb;Cx;Cy.
891        if parts.next().is_some() {
892            return None;
893        }
894
895        let (button, mods) = self.decode_mouse_button(button_code);
896        let kind = if button_code & 64 != 0 {
897            // Scroll: bit 6 set, direction in low bits.
898            match button_code & 3 {
899                0 => MouseEventKind::ScrollUp,
900                1 => MouseEventKind::ScrollDown,
901                2 => MouseEventKind::ScrollLeft,
902                _ => MouseEventKind::ScrollRight,
903            }
904        } else if button_code & 32 != 0 {
905            // Motion: bit 5 set.
906            if button_code & 3 == 3 {
907                MouseEventKind::Moved
908            } else {
909                MouseEventKind::Drag(button)
910            }
911        } else if (button_code & 3) == 3 {
912            // Legacy release doesn't identify which button was released.
913            MouseEventKind::Up(MouseButton::Left)
914        } else {
915            MouseEventKind::Down(button)
916        };
917
918        Some(Event::Mouse(MouseEvent {
919            kind,
920            x: x.saturating_sub(1),
921            y: y.saturating_sub(1),
922            modifiers: mods,
923        }))
924    }
925
926    /// Decode mouse button code to button and modifiers.
927    fn decode_mouse_button(&self, code: u16) -> (MouseButton, Modifiers) {
928        let button = match code & 0b11 {
929            0 => MouseButton::Left,
930            1 => MouseButton::Middle,
931            2 => MouseButton::Right,
932            _ => MouseButton::Left,
933        };
934
935        let mut mods = Modifiers::NONE;
936        if code & 4 != 0 {
937            mods |= Modifiers::SHIFT;
938        }
939        if code & 8 != 0 {
940            mods |= Modifiers::ALT;
941        }
942        if code & 16 != 0 {
943            mods |= Modifiers::CTRL;
944        }
945
946        (button, mods)
947    }
948
949    /// Process SS3 (ESC O) sequences.
950    fn process_ss3(&mut self, byte: u8) -> Option<Event> {
951        // Robustness: ESC restarts sequence
952        if byte == 0x1B {
953            self.state = ParserState::Escape;
954            return None;
955        }
956
957        self.state = ParserState::Ground;
958
959        let code = match byte {
960            b'P' => KeyCode::F(1),
961            b'Q' => KeyCode::F(2),
962            b'R' => KeyCode::F(3),
963            b'S' => KeyCode::F(4),
964            b'A' => KeyCode::Up,
965            b'B' => KeyCode::Down,
966            b'C' => KeyCode::Right,
967            b'D' => KeyCode::Left,
968            b'H' => KeyCode::Home,
969            b'F' => KeyCode::End,
970            _ => return None,
971        };
972
973        Some(Event::Key(KeyEvent::new(code)))
974    }
975
976    /// Process OSC start.
977    fn process_osc(&mut self, byte: u8) -> Option<Event> {
978        // Handle ESC as potential ST terminator (ESC \) - don't add to buffer
979        if byte == 0x1B {
980            self.state = ParserState::OscEscape;
981            return None;
982        }
983
984        self.buffer.push(byte);
985
986        match byte {
987            // BEL terminates immediately
988            0x07 => {
989                self.state = ParserState::Ground;
990                self.parse_osc_sequence()
991            }
992            // Continue collecting
993            _ => {
994                self.state = ParserState::OscContent;
995                None
996            }
997        }
998    }
999
1000    /// Process OSC content.
1001    fn process_osc_content(&mut self, byte: u8) -> Option<Event> {
1002        // Handle ESC (0x1B) as potential terminator or reset
1003        if byte == 0x1B {
1004            self.state = ParserState::OscEscape;
1005            return None;
1006        }
1007
1008        // Robustness: Abort on control characters (except BEL) to prevent swallowing logs
1009        if byte < 0x20 && byte != 0x07 {
1010            self.state = ParserState::Ground;
1011            self.buffer.clear();
1012            return self.process_ground(byte);
1013        }
1014
1015        // DoS protection
1016        if self.buffer.len() >= MAX_OSC_LEN {
1017            self.state = ParserState::OscIgnore;
1018            self.buffer.clear();
1019            return None;
1020        }
1021
1022        match byte {
1023            // BEL terminates
1024            0x07 => {
1025                self.state = ParserState::Ground;
1026                self.parse_osc_sequence()
1027            }
1028            // Continue collecting
1029            _ => {
1030                self.buffer.push(byte);
1031                None
1032            }
1033        }
1034    }
1035
1036    /// Process ESC inside OSC (checking for ST terminator).
1037    fn process_osc_escape(&mut self, byte: u8) -> Option<Event> {
1038        if byte == b'\\' {
1039            // ST (String Terminator) found
1040            self.state = ParserState::Ground;
1041            self.parse_osc_sequence()
1042        } else if byte == 0x1B {
1043            // ESC ESC - treat second ESC as start of new sequence (restart)
1044            self.state = ParserState::Escape;
1045            self.buffer.clear();
1046            None
1047        } else {
1048            // ESC followed by something else.
1049            // Strict ANSI would say the OSC is cancelled by the ESC.
1050            // We treat this as a restart of parsing at the *current* byte,
1051            // effectively interpreting the previous ESC as a cancel.
1052
1053            self.buffer.clear();
1054            self.state = ParserState::Escape;
1055            self.process_escape(byte)
1056        }
1057    }
1058
1059    /// Ignore bytes until end of OSC sequence.
1060    fn process_osc_ignore(&mut self, byte: u8) -> Option<Event> {
1061        match byte {
1062            // BEL terminates
1063            0x07 => {
1064                self.state = ParserState::Ground;
1065                None
1066            }
1067            // ESC might start terminator or new sequence
1068            0x1B => {
1069                self.state = ParserState::OscEscape;
1070                None
1071            }
1072            // Abort on control characters to prevent swallowing logs (except DEL 0x7F)
1073            _ if byte < 0x20 => {
1074                self.state = ParserState::Ground;
1075                self.process_ground(byte)
1076            }
1077            // Continue ignoring
1078            _ => None,
1079        }
1080    }
1081
1082    /// Ignore bytes inside a DCS (`ESC P …`) string until its terminator.
1083    ///
1084    /// The content (e.g. an XTGETTCAP reply `1+r524742=8/8/8`) is discarded — a
1085    /// DCS never carries key input. Like [`Self::process_osc_ignore`], a
1086    /// non-ESC/BEL control byte aborts the string and is reprocessed, so a
1087    /// truncated/never-terminated string cannot permanently swallow real input.
1088    fn process_dcs_ignore(&mut self, byte: u8) -> Option<Event> {
1089        match byte {
1090            // BEL terminates (lenient: some terminals close strings with BEL).
1091            0x07 => {
1092                self.state = ParserState::Ground;
1093                None
1094            }
1095            // ESC may begin the ST (ESC \) terminator.
1096            0x1B => {
1097                self.state = ParserState::DcsEscape;
1098                None
1099            }
1100            // Abort on other control characters so a malformed string can't
1101            // swallow subsequent legitimate input (matches OSC-ignore).
1102            _ if byte < 0x20 => {
1103                self.state = ParserState::Ground;
1104                self.process_ground(byte)
1105            }
1106            // Consume DCS payload (hex, '=', '/', etc.).
1107            _ => None,
1108        }
1109    }
1110
1111    /// After an ESC inside a DCS string: complete on `\` (ST) or recover.
1112    fn process_dcs_escape(&mut self, byte: u8) -> Option<Event> {
1113        if byte == b'\\' {
1114            // ST found — the control string is complete and discarded (no event).
1115            self.state = ParserState::Ground;
1116            None
1117        } else if byte == 0x1B {
1118            // ESC ESC — treat the second ESC as the start of a new sequence.
1119            self.state = ParserState::Escape;
1120            None
1121        } else {
1122            // ESC followed by something else cancels the string; reprocess the
1123            // byte as a fresh escape sequence (matches OSC-escape recovery).
1124            self.state = ParserState::Escape;
1125            self.process_escape(byte)
1126        }
1127    }
1128
1129    /// Parse a complete OSC sequence.
1130    fn parse_osc_sequence(&mut self) -> Option<Event> {
1131        let seq = std::mem::take(&mut self.buffer);
1132
1133        // OSC 52 clipboard response: OSC 52 ; c ; <base64> BEL/ST
1134        if seq.starts_with(b"52;") {
1135            return self.parse_osc52_clipboard(&seq);
1136        }
1137
1138        // Other OSC sequences (e.g., OSC 8 hyperlinks) are not parsed as events
1139        None
1140    }
1141
1142    /// Parse OSC 52 clipboard response.
1143    fn parse_osc52_clipboard(&self, seq: &[u8]) -> Option<Event> {
1144        // Format: 52;c;<base64> or 52;p;<base64>
1145        let content = &seq[3..]; // Skip "52;"
1146        if content.is_empty() {
1147            return None;
1148        }
1149
1150        // OSC 52 uses clipboard selectors: c=clipboard, p=primary, s=secondary
1151        // We map all to Osc52 source type since that's how we received it
1152        let source = ClipboardSource::Osc52;
1153
1154        // Skip "c;" prefix
1155        let base64_start = content.iter().position(|&b| b == b';').map(|i| i + 1)?;
1156        let base64_data = &content[base64_start..];
1157
1158        // Decode base64 (simple implementation)
1159        let decoded = self.decode_base64(base64_data)?;
1160
1161        Some(Event::Clipboard(ClipboardEvent::new(
1162            String::from_utf8_lossy(&decoded).into_owned(),
1163            source,
1164        )))
1165    }
1166
1167    /// Simple base64 decoder.
1168    fn decode_base64(&self, input: &[u8]) -> Option<Vec<u8>> {
1169        const DECODE_TABLE: [i8; 256] = {
1170            let mut table = [-1i8; 256];
1171            let mut i = 0u8;
1172            while i < 26 {
1173                table[(b'A' + i) as usize] = i as i8;
1174                table[(b'a' + i) as usize] = (i + 26) as i8;
1175                i += 1;
1176            }
1177            let mut i = 0u8;
1178            while i < 10 {
1179                table[(b'0' + i) as usize] = (i + 52) as i8;
1180                i += 1;
1181            }
1182            table[b'+' as usize] = 62;
1183            table[b'/' as usize] = 63;
1184            table
1185        };
1186
1187        let mut output = Vec::with_capacity(input.len() * 3 / 4);
1188        let mut buffer = 0u32;
1189        let mut bits = 0u8;
1190
1191        for &byte in input {
1192            if byte == b'=' {
1193                break;
1194            }
1195            let value = DECODE_TABLE[byte as usize];
1196            if value < 0 {
1197                continue; // Skip whitespace/invalid
1198            }
1199            buffer = (buffer << 6) | (value as u32);
1200            bits += 6;
1201            if bits >= 8 {
1202                bits -= 8;
1203                output.push((buffer >> bits) as u8);
1204                buffer &= (1 << bits) - 1;
1205            }
1206        }
1207
1208        Some(output)
1209    }
1210
1211    /// Process UTF-8 continuation bytes.
1212    fn process_utf8(&mut self, byte: u8, collected: u8, expected: u8) -> Option<Event> {
1213        // Check for valid continuation byte
1214        if (byte & 0xC0) != 0x80 {
1215            // Invalid - return to ground and re-process the unexpected byte.
1216            // Also emit a replacement character for the invalid sequence we just aborted.
1217            self.state = ParserState::Ground;
1218
1219            // Queue the replacement event for the next iteration of the parse loop
1220            self.pending_event = self.process_ground(byte);
1221
1222            return Some(Event::Key(KeyEvent::new(KeyCode::Char(
1223                std::char::REPLACEMENT_CHARACTER,
1224            ))));
1225        }
1226
1227        self.utf8_buffer[collected as usize] = byte;
1228        let new_collected = collected + 1;
1229
1230        if new_collected == expected {
1231            // Complete - decode and emit
1232            self.state = ParserState::Ground;
1233            match std::str::from_utf8(&self.utf8_buffer[..expected as usize]) {
1234                Ok(s) => {
1235                    let c = s.chars().next()?;
1236                    Some(Event::Key(KeyEvent::new(KeyCode::Char(c))))
1237                }
1238                Err(_) => Some(Event::Key(KeyEvent::new(KeyCode::Char(
1239                    std::char::REPLACEMENT_CHARACTER,
1240                )))),
1241            }
1242        } else {
1243            // Need more bytes
1244            self.state = ParserState::Utf8 {
1245                collected: new_collected,
1246                expected,
1247            };
1248            None
1249        }
1250    }
1251
1252    /// Process bytes while in X10 mouse mode.
1253    fn process_mouse_x10(&mut self, byte: u8) -> Option<Event> {
1254        if let ParserState::MouseX10 {
1255            ref mut collected,
1256            ref mut buffer,
1257        } = self.state
1258        {
1259            buffer[*collected as usize] = byte;
1260            *collected += 1;
1261
1262            if *collected == 3 {
1263                // Copy buffer before reassigning state (borrow of self.state).
1264                let buf = *buffer;
1265                self.state = ParserState::Ground;
1266
1267                // X10 encoding: byte = value + 32.
1268                // Reject malformed packets so noise bytes do not become bogus
1269                // pointer events.
1270                if buf[0] < 32 || buf[1] < 33 || buf[2] < 33 {
1271                    return None;
1272                }
1273                let cb = buf[0].saturating_sub(32) as u16;
1274                let cx = buf[1].saturating_sub(33) as u16; // 1-based -> 0-based
1275                let cy = buf[2].saturating_sub(33) as u16;
1276
1277                let (button, mods) = self.decode_mouse_button(cb);
1278
1279                // Check for release (button 3)
1280                // Note: X10 release doesn't track which button was released,
1281                // so we default to Left for the event kind, or rely on logic downstream.
1282                // However, decode_mouse_button(3) returns Left.
1283                // We check low 2 bits: 0=Btn1, 1=Btn2, 2=Btn3, 3=Release
1284                let kind = if (cb & 3) == 3 {
1285                    // Release event
1286                    MouseEventKind::Up(MouseButton::Left)
1287                } else if cb & 64 != 0 {
1288                    // Scroll event (bit 6 set)
1289                    match cb & 3 {
1290                        0 => MouseEventKind::ScrollUp,
1291                        1 => MouseEventKind::ScrollDown,
1292                        // X10 doesn't support left/right scroll usually
1293                        _ => MouseEventKind::ScrollUp,
1294                    }
1295                } else {
1296                    // Press event
1297                    MouseEventKind::Down(button)
1298                };
1299
1300                return Some(Event::Mouse(MouseEvent {
1301                    kind,
1302                    x: cx,
1303                    y: cy,
1304                    modifiers: mods,
1305                }));
1306            }
1307        }
1308        None
1309    }
1310
1311    /// Process bytes while in paste mode.
1312    fn process_paste_byte(&mut self, byte: u8) -> Option<Event> {
1313        const END_SEQ: &[u8] = b"\x1b[201~";
1314
1315        // Logic:
1316        // 1. If we have room in paste_buffer, push it.
1317        // 2. If we are full, push to self.buffer (used as a tail tracker) to detect END_SEQ.
1318        // 3. Always check if the effective stream ends with END_SEQ.
1319
1320        if self.paste_buffer.len() < MAX_PASTE_LEN {
1321            self.paste_buffer.push(byte);
1322
1323            // Check for end sequence in paste_buffer
1324            if self.paste_buffer.ends_with(END_SEQ) {
1325                self.in_paste = false;
1326                // Remove the end sequence from content
1327                let content_len = self.paste_buffer.len() - END_SEQ.len();
1328                let content =
1329                    String::from_utf8_lossy(&self.paste_buffer[..content_len]).into_owned();
1330                self.paste_buffer.clear();
1331                return Some(Event::Paste(PasteEvent::bracketed(content)));
1332            }
1333        } else {
1334            // Buffer is full. DoS protection active.
1335            // We stop collecting content, but we MUST track the end sequence.
1336            // Use self.buffer as a sliding window for the tail.
1337
1338            self.buffer.push(byte);
1339            if self.buffer.len() > END_SEQ.len() {
1340                self.buffer.remove(0);
1341            }
1342
1343            // Check if we found the end sequence.
1344            // The sequence might be split between paste_buffer and buffer.
1345            // We only need to check the last 6 bytes.
1346            // Since `buffer` contains the most recent bytes (up to 6), and `paste_buffer` is full...
1347
1348            // Construct a view of the last 6 bytes
1349            let mut last_bytes = [0u8; 6];
1350            let tail_len = self.buffer.len();
1351            let paste_len = self.paste_buffer.len();
1352
1353            // Only proceed if we have enough total bytes to form the end sequence
1354            if tail_len + paste_len >= 6 {
1355                // Fill from buffer (reverse order)
1356                for i in 0..tail_len {
1357                    last_bytes[6 - tail_len + i] = self.buffer[i];
1358                }
1359                // Fill remaining from paste_buffer
1360                let remaining = 6 - tail_len;
1361                if remaining > 0 {
1362                    let start = paste_len - remaining;
1363                    last_bytes[..remaining]
1364                        .copy_from_slice(&self.paste_buffer[start..(remaining + start)]);
1365                }
1366
1367                if last_bytes == END_SEQ {
1368                    self.in_paste = false;
1369
1370                    // We found the end sequence.
1371                    // The content is `paste_buffer` MINUS the part of END_SEQ that was in it.
1372                    // `remaining` bytes of END_SEQ were in paste_buffer.
1373
1374                    let content_len = paste_len - remaining;
1375                    let content =
1376                        String::from_utf8_lossy(&self.paste_buffer[..content_len]).into_owned();
1377
1378                    self.paste_buffer.clear();
1379                    self.buffer.clear();
1380
1381                    return Some(Event::Paste(PasteEvent::bracketed(content)));
1382                }
1383            }
1384        }
1385
1386        None
1387    }
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392    use super::*;
1393
1394    #[test]
1395    fn csi_ignore_handles_final_bytes() {
1396        let mut parser = InputParser::new();
1397
1398        // Create a very long CSI sequence terminated by '@' (0x40)
1399        // 0x40 is a valid Final Byte (ECMA-48), but our parser currently only checks A-Za-z~
1400        let mut seq = vec![0x1B, b'['];
1401        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100)); // Trigger CsiIgnore
1402        seq.push(b'@'); // Final byte
1403
1404        let events = parser.parse(&seq);
1405        assert_eq!(events.len(), 0);
1406
1407        // Feed 'a'. If '@' was correctly treated as final byte, 'a' should be parsed as 'a'.
1408        // If '@' was ignored (stayed in CsiIgnore), 'a' terminates the sequence and is swallowed.
1409        let events = parser.parse(b"a");
1410        assert_eq!(events.len(), 1, "Subsequent char 'a' was swallowed");
1411        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
1412    }
1413
1414    #[test]
1415    fn ascii_characters_parsed() {
1416        let mut parser = InputParser::new();
1417
1418        let events = parser.parse(b"abc");
1419        assert_eq!(events.len(), 3);
1420        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
1421        assert!(matches!(events[1], Event::Key(k) if k.code == KeyCode::Char('b')));
1422        assert!(matches!(events[2], Event::Key(k) if k.code == KeyCode::Char('c')));
1423    }
1424
1425    #[test]
1426    fn control_characters() {
1427        let mut parser = InputParser::new();
1428
1429        // Ctrl+A
1430        let events = parser.parse(&[0x01]);
1431        assert_eq!(events.len(), 1);
1432        assert!(matches!(
1433            events[0],
1434            Event::Key(k) if k.code == KeyCode::Char('a') && k.modifiers.contains(Modifiers::CTRL)
1435        ));
1436
1437        // Backspace
1438        let events = parser.parse(&[0x7F]);
1439        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Backspace));
1440    }
1441
1442    #[test]
1443    fn arrow_keys() {
1444        let mut parser = InputParser::new();
1445
1446        assert!(matches!(
1447            parser.parse(b"\x1b[A").first(),
1448            Some(Event::Key(k)) if k.code == KeyCode::Up
1449        ));
1450        assert!(matches!(
1451            parser.parse(b"\x1b[B").first(),
1452            Some(Event::Key(k)) if k.code == KeyCode::Down
1453        ));
1454        assert!(matches!(
1455            parser.parse(b"\x1b[C").first(),
1456            Some(Event::Key(k)) if k.code == KeyCode::Right
1457        ));
1458        assert!(matches!(
1459            parser.parse(b"\x1b[D").first(),
1460            Some(Event::Key(k)) if k.code == KeyCode::Left
1461        ));
1462    }
1463
1464    #[test]
1465    fn c1_csi_arrow_keys() {
1466        let mut parser = InputParser::new();
1467
1468        assert!(matches!(
1469            parser.parse(&[0x9B, b'A']).first(),
1470            Some(Event::Key(k)) if k.code == KeyCode::Up
1471        ));
1472        assert!(matches!(
1473            parser.parse(&[0x9B, b'B']).first(),
1474            Some(Event::Key(k)) if k.code == KeyCode::Down
1475        ));
1476    }
1477
1478    #[test]
1479    fn c1_csi_mouse_sgr_protocol() {
1480        let mut parser = InputParser::new();
1481
1482        let events = parser.parse(&[0x9B, b'<', b'0', b';', b'1', b'0', b';', b'2', b'0', b'M']);
1483        assert!(matches!(
1484            events.first(),
1485            Some(Event::Mouse(m)) if m.x == 9 && m.y == 19
1486        ));
1487    }
1488
1489    #[test]
1490    fn function_keys_ss3() {
1491        let mut parser = InputParser::new();
1492
1493        assert!(matches!(
1494            parser.parse(b"\x1bOP").first(),
1495            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1496        ));
1497        assert!(matches!(
1498            parser.parse(b"\x1bOQ").first(),
1499            Some(Event::Key(k)) if k.code == KeyCode::F(2)
1500        ));
1501        assert!(matches!(
1502            parser.parse(b"\x1bOR").first(),
1503            Some(Event::Key(k)) if k.code == KeyCode::F(3)
1504        ));
1505        assert!(matches!(
1506            parser.parse(b"\x1bOS").first(),
1507            Some(Event::Key(k)) if k.code == KeyCode::F(4)
1508        ));
1509    }
1510
1511    #[test]
1512    fn function_keys_csi() {
1513        let mut parser = InputParser::new();
1514
1515        assert!(matches!(
1516            parser.parse(b"\x1b[15~").first(),
1517            Some(Event::Key(k)) if k.code == KeyCode::F(5)
1518        ));
1519        assert!(matches!(
1520            parser.parse(b"\x1b[17~").first(),
1521            Some(Event::Key(k)) if k.code == KeyCode::F(6)
1522        ));
1523    }
1524
1525    #[test]
1526    fn modifiers_in_csi() {
1527        let mut parser = InputParser::new();
1528
1529        // Shift+Up: CSI 1;2 A
1530        let events = parser.parse(b"\x1b[1;2A");
1531        assert!(matches!(
1532            events.first(),
1533            Some(Event::Key(k)) if k.code == KeyCode::Up && k.modifiers.contains(Modifiers::SHIFT)
1534        ));
1535
1536        // Ctrl+Up: CSI 1;5 A
1537        let events = parser.parse(b"\x1b[1;5A");
1538        assert!(matches!(
1539            events.first(),
1540            Some(Event::Key(k)) if k.code == KeyCode::Up && k.modifiers.contains(Modifiers::CTRL)
1541        ));
1542    }
1543
1544    #[test]
1545    fn modifiers_in_csi_alt_ctrl() {
1546        let mut parser = InputParser::new();
1547
1548        // Alt+Ctrl+Up: CSI 1;7 A (1 + ALT(2) + CTRL(4) = 7)
1549        let events = parser.parse(b"\x1b[1;7A");
1550        assert!(matches!(
1551            events.first(),
1552            Some(Event::Key(k))
1553                if k.code == KeyCode::Up
1554                    && k.modifiers.contains(Modifiers::ALT)
1555                    && k.modifiers.contains(Modifiers::CTRL)
1556        ));
1557    }
1558
1559    #[test]
1560    fn kitty_keyboard_basic_char() {
1561        let mut parser = InputParser::new();
1562
1563        let events = parser.parse(b"\x1b[97u");
1564        assert!(matches!(
1565            events.first(),
1566            Some(Event::Key(k))
1567                if k.code == KeyCode::Char('a')
1568                    && k.modifiers == Modifiers::NONE
1569                    && k.kind == KeyEventKind::Press
1570        ));
1571    }
1572
1573    #[test]
1574    fn kitty_keyboard_with_modifiers_and_kind() {
1575        let mut parser = InputParser::new();
1576
1577        // Ctrl+repeat for 'a' (modifiers=5, event_type=2)
1578        let events = parser.parse(b"\x1b[97;5:2u");
1579        assert!(matches!(
1580            events.first(),
1581            Some(Event::Key(k))
1582                if k.code == KeyCode::Char('a')
1583                    && k.modifiers.contains(Modifiers::CTRL)
1584                    && k.kind == KeyEventKind::Repeat
1585        ));
1586    }
1587
1588    #[test]
1589    fn kitty_keyboard_function_key() {
1590        let mut parser = InputParser::new();
1591
1592        let events = parser.parse(b"\x1b[57364;1u");
1593        assert!(matches!(
1594            events.first(),
1595            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1596        ));
1597    }
1598
1599    #[test]
1600    fn alt_key_escapes() {
1601        let mut parser = InputParser::new();
1602
1603        let events = parser.parse(b"\x1ba");
1604        assert!(matches!(
1605            events.first(),
1606            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.modifiers.contains(Modifiers::ALT)
1607        ));
1608    }
1609
1610    #[test]
1611    fn alt_backspace() {
1612        let mut parser = InputParser::new();
1613
1614        let events = parser.parse(b"\x1b\x7f");
1615        assert!(matches!(
1616            events.first(),
1617            Some(Event::Key(k))
1618                if k.code == KeyCode::Backspace && k.modifiers.contains(Modifiers::ALT)
1619        ));
1620    }
1621
1622    #[test]
1623    fn escape_escape_resets_state() {
1624        let mut parser = InputParser::new();
1625
1626        let events = parser.parse(b"\x1b\x1b");
1627        assert!(matches!(
1628            events.first(),
1629            Some(Event::Key(k)) if k.code == KeyCode::Escape && k.modifiers.contains(Modifiers::ALT)
1630        ));
1631
1632        let events = parser.parse(b"a");
1633        assert!(matches!(
1634            events.first(),
1635            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.modifiers == Modifiers::NONE
1636        ));
1637    }
1638
1639    #[test]
1640    fn focus_events() {
1641        let mut parser = InputParser::new();
1642
1643        assert!(matches!(
1644            parser.parse(b"\x1b[I").first(),
1645            Some(Event::Focus(true))
1646        ));
1647        assert!(matches!(
1648            parser.parse(b"\x1b[O").first(),
1649            Some(Event::Focus(false))
1650        ));
1651    }
1652
1653    #[test]
1654    fn bracketed_paste() {
1655        let mut parser = InputParser::new();
1656
1657        // Start paste mode, paste content, end paste mode
1658        let events = parser.parse(b"\x1b[200~hello world\x1b[201~");
1659        assert_eq!(events.len(), 1);
1660        assert!(matches!(
1661            &events[0],
1662            Event::Paste(p) if p.text == "hello world"
1663        ));
1664    }
1665
1666    #[test]
1667    fn mouse_sgr_protocol() {
1668        let mut parser = InputParser::new();
1669
1670        // Left click at (10, 20)
1671        let events = parser.parse(b"\x1b[<0;10;20M");
1672        assert!(matches!(
1673            events.first(),
1674            Some(Event::Mouse(m)) if m.x == 9 && m.y == 19 // 0-indexed
1675        ));
1676    }
1677
1678    #[test]
1679    fn mouse_sgr_protocol_with_subparams() {
1680        let mut parser = InputParser::new();
1681
1682        // Accept numeric prefixes when terminals include sub-params.
1683        let events = parser.parse(b"\x1b[<0:0;10:0;20:0M");
1684        assert!(matches!(
1685            events.first(),
1686            Some(Event::Mouse(m))
1687                if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1688                    && m.x == 9
1689                    && m.y == 19
1690        ));
1691    }
1692
1693    #[test]
1694    fn mouse_sgr_protocol_large_coords_clamped() {
1695        let mut parser = InputParser::new();
1696
1697        // Coordinates beyond u16 range should be clamped instead of dropped.
1698        let events = parser.parse(b"\x1b[<0;70000;80000M");
1699        assert!(matches!(
1700            events.first(),
1701            Some(Event::Mouse(m))
1702                if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1703                    && m.x == u16::MAX
1704                    && m.y == u16::MAX
1705        ));
1706    }
1707
1708    #[test]
1709    fn mouse_sgr_protocol_negative_coords_clamped() {
1710        let mut parser = InputParser::new();
1711
1712        // Some pixel-mouse emitters can report negative coords near edges.
1713        // Clamp to origin rather than dropping the event.
1714        let events = parser.parse(b"\x1b[<0;-12;-3M");
1715        assert!(matches!(
1716            events.first(),
1717            Some(Event::Mouse(m))
1718                if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1719                    && m.x == 0
1720                    && m.y == 0
1721        ));
1722    }
1723
1724    #[test]
1725    fn mouse_sgr_modifiers() {
1726        let mut parser = InputParser::new();
1727
1728        // Shift+Alt+Ctrl + left button (0 + 4 + 8 + 16 = 28)
1729        let events = parser.parse(b"\x1b[<28;3;4M");
1730        assert!(matches!(
1731            events.first(),
1732            Some(Event::Mouse(m))
1733                if m.modifiers.contains(Modifiers::SHIFT)
1734                    && m.modifiers.contains(Modifiers::ALT)
1735                    && m.modifiers.contains(Modifiers::CTRL)
1736        ));
1737    }
1738
1739    #[test]
1740    fn mouse_sgr_scroll_up() {
1741        let mut parser = InputParser::new();
1742
1743        // Scroll up: button code 64
1744        let events = parser.parse(b"\x1b[<64;5;5M");
1745        assert!(matches!(
1746            events.first(),
1747            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollUp)
1748        ));
1749    }
1750
1751    #[test]
1752    fn mouse_sgr_scroll_down() {
1753        let mut parser = InputParser::new();
1754
1755        // Scroll down: button code 65
1756        let events = parser.parse(b"\x1b[<65;5;5M");
1757        assert!(matches!(
1758            events.first(),
1759            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollDown)
1760        ));
1761    }
1762
1763    #[test]
1764    fn mouse_sgr_scroll_left() {
1765        let mut parser = InputParser::new();
1766
1767        // Scroll left: button code 66
1768        let events = parser.parse(b"\x1b[<66;5;5M");
1769        assert!(matches!(
1770            events.first(),
1771            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollLeft)
1772        ));
1773    }
1774
1775    #[test]
1776    fn mouse_sgr_scroll_right() {
1777        let mut parser = InputParser::new();
1778
1779        // Scroll right: button code 67
1780        let events = parser.parse(b"\x1b[<67;5;5M");
1781        assert!(matches!(
1782            events.first(),
1783            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollRight)
1784        ));
1785    }
1786
1787    #[test]
1788    fn mouse_sgr_drag_left() {
1789        let mut parser = InputParser::new();
1790
1791        // Drag with left button: button code 32
1792        let events = parser.parse(b"\x1b[<32;10;20M");
1793        assert!(matches!(
1794            events.first(),
1795            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Drag(MouseButton::Left))
1796        ));
1797    }
1798
1799    #[test]
1800    fn utf8_characters() {
1801        let mut parser = InputParser::new();
1802
1803        // é (U+00E9) = 0xC3 0xA9
1804        let events = parser.parse(&[0xC3, 0xA9]);
1805        assert!(matches!(
1806            events.first(),
1807            Some(Event::Key(k)) if k.code == KeyCode::Char('é')
1808        ));
1809    }
1810
1811    #[test]
1812    fn invalid_utf8_emits_replacement_then_reprocesses_byte() {
1813        let mut parser = InputParser::new();
1814
1815        // 0xE2 expects a 3-byte sequence, 0x28 is invalid continuation.
1816        let events = parser.parse(&[0xE2, 0x28]);
1817        assert_eq!(events.len(), 2);
1818        assert!(matches!(
1819            events[0],
1820            Event::Key(k) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)
1821        ));
1822        assert!(matches!(
1823            events[1],
1824            Event::Key(k) if k.code == KeyCode::Char('(')
1825        ));
1826    }
1827
1828    #[test]
1829    fn dos_protection_csi() {
1830        let mut parser = InputParser::new();
1831
1832        // Create a very long CSI sequence
1833        let mut seq = vec![0x1B, b'['];
1834        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100));
1835        seq.push(b'A');
1836
1837        // DoS protection kicks in and switches to CsiIgnore
1838        // Excess bytes should be ignored, NOT leaked as characters
1839        let events = parser.parse(&seq);
1840        assert_eq!(
1841            events.len(),
1842            0,
1843            "Oversized CSI sequence should produce no events"
1844        );
1845
1846        // The key invariant: parser should be back in ground state and functional
1847        // Verify by parsing a normal sequence after the attack
1848        let events = parser.parse(b"\x1b[A");
1849        assert!(matches!(
1850            events.first(),
1851            Some(Event::Key(k)) if k.code == KeyCode::Up
1852        ));
1853    }
1854
1855    #[test]
1856    fn incomplete_csi_sequence_emits_no_event() {
1857        let mut parser = InputParser::new();
1858        let events = parser.parse(b"\x1b[");
1859        assert!(events.is_empty());
1860    }
1861
1862    #[test]
1863    fn dos_protection_paste() {
1864        let mut parser = InputParser::new();
1865
1866        // Start paste mode
1867        parser.parse(b"\x1b[200~");
1868
1869        // Paste content up to the limit
1870        let content = vec![b'x'; MAX_PASTE_LEN - 100]; // Leave room for end sequence
1871        parser.parse(&content);
1872
1873        // End paste mode
1874        let events = parser.parse(b"\x1b[201~");
1875
1876        // Should have collected content up to limit
1877        assert!(matches!(
1878            events.first(),
1879            Some(Event::Paste(p)) if p.text.len() <= MAX_PASTE_LEN
1880        ));
1881    }
1882
1883    #[test]
1884    fn dos_protection_paste_overflow_terminator() {
1885        let mut parser = InputParser::new();
1886
1887        // Start paste mode
1888        parser.parse(b"\x1b[200~");
1889
1890        // Overflow the buffer by pushing more than MAX_PASTE_LEN bytes.
1891        // DoS protection stops collecting content once buffer is full,
1892        // but continues tracking the end sequence to properly exit paste mode.
1893        let overflow = 100;
1894        let content = vec![b'a'; MAX_PASTE_LEN + overflow];
1895        parser.parse(&content);
1896
1897        // Send terminator - parser MUST detect it and exit paste mode.
1898        // Even though the buffer overflowed, the terminator detection still works.
1899        let events = parser.parse(b"\x1b[201~");
1900
1901        assert_eq!(events.len(), 1, "Should emit paste event");
1902        match &events[0] {
1903            Event::Paste(p) => {
1904                // Content is capped at MAX_PASTE_LEN due to DoS protection.
1905                // Overflow bytes are discarded but terminator is still detected.
1906                assert_eq!(
1907                    p.text.len(),
1908                    MAX_PASTE_LEN,
1909                    "Paste should be capped at MAX_PASTE_LEN bytes"
1910                );
1911                // The content should be all 'a' since we filled with 'a'
1912                assert!(p.text.chars().all(|c| c == 'a'));
1913            }
1914            _ => unreachable!("Expected Paste event"),
1915        }
1916
1917        // Verify we are back in ground state by parsing a key
1918        let events = parser.parse(b"b");
1919        assert_eq!(events.len(), 1);
1920        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('b')));
1921    }
1922
1923    #[test]
1924    fn no_panic_on_invalid_input() {
1925        let mut parser = InputParser::new();
1926
1927        // Random bytes that might trip up the parser
1928        let garbage = [0xFF, 0xFE, 0x00, 0x1B, 0x1B, 0x1B, b'[', 0xFF, b']', 0x00];
1929
1930        // Should not panic
1931        let _ = parser.parse(&garbage);
1932    }
1933
1934    #[test]
1935    fn dos_protection_paste_boundary() {
1936        let mut parser = InputParser::new();
1937        // Start paste mode
1938        parser.parse(b"\x1b[200~");
1939
1940        // Fill buffer exactly to limit
1941        let content = vec![b'x'; MAX_PASTE_LEN];
1942        parser.parse(&content);
1943
1944        // Send end sequence
1945        // This will be processed by the DoS protection fallback logic
1946        let events = parser.parse(b"\x1b[201~");
1947
1948        assert!(
1949            !events.is_empty(),
1950            "Parser trapped in paste mode after hitting limit"
1951        );
1952        assert!(matches!(events[0], Event::Paste(_)));
1953    }
1954
1955    // ── Navigation keys via CSI ~ sequences ──────────────────────────
1956
1957    #[test]
1958    fn csi_tilde_home() {
1959        let mut parser = InputParser::new();
1960        let events = parser.parse(b"\x1b[1~");
1961        assert!(matches!(
1962            events.first(),
1963            Some(Event::Key(k)) if k.code == KeyCode::Home
1964        ));
1965    }
1966
1967    #[test]
1968    fn csi_tilde_insert() {
1969        let mut parser = InputParser::new();
1970        let events = parser.parse(b"\x1b[2~");
1971        assert!(matches!(
1972            events.first(),
1973            Some(Event::Key(k)) if k.code == KeyCode::Insert
1974        ));
1975    }
1976
1977    #[test]
1978    fn csi_tilde_delete() {
1979        let mut parser = InputParser::new();
1980        let events = parser.parse(b"\x1b[3~");
1981        assert!(matches!(
1982            events.first(),
1983            Some(Event::Key(k)) if k.code == KeyCode::Delete
1984        ));
1985    }
1986
1987    #[test]
1988    fn csi_tilde_end() {
1989        let mut parser = InputParser::new();
1990        let events = parser.parse(b"\x1b[4~");
1991        assert!(matches!(
1992            events.first(),
1993            Some(Event::Key(k)) if k.code == KeyCode::End
1994        ));
1995    }
1996
1997    #[test]
1998    fn csi_tilde_page_up() {
1999        let mut parser = InputParser::new();
2000        let events = parser.parse(b"\x1b[5~");
2001        assert!(matches!(
2002            events.first(),
2003            Some(Event::Key(k)) if k.code == KeyCode::PageUp
2004        ));
2005    }
2006
2007    #[test]
2008    fn csi_tilde_page_down() {
2009        let mut parser = InputParser::new();
2010        let events = parser.parse(b"\x1b[6~");
2011        assert!(matches!(
2012            events.first(),
2013            Some(Event::Key(k)) if k.code == KeyCode::PageDown
2014        ));
2015    }
2016
2017    // ── Navigation keys via CSI H/F (xterm-style) ───────────────────
2018
2019    #[test]
2020    fn csi_home_and_end() {
2021        let mut parser = InputParser::new();
2022        assert!(matches!(
2023            parser.parse(b"\x1b[H").first(),
2024            Some(Event::Key(k)) if k.code == KeyCode::Home
2025        ));
2026        assert!(matches!(
2027            parser.parse(b"\x1b[F").first(),
2028            Some(Event::Key(k)) if k.code == KeyCode::End
2029        ));
2030    }
2031
2032    // ── SS3 Home/End ─────────────────────────────────────────────────
2033
2034    #[test]
2035    fn ss3_home_and_end() {
2036        let mut parser = InputParser::new();
2037        assert!(matches!(
2038            parser.parse(b"\x1bOH").first(),
2039            Some(Event::Key(k)) if k.code == KeyCode::Home
2040        ));
2041        assert!(matches!(
2042            parser.parse(b"\x1bOF").first(),
2043            Some(Event::Key(k)) if k.code == KeyCode::End
2044        ));
2045    }
2046
2047    // ── BackTab (Shift+Tab via CSI Z) ────────────────────────────────
2048
2049    #[test]
2050    fn backtab_csi_z() {
2051        let mut parser = InputParser::new();
2052        let events = parser.parse(b"\x1b[Z");
2053        assert!(matches!(
2054            events.first(),
2055            Some(Event::Key(k)) if k.code == KeyCode::BackTab
2056        ));
2057    }
2058
2059    // ── F7-F12 keys via CSI tilde ────────────────────────────────────
2060
2061    #[test]
2062    fn function_keys_f7_to_f12() {
2063        let mut parser = InputParser::new();
2064        assert!(matches!(
2065            parser.parse(b"\x1b[18~").first(),
2066            Some(Event::Key(k)) if k.code == KeyCode::F(7)
2067        ));
2068        assert!(matches!(
2069            parser.parse(b"\x1b[19~").first(),
2070            Some(Event::Key(k)) if k.code == KeyCode::F(8)
2071        ));
2072        assert!(matches!(
2073            parser.parse(b"\x1b[20~").first(),
2074            Some(Event::Key(k)) if k.code == KeyCode::F(9)
2075        ));
2076        assert!(matches!(
2077            parser.parse(b"\x1b[21~").first(),
2078            Some(Event::Key(k)) if k.code == KeyCode::F(10)
2079        ));
2080        assert!(matches!(
2081            parser.parse(b"\x1b[23~").first(),
2082            Some(Event::Key(k)) if k.code == KeyCode::F(11)
2083        ));
2084        assert!(matches!(
2085            parser.parse(b"\x1b[24~").first(),
2086            Some(Event::Key(k)) if k.code == KeyCode::F(12)
2087        ));
2088    }
2089
2090    // ── Modifier combinations on navigation keys ─────────────────────
2091
2092    #[test]
2093    fn ctrl_home_and_alt_end() {
2094        let mut parser = InputParser::new();
2095
2096        // Ctrl+Home: CSI 1;5 H
2097        let events = parser.parse(b"\x1b[1;5H");
2098        assert!(matches!(
2099            events.first(),
2100            Some(Event::Key(k)) if k.code == KeyCode::Home && k.modifiers.contains(Modifiers::CTRL)
2101        ));
2102
2103        // Alt+End: CSI 1;3 F
2104        let events = parser.parse(b"\x1b[1;3F");
2105        assert!(matches!(
2106            events.first(),
2107            Some(Event::Key(k)) if k.code == KeyCode::End && k.modifiers.contains(Modifiers::ALT)
2108        ));
2109    }
2110
2111    #[test]
2112    fn shift_ctrl_arrow() {
2113        let mut parser = InputParser::new();
2114
2115        // Shift+Ctrl+Right: CSI 1;6 C (modifier value 6 = 1 + Shift|Ctrl = 1 + 5)
2116        let events = parser.parse(b"\x1b[1;6C");
2117        assert!(matches!(
2118            events.first(),
2119            Some(Event::Key(k)) if k.code == KeyCode::Right
2120                && k.modifiers.contains(Modifiers::SHIFT)
2121                && k.modifiers.contains(Modifiers::CTRL)
2122        ));
2123    }
2124
2125    #[test]
2126    fn modifiers_on_tilde_keys() {
2127        let mut parser = InputParser::new();
2128
2129        // Ctrl+Delete: CSI 3;5 ~
2130        let events = parser.parse(b"\x1b[3;5~");
2131        assert!(matches!(
2132            events.first(),
2133            Some(Event::Key(k)) if k.code == KeyCode::Delete && k.modifiers.contains(Modifiers::CTRL)
2134        ));
2135
2136        // Shift+PageUp: CSI 5;2 ~
2137        let events = parser.parse(b"\x1b[5;2~");
2138        assert!(matches!(
2139            events.first(),
2140            Some(Event::Key(k)) if k.code == KeyCode::PageUp && k.modifiers.contains(Modifiers::SHIFT)
2141        ));
2142    }
2143
2144    // ── Mouse right/middle click and release ─────────────────────────
2145
2146    #[test]
2147    fn mouse_sgr_right_click() {
2148        let mut parser = InputParser::new();
2149        // Right click: button code 2
2150        let events = parser.parse(b"\x1b[<2;15;10M");
2151        assert!(matches!(
2152            events.first(),
2153            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Right))
2154                && m.x == 14 && m.y == 9
2155        ));
2156    }
2157
2158    #[test]
2159    fn mouse_sgr_middle_click() {
2160        let mut parser = InputParser::new();
2161        // Middle click: button code 1
2162        let events = parser.parse(b"\x1b[<1;5;5M");
2163        assert!(matches!(
2164            events.first(),
2165            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Middle))
2166        ));
2167    }
2168
2169    #[test]
2170    fn mouse_sgr_button_release() {
2171        let mut parser = InputParser::new();
2172        // Left button release: final byte 'm'
2173        let events = parser.parse(b"\x1b[<0;10;20m");
2174        assert!(matches!(
2175            events.first(),
2176            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Up(MouseButton::Left))
2177        ));
2178    }
2179
2180    #[test]
2181    fn mouse_sgr_button_release_uppercase_m_compat() {
2182        let mut parser = InputParser::new();
2183        // Compatibility release encoding used by some terminals:
2184        // final byte 'M' with low bits == 3.
2185        let events = parser.parse(b"\x1b[<3;10;20M");
2186        assert!(matches!(
2187            events.first(),
2188            Some(Event::Mouse(m))
2189                if matches!(m.kind, MouseEventKind::Up(MouseButton::Left))
2190                    && m.x == 9
2191                    && m.y == 19
2192        ));
2193    }
2194
2195    #[test]
2196    fn mouse_sgr_moved() {
2197        let mut parser = InputParser::new();
2198        // Mouse move (no button): button code 35 (32 | 3, bit 5 set + bits 0-1 = 3)
2199        let events = parser.parse(b"\x1b[<35;10;20M");
2200        assert!(matches!(
2201            events.first(),
2202            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Moved)
2203        ));
2204    }
2205
2206    #[test]
2207    fn mouse_sgr_with_modifiers() {
2208        let mut parser = InputParser::new();
2209        // Shift+Left click: button_code bit 2 set (shift) = 4
2210        let events = parser.parse(b"\x1b[<4;5;5M");
2211        assert!(matches!(
2212            events.first(),
2213            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2214                && m.modifiers.contains(Modifiers::SHIFT)
2215        ));
2216
2217        // Ctrl+Left click: button_code bit 4 set (ctrl) = 16
2218        let events = parser.parse(b"\x1b[<16;5;5M");
2219        assert!(matches!(
2220            events.first(),
2221            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2222                && m.modifiers.contains(Modifiers::CTRL)
2223        ));
2224
2225        // Alt+Left click: button_code bit 3 set (alt) = 8
2226        let events = parser.parse(b"\x1b[<8;5;5M");
2227        assert!(matches!(
2228            events.first(),
2229            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2230                && m.modifiers.contains(Modifiers::ALT)
2231        ));
2232    }
2233
2234    #[test]
2235    fn mouse_legacy_1015_when_enabled() {
2236        let mut parser = InputParser::new();
2237        parser.set_expect_x10_mouse(true);
2238
2239        let events = parser.parse(b"\x1b[0;10;20M");
2240        assert!(matches!(
2241            events.first(),
2242            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2243                && m.x == 9 && m.y == 19
2244        ));
2245    }
2246
2247    #[test]
2248    fn mouse_legacy_1015_with_fallback_enabled() {
2249        let mut parser = InputParser::new();
2250        parser.set_allow_legacy_mouse(true);
2251
2252        let events = parser.parse(b"\x1b[0;10;20M");
2253        assert!(matches!(
2254            events.first(),
2255            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2256                && m.x == 9 && m.y == 19
2257        ));
2258    }
2259
2260    #[test]
2261    fn mouse_legacy_1015_ignored_when_disabled() {
2262        let mut parser = InputParser::new();
2263        let events = parser.parse(b"\x1b[0;10;20M");
2264        assert!(
2265            events.is_empty(),
2266            "legacy mouse should require explicit opt-in"
2267        );
2268    }
2269
2270    #[test]
2271    fn mouse_x10_when_enabled() {
2272        let mut parser = InputParser::new();
2273        parser.set_expect_x10_mouse(true);
2274
2275        let events = parser.parse(&[0x1B, b'[', b'M', 32, 42, 52]);
2276        assert!(matches!(
2277            events.first(),
2278            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2279                && m.x == 9 && m.y == 19
2280        ));
2281    }
2282
2283    #[test]
2284    fn mouse_x10_malformed_packet_ignored() {
2285        let mut parser = InputParser::new();
2286        parser.set_expect_x10_mouse(true);
2287
2288        // Invalid X10 payload bytes (<32 / <33) should be dropped.
2289        let events = parser.parse(&[0x1B, b'[', b'M', 31, 0, 10]);
2290        assert!(
2291            events.iter().all(|event| !matches!(event, Event::Mouse(_))),
2292            "malformed X10 payload must not emit mouse events"
2293        );
2294    }
2295
2296    // ── Kitty keyboard release events and special keys ───────────────
2297
2298    #[test]
2299    fn kitty_keyboard_release_event() {
2300        let mut parser = InputParser::new();
2301        // Release event: kind=3
2302        let events = parser.parse(b"\x1b[97;1:3u");
2303        assert!(matches!(
2304            events.first(),
2305            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.kind == KeyEventKind::Release
2306        ));
2307
2308        // Ctrl+release for 'A' (modifiers=5, event_type=3)
2309        let events = parser.parse(b"\x1b[65;5:3u");
2310        assert!(matches!(
2311            events.first(),
2312            Some(Event::Key(k))
2313                if k.code == KeyCode::Char('A')
2314                    && k.modifiers.contains(Modifiers::CTRL)
2315                    && k.kind == KeyEventKind::Release
2316        ));
2317    }
2318
2319    #[test]
2320    fn kitty_keyboard_special_keys() {
2321        let mut parser = InputParser::new();
2322
2323        // Escape: 57344
2324        assert!(matches!(
2325            parser.parse(b"\x1b[57344u").first(),
2326            Some(Event::Key(k)) if k.code == KeyCode::Escape
2327        ));
2328
2329        // Enter: 57345
2330        assert!(matches!(
2331            parser.parse(b"\x1b[57345u").first(),
2332            Some(Event::Key(k)) if k.code == KeyCode::Enter
2333        ));
2334
2335        // Tab: 57346
2336        assert!(matches!(
2337            parser.parse(b"\x1b[57346u").first(),
2338            Some(Event::Key(k)) if k.code == KeyCode::Tab
2339        ));
2340
2341        // Backspace: 57347
2342        assert!(matches!(
2343            parser.parse(b"\x1b[57347u").first(),
2344            Some(Event::Key(k)) if k.code == KeyCode::Backspace
2345        ));
2346
2347        // Insert: 57348
2348        assert!(matches!(
2349            parser.parse(b"\x1b[57348u").first(),
2350            Some(Event::Key(k)) if k.code == KeyCode::Insert
2351        ));
2352
2353        // Delete: 57349
2354        assert!(matches!(
2355            parser.parse(b"\x1b[57349u").first(),
2356            Some(Event::Key(k)) if k.code == KeyCode::Delete
2357        ));
2358    }
2359
2360    #[test]
2361    fn kitty_keyboard_navigation_keys() {
2362        let mut parser = InputParser::new();
2363
2364        // Left: 57350
2365        assert!(matches!(
2366            parser.parse(b"\x1b[57350u").first(),
2367            Some(Event::Key(k)) if k.code == KeyCode::Left
2368        ));
2369        // Right: 57351
2370        assert!(matches!(
2371            parser.parse(b"\x1b[57351u").first(),
2372            Some(Event::Key(k)) if k.code == KeyCode::Right
2373        ));
2374        // Up: 57352
2375        assert!(matches!(
2376            parser.parse(b"\x1b[57352u").first(),
2377            Some(Event::Key(k)) if k.code == KeyCode::Up
2378        ));
2379        // Down: 57353
2380        assert!(matches!(
2381            parser.parse(b"\x1b[57353u").first(),
2382            Some(Event::Key(k)) if k.code == KeyCode::Down
2383        ));
2384        // PageUp: 57354
2385        assert!(matches!(
2386            parser.parse(b"\x1b[57354u").first(),
2387            Some(Event::Key(k)) if k.code == KeyCode::PageUp
2388        ));
2389        // PageDown: 57355
2390        assert!(matches!(
2391            parser.parse(b"\x1b[57355u").first(),
2392            Some(Event::Key(k)) if k.code == KeyCode::PageDown
2393        ));
2394        // Home: 57356
2395        assert!(matches!(
2396            parser.parse(b"\x1b[57356u").first(),
2397            Some(Event::Key(k)) if k.code == KeyCode::Home
2398        ));
2399        // End: 57357
2400        assert!(matches!(
2401            parser.parse(b"\x1b[57357u").first(),
2402            Some(Event::Key(k)) if k.code == KeyCode::End
2403        ));
2404    }
2405
2406    #[test]
2407    fn kitty_keyboard_f_keys() {
2408        let mut parser = InputParser::new();
2409        // F1: 57364
2410        assert!(matches!(
2411            parser.parse(b"\x1b[57364u").first(),
2412            Some(Event::Key(k)) if k.code == KeyCode::F(1)
2413        ));
2414        // F12: 57375
2415        assert!(matches!(
2416            parser.parse(b"\x1b[57375u").first(),
2417            Some(Event::Key(k)) if k.code == KeyCode::F(12)
2418        ));
2419        // F24: 57387
2420        assert!(matches!(
2421            parser.parse(b"\x1b[57387u").first(),
2422            Some(Event::Key(k)) if k.code == KeyCode::F(24)
2423        ));
2424    }
2425
2426    #[test]
2427    fn kitty_keyboard_ascii_as_standard() {
2428        let mut parser = InputParser::new();
2429        // Tab (9), Enter (13), Escape (27), Backspace (127)
2430        assert!(matches!(
2431            parser.parse(b"\x1b[9u").first(),
2432            Some(Event::Key(k)) if k.code == KeyCode::Tab
2433        ));
2434        assert!(matches!(
2435            parser.parse(b"\x1b[13u").first(),
2436            Some(Event::Key(k)) if k.code == KeyCode::Enter
2437        ));
2438        assert!(matches!(
2439            parser.parse(b"\x1b[27u").first(),
2440            Some(Event::Key(k)) if k.code == KeyCode::Escape
2441        ));
2442        assert!(matches!(
2443            parser.parse(b"\x1b[127u").first(),
2444            Some(Event::Key(k)) if k.code == KeyCode::Backspace
2445        ));
2446        // Backspace alternate: 8
2447        assert!(matches!(
2448            parser.parse(b"\x1b[8u").first(),
2449            Some(Event::Key(k)) if k.code == KeyCode::Backspace
2450        ));
2451    }
2452
2453    // ── OSC 52 clipboard ─────────────────────────────────────────────
2454
2455    #[test]
2456    fn osc52_clipboard_bel_terminated() {
2457        let mut parser = InputParser::new();
2458        // OSC 52;c;<base64 "hello"> BEL
2459        // "hello" in base64 is "aGVsbG8="
2460        let events = parser.parse(b"\x1b]52;c;aGVsbG8=\x07");
2461        assert!(matches!(
2462            events.first(),
2463            Some(Event::Clipboard(c)) if c.content == "hello" && c.source == ClipboardSource::Osc52
2464        ));
2465    }
2466
2467    #[test]
2468    fn osc52_clipboard_st_terminated() {
2469        let mut parser = InputParser::new();
2470        // OSC 52;c;<base64 "hello"> ESC \
2471        let events = parser.parse(b"\x1b]52;c;aGVsbG8=\x1b\\");
2472        assert!(matches!(
2473            events.first(),
2474            Some(Event::Clipboard(c)) if c.content == "hello"
2475        ));
2476    }
2477
2478    // --- DCS / control-string handling (XTGETTCAP-reply leak guard) ---
2479
2480    #[test]
2481    fn dcs_xtgettcap_reply_produces_no_events() {
2482        let mut parser = InputParser::new();
2483        // A leaked XTGETTCAP `RGB` reply (e.g. a truecolor probe whose answer
2484        // arrives after the probe timed out on a slow ssh link) MUST be consumed
2485        // silently — not decoded as `Alt+P` then the payload as literal keys.
2486        let events = parser.parse(b"\x1bP1+r524742=8/8/8\x1b\\");
2487        assert!(
2488            events.is_empty(),
2489            "a DCS reply must produce no events, got: {events:?}"
2490        );
2491    }
2492
2493    #[test]
2494    fn dcs_then_real_key_recovers_to_ground() {
2495        let mut parser = InputParser::new();
2496        // After an ST-terminated DCS the parser must be back in Ground so the
2497        // following real keypress parses normally.
2498        let events = parser.parse(b"\x1bP1+r524742=8/8/8\x1b\\a");
2499        assert!(
2500            matches!(events.as_slice(), [Event::Key(k)] if k.code == KeyCode::Char('a')),
2501            "parser must recover to Ground after a DCS, got: {events:?}"
2502        );
2503    }
2504
2505    #[test]
2506    fn dcs_bel_terminated_is_ignored() {
2507        let mut parser = InputParser::new();
2508        let events = parser.parse(b"\x1bPsome-payload\x07b");
2509        assert!(
2510            matches!(events.as_slice(), [Event::Key(k)] if k.code == KeyCode::Char('b')),
2511            "BEL-terminated DCS ignored, then key parses, got: {events:?}"
2512        );
2513    }
2514
2515    #[test]
2516    fn sos_pm_apc_introducers_stay_alt_keys() {
2517        // We deliberately intercept ONLY DCS (ESC P), not the sibling C1 string
2518        // introducers SOS/PM/APC — terminals never send those as responses, so
2519        // they remain `Alt+Shift+X` / `Alt+^` / `Alt+_` keypresses.
2520        for &introducer in b"X^_" {
2521            let mut parser = InputParser::new();
2522            let events = parser.parse(&[0x1b, introducer]);
2523            assert!(
2524                matches!(
2525                    events.as_slice(),
2526                    [Event::Key(k)]
2527                        if k.code == KeyCode::Char(introducer as char)
2528                            && k.modifiers.contains(Modifiers::ALT)
2529                ),
2530                "ESC {} must stay an Alt key, got: {events:?}",
2531                introducer as char
2532            );
2533        }
2534    }
2535
2536    #[test]
2537    fn dcs_esc_then_csi_recovers_to_arrow_key() {
2538        let mut parser = InputParser::new();
2539        // ESC inside the DCS payload, followed not by `\` (ST) but by a CSI
2540        // (`[A` = Up): the string is cancelled and the CSI parses cleanly.
2541        let events = parser.parse(b"\x1bP payload \x1b[A");
2542        assert!(
2543            matches!(events.as_slice(), [Event::Key(k)] if k.code == KeyCode::Up),
2544            "ESC-mid-DCS then a CSI must recover and parse the arrow, got: {events:?}"
2545        );
2546    }
2547
2548    #[test]
2549    fn dcs_aborts_on_control_char_so_input_is_not_swallowed() {
2550        let mut parser = InputParser::new();
2551        // A never-terminated DCS followed by Enter (CR, a control byte): the
2552        // control char must abort the string and be reprocessed, so a malformed
2553        // string cannot swallow subsequent real input forever.
2554        let events = parser.parse(b"\x1bPunterminated\r");
2555        assert!(
2556            !events.is_empty(),
2557            "a control char must abort a stuck DCS and emit the key, got: {events:?}"
2558        );
2559    }
2560
2561    #[test]
2562    fn osc52_clipboard_primary_selection() {
2563        let mut parser = InputParser::new();
2564        // Primary selection: p instead of c
2565        // "abc" in base64 is "YWJj"
2566        let events = parser.parse(b"\x1b]52;p;YWJj\x07");
2567        assert!(matches!(
2568            events.first(),
2569            Some(Event::Clipboard(c)) if c.content == "abc"
2570        ));
2571    }
2572
2573    // ── Control keys ─────────────────────────────────────────────────
2574
2575    #[test]
2576    fn ctrl_space_is_null() {
2577        let mut parser = InputParser::new();
2578        let events = parser.parse(&[0x00]);
2579        assert!(matches!(
2580            events.first(),
2581            Some(Event::Key(k)) if k.code == KeyCode::Null
2582        ));
2583    }
2584
2585    #[test]
2586    fn all_ctrl_letter_keys() {
2587        let mut parser = InputParser::new();
2588        // Ctrl+A (0x01) through Ctrl+Z (0x1A), skipping Backspace (0x08), Tab (0x09), and Enter (0x0D)
2589        for byte in 0x01..=0x1Au8 {
2590            let events = parser.parse(&[byte]);
2591            assert_eq!(
2592                events.len(),
2593                1,
2594                "Ctrl+{} should produce one event",
2595                (byte + b'a' - 1) as char
2596            );
2597            match byte {
2598                0x08 => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Backspace)),
2599                0x09 => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Tab)),
2600                0x0D => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Enter)),
2601                _ => {
2602                    let expected_char = (byte + b'a' - 1) as char;
2603                    match &events[0] {
2604                        Event::Key(k) => {
2605                            assert_eq!(
2606                                k.code,
2607                                KeyCode::Char(expected_char),
2608                                "Byte 0x{byte:02X} should produce Ctrl+{expected_char}"
2609                            );
2610                            assert!(
2611                                k.modifiers.contains(Modifiers::CTRL),
2612                                "Byte 0x{byte:02X} should have Ctrl modifier"
2613                            );
2614                        }
2615                        other => {
2616                            panic!("Byte 0x{byte:02X}: expected Key event, got {other:?}");
2617                        }
2618                    }
2619                }
2620            }
2621        }
2622    }
2623
2624    // ── UTF-8 multi-byte: 3-byte and 4-byte ─────────────────────────
2625
2626    #[test]
2627    fn utf8_3byte_cjk() {
2628        let mut parser = InputParser::new();
2629        // 中 (U+4E2D) = 0xE4 0xB8 0xAD
2630        let events = parser.parse(&[0xE4, 0xB8, 0xAD]);
2631        assert!(matches!(
2632            events.first(),
2633            Some(Event::Key(k)) if k.code == KeyCode::Char('中')
2634        ));
2635    }
2636
2637    #[test]
2638    fn utf8_4byte_emoji() {
2639        let mut parser = InputParser::new();
2640        // 🦀 (U+1F980) = 0xF0 0x9F 0xA6 0x80
2641        let events = parser.parse(&[0xF0, 0x9F, 0xA6, 0x80]);
2642        assert!(matches!(
2643            events.first(),
2644            Some(Event::Key(k)) if k.code == KeyCode::Char('🦀')
2645        ));
2646    }
2647
2648    // ── Empty input ──────────────────────────────────────────────────
2649
2650    #[test]
2651    fn empty_input_returns_no_events() {
2652        let mut parser = InputParser::new();
2653        let events = parser.parse(b"");
2654        assert!(events.is_empty());
2655    }
2656
2657    // ── Unknown CSI tilde values ─────────────────────────────────────
2658
2659    #[test]
2660    fn unknown_csi_tilde_ignored() {
2661        let mut parser = InputParser::new();
2662        // Code 99 is not a known tilde key
2663        let events = parser.parse(b"\x1b[99~");
2664        assert!(events.is_empty());
2665
2666        // Parser should still work
2667        let events = parser.parse(b"a");
2668        assert!(matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char('a')));
2669    }
2670
2671    // ── Alt+various characters ───────────────────────────────────────
2672
2673    #[test]
2674    fn alt_special_chars() {
2675        let mut parser = InputParser::new();
2676
2677        // Alt+space
2678        let events = parser.parse(b"\x1b ");
2679        assert!(matches!(
2680            events.first(),
2681            Some(Event::Key(k)) if k.code == KeyCode::Char(' ') && k.modifiers.contains(Modifiers::ALT)
2682        ));
2683
2684        // Alt+digit
2685        let events = parser.parse(b"\x1b5");
2686        assert!(matches!(
2687            events.first(),
2688            Some(Event::Key(k)) if k.code == KeyCode::Char('5') && k.modifiers.contains(Modifiers::ALT)
2689        ));
2690
2691        // Alt+bracket
2692        let events = parser.parse(b"\x1b}");
2693        assert!(matches!(
2694            events.first(),
2695            Some(Event::Key(k)) if k.code == KeyCode::Char('}') && k.modifiers.contains(Modifiers::ALT)
2696        ));
2697    }
2698
2699    #[test]
2700    fn alt_ctrl_key_combinations() {
2701        let mut parser = InputParser::new();
2702
2703        // ESC + Ctrl+A (0x01) -> Alt+Ctrl+A
2704        let events = parser.parse(&[0x1B, 0x01]);
2705        assert_eq!(events.len(), 1);
2706        match &events[0] {
2707            Event::Key(k) => {
2708                assert_eq!(k.code, KeyCode::Char('a'));
2709                assert!(k.modifiers.contains(Modifiers::ALT));
2710                assert!(k.modifiers.contains(Modifiers::CTRL));
2711            }
2712            _ => panic!("Expected Key event"),
2713        }
2714
2715        // ESC + Backspace (0x08) -> Alt+Backspace (Ctrl+H is Backspace)
2716        // Note: 0x08 is Backspace in process_ground.
2717        // So ESC + 0x08 should be Alt+Backspace.
2718        let events = parser.parse(&[0x1B, 0x08]);
2719        assert_eq!(events.len(), 1);
2720        match &events[0] {
2721            Event::Key(k) => {
2722                assert_eq!(k.code, KeyCode::Backspace);
2723                assert!(k.modifiers.contains(Modifiers::ALT));
2724            }
2725            _ => panic!("Expected Key event"),
2726        }
2727    }
2728
2729    // ── SS3 arrow keys ───────────────────────────────────────────────
2730
2731    #[test]
2732    fn ss3_arrow_keys() {
2733        let mut parser = InputParser::new();
2734        assert!(matches!(
2735            parser.parse(b"\x1bOA").first(),
2736            Some(Event::Key(k)) if k.code == KeyCode::Up
2737        ));
2738        assert!(matches!(
2739            parser.parse(b"\x1bOB").first(),
2740            Some(Event::Key(k)) if k.code == KeyCode::Down
2741        ));
2742        assert!(matches!(
2743            parser.parse(b"\x1bOC").first(),
2744            Some(Event::Key(k)) if k.code == KeyCode::Right
2745        ));
2746        assert!(matches!(
2747            parser.parse(b"\x1bOD").first(),
2748            Some(Event::Key(k)) if k.code == KeyCode::Left
2749        ));
2750    }
2751
2752    // ── Xterm modifier encoding ──────────────────────────────────────
2753
2754    #[test]
2755    fn xterm_modifier_encoding() {
2756        // Verify modifiers_from_xterm decoding (value = 1 + modifier_bits)
2757        assert_eq!(InputParser::modifiers_from_xterm(1), Modifiers::NONE);
2758        assert_eq!(InputParser::modifiers_from_xterm(2), Modifiers::SHIFT);
2759        assert_eq!(InputParser::modifiers_from_xterm(3), Modifiers::ALT);
2760        assert_eq!(
2761            InputParser::modifiers_from_xterm(4),
2762            Modifiers::SHIFT | Modifiers::ALT
2763        );
2764        assert_eq!(InputParser::modifiers_from_xterm(5), Modifiers::CTRL);
2765        assert_eq!(
2766            InputParser::modifiers_from_xterm(6),
2767            Modifiers::SHIFT | Modifiers::CTRL
2768        );
2769        assert_eq!(InputParser::modifiers_from_xterm(9), Modifiers::SUPER);
2770    }
2771
2772    // ── SS3 interrupted by ESC ───────────────────────────────────────
2773
2774    #[test]
2775    fn ss3_interrupted_by_esc() {
2776        let mut parser = InputParser::new();
2777        // ESC O ESC should restart into Escape state
2778        let events = parser.parse(b"\x1bO\x1b[A");
2779        // Should get Up arrow from the new ESC [ A sequence
2780        assert!(matches!(
2781            events.first(),
2782            Some(Event::Key(k)) if k.code == KeyCode::Up
2783        ));
2784    }
2785
2786    // ── Kitty keyboard: unhandled keycodes ───────────────────────────
2787
2788    #[test]
2789    fn kitty_keyboard_reserved_keycode_ignored() {
2790        let mut parser = InputParser::new();
2791        // Reserved range 57358..=57363 returns None
2792        let events = parser.parse(b"\x1b[57360u");
2793        assert!(events.is_empty());
2794
2795        // Parser still works
2796        let events = parser.parse(b"x");
2797        assert!(matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char('x')));
2798    }
2799    #[test]
2800    fn utf8_invalid_sequence_emits_replacement() {
2801        let mut parser = InputParser::new();
2802
2803        // 0xE0 is a start of 3-byte sequence.
2804        // 0x41 ('A') is not a valid continuation byte.
2805        // Should emit Replacement Character then 'A'.
2806        let events = parser.parse(&[0xE0, 0x41]);
2807        assert_eq!(events.len(), 2);
2808
2809        match &events[0] {
2810            Event::Key(k) => assert_eq!(k.code, KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
2811            _ => panic!("Expected replacement character"),
2812        }
2813
2814        match &events[1] {
2815            Event::Key(k) => assert_eq!(k.code, KeyCode::Char('A')),
2816            _ => panic!("Expected character 'A'"),
2817        }
2818    }
2819
2820    #[test]
2821    fn utf8_invalid_lead_emits_replacement() {
2822        let mut parser = InputParser::new();
2823
2824        // 0xC0 is an invalid UTF-8 lead byte (overlong sequence).
2825        let events = parser.parse(&[0xC0, b'a']);
2826        assert!(
2827            matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
2828            "Expected replacement for invalid lead"
2829        );
2830        assert!(
2831            events
2832                .iter()
2833                .any(|e| matches!(e, Event::Key(k) if k.code == KeyCode::Char('a'))),
2834            "Expected subsequent ASCII to be preserved"
2835        );
2836
2837        // 0xF5 is an out-of-range UTF-8 lead byte.
2838        let events = parser.parse(&[0xF5, b'b']);
2839        assert!(
2840            matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
2841            "Expected replacement for out-of-range lead"
2842        );
2843        assert!(
2844            events
2845                .iter()
2846                .any(|e| matches!(e, Event::Key(k) if k.code == KeyCode::Char('b'))),
2847            "Expected subsequent ASCII to be preserved"
2848        );
2849    }
2850}
2851
2852#[cfg(test)]
2853mod proptest_fuzz {
2854    use super::*;
2855    use proptest::prelude::*;
2856
2857    // ── Strategy helpers ────────────────────────────────────────────────
2858    // Avoid turbofish inside proptest! macro (Rust 2024 edition compat).
2859
2860    fn arb_byte() -> impl Strategy<Value = u8> {
2861        any::<u8>()
2862    }
2863
2864    fn arb_byte_vec(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
2865        prop::collection::vec(arb_byte(), 0..=max_len)
2866    }
2867
2868    /// Generate a well-formed CSI sequence: ESC [ <params> <final byte>.
2869    fn csi_sequence() -> impl Strategy<Value = Vec<u8>> {
2870        let params = prop::collection::vec(0x30u8..=0x3F, 0..=20);
2871        let final_byte = 0x40u8..=0x7E;
2872        (params, final_byte).prop_map(|(p, f)| {
2873            let mut buf = vec![0x1B, b'['];
2874            buf.extend_from_slice(&p);
2875            buf.push(f);
2876            buf
2877        })
2878    }
2879
2880    /// Generate an OSC sequence: ESC ] <content> ST.
2881    fn osc_sequence() -> impl Strategy<Value = Vec<u8>> {
2882        let content = prop::collection::vec(0x20u8..=0x7E, 0..=64);
2883        let terminator = prop_oneof![
2884            Just(vec![0x1B, b'\\']), // ESC backslash
2885            Just(vec![0x07]),        // BEL
2886        ];
2887        (content, terminator).prop_map(|(c, t)| {
2888            let mut buf = vec![0x1B, b']'];
2889            buf.extend_from_slice(&c);
2890            buf.extend_from_slice(&t);
2891            buf
2892        })
2893    }
2894
2895    /// Generate an SS3 sequence: ESC O <final byte>.
2896    fn ss3_sequence() -> impl Strategy<Value = Vec<u8>> {
2897        (0x40u8..=0x7E).prop_map(|f| vec![0x1B, b'O', f])
2898    }
2899
2900    /// Generate a bracketed paste: ESC[200~ <content> ESC[201~.
2901    fn paste_sequence() -> impl Strategy<Value = Vec<u8>> {
2902        prop::collection::vec(0x20u8..=0x7E, 0..=128).prop_map(|content| {
2903            let mut buf = vec![0x1B, b'[', b'2', b'0', b'0', b'~'];
2904            buf.extend_from_slice(&content);
2905            buf.extend_from_slice(b"\x1b[201~");
2906            buf
2907        })
2908    }
2909
2910    /// Generate structured adversarial input: mix of valid sequences and random bytes.
2911    fn mixed_adversarial() -> impl Strategy<Value = Vec<u8>> {
2912        let fragment = prop_oneof![
2913            csi_sequence(),
2914            osc_sequence(),
2915            ss3_sequence(),
2916            paste_sequence(),
2917            arb_byte_vec(16),                            // random bytes
2918            Just(vec![0x1B]),                            // bare ESC
2919            Just(vec![0x1B, b'[']),                      // unterminated CSI
2920            Just(vec![0x1B, b']']),                      // unterminated OSC
2921            prop::collection::vec(0x80u8..=0xFF, 1..=4), // high bytes
2922        ];
2923        prop::collection::vec(fragment, 1..=8)
2924            .prop_map(|frags| frags.into_iter().flatten().collect())
2925    }
2926
2927    // ── Property tests ─────────────────────────────────────────────────
2928
2929    proptest! {
2930        /// Random bytes must never panic.
2931        #[test]
2932        fn random_bytes_never_panic(input in arb_byte_vec(512)) {
2933            let mut parser = InputParser::new();
2934            let _ = parser.parse(&input);
2935        }
2936
2937        /// After parsing any input, the parser must be reusable for normal keys.
2938        #[test]
2939        fn parser_recovers_after_garbage(input in arb_byte_vec(256)) {
2940            let mut parser = InputParser::new();
2941            let _ = parser.parse(&input);
2942
2943            // Feed a clean known sequence (letter 'z') after the garbage.
2944            let events = parser.parse(b"z");
2945            // Parser must not panic. We can't assert exact events because
2946            // the parser may still be mid-sequence, but it must not panic.
2947            let _ = events;
2948        }
2949
2950        /// Structured mixed input (valid sequences + garbage) must never panic.
2951        #[test]
2952        fn mixed_sequences_never_panic(input in mixed_adversarial()) {
2953            let mut parser = InputParser::new();
2954            let _ = parser.parse(&input);
2955        }
2956
2957        /// All generated events must be valid (non-panicking Debug).
2958        #[test]
2959        fn events_are_well_formed(input in arb_byte_vec(256)) {
2960            let mut parser = InputParser::new();
2961            let events = parser.parse(&input);
2962            for event in &events {
2963                // Exercise Debug impl — catches inconsistent internal state.
2964                let _ = format!("{event:?}");
2965            }
2966        }
2967
2968        /// CSI sequences never produce more events than bytes fed.
2969        #[test]
2970        fn csi_event_count_bounded(seq in csi_sequence()) {
2971            let mut parser = InputParser::new();
2972            let events = parser.parse(&seq);
2973            prop_assert!(events.len() <= seq.len(),
2974                "Got {} events from {} bytes", events.len(), seq.len());
2975        }
2976
2977        /// OSC sequences never produce more events than bytes fed.
2978        #[test]
2979        fn osc_event_count_bounded(seq in osc_sequence()) {
2980            let mut parser = InputParser::new();
2981            let events = parser.parse(&seq);
2982            prop_assert!(events.len() <= seq.len(),
2983                "Got {} events from {} bytes", events.len(), seq.len());
2984        }
2985
2986        /// Paste content is always bounded by MAX_PASTE_LEN.
2987        #[test]
2988        fn paste_content_bounded(content in prop::collection::vec(arb_byte(), 0..=2048)) {
2989            let mut parser = InputParser::new();
2990            let mut input = vec![0x1B, b'[', b'2', b'0', b'0', b'~'];
2991            input.extend_from_slice(&content);
2992            input.extend_from_slice(b"\x1b[201~");
2993
2994            let events = parser.parse(&input);
2995            for event in &events {
2996                if let Event::Paste(p) = event {
2997                    prop_assert!(p.text.len() <= MAX_PASTE_LEN,
2998                        "Paste text {} exceeds limit {}", p.text.len(), MAX_PASTE_LEN);
2999                }
3000            }
3001        }
3002
3003        /// Feeding input byte-by-byte yields same events as feeding all at once.
3004        #[test]
3005        fn incremental_matches_bulk(input in arb_byte_vec(128)) {
3006            let mut bulk_parser = InputParser::new();
3007            let bulk_events = bulk_parser.parse(&input);
3008
3009            let mut incr_parser = InputParser::new();
3010            let mut incr_events = Vec::new();
3011            for byte in &input {
3012                incr_events.extend(incr_parser.parse(std::slice::from_ref(byte)));
3013            }
3014
3015            let bulk_dbg: Vec<String> = bulk_events.iter().map(|e| format!("{e:?}")).collect();
3016            let incr_dbg: Vec<String> = incr_events.iter().map(|e| format!("{e:?}")).collect();
3017            prop_assert_eq!(bulk_dbg, incr_dbg,
3018                "Bulk vs incremental mismatch for input {:?}", input);
3019        }
3020
3021        /// Repeated parsing of the same input must always produce the same result
3022        /// (parser is deterministic after reset).
3023        #[test]
3024        fn deterministic_output(input in arb_byte_vec(128)) {
3025            let mut parser1 = InputParser::new();
3026            let events1 = parser1.parse(&input);
3027
3028            let mut parser2 = InputParser::new();
3029            let events2 = parser2.parse(&input);
3030
3031            let dbg1: Vec<String> = events1.iter().map(|e| format!("{e:?}")).collect();
3032            let dbg2: Vec<String> = events2.iter().map(|e| format!("{e:?}")).collect();
3033            prop_assert_eq!(dbg1, dbg2);
3034        }
3035    }
3036
3037    // ── Targeted invariant tests (outside proptest! macro) ─────────────
3038
3039    /// After a long garbage run, parser handles a simple key within bounded time.
3040    #[test]
3041    fn no_quadratic_blowup() {
3042        let mut parser = InputParser::new();
3043
3044        // 64KB of random-ish bytes (repeating pattern).
3045        let garbage: Vec<u8> = (0..65536).map(|i| (i % 256) as u8).collect();
3046        let _ = parser.parse(&garbage);
3047
3048        // Follow with a clean key — must not take pathological time.
3049        let events = parser.parse(b"a");
3050        let _ = events; // primarily asserting no hang/panic
3051    }
3052
3053    /// Oversized CSI sequence triggers DoS protection without panic.
3054    #[test]
3055    fn oversized_csi_transitions_to_ignore() {
3056        let mut parser = InputParser::new();
3057
3058        // CSI followed by MAX_CSI_LEN+100 parameter bytes then a final byte.
3059        let mut input = vec![0x1B, b'['];
3060        input.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100));
3061        input.push(b'm');
3062
3063        let _ = parser.parse(&input);
3064
3065        // Parser must still be usable.
3066        let events = parser.parse(b"x");
3067        assert_eq!(events.len(), 1);
3068        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('x')));
3069    }
3070
3071    /// Oversized OSC sequence triggers DoS protection without panic.
3072    #[test]
3073    fn oversized_osc_transitions_to_ignore() {
3074        let mut parser = InputParser::new();
3075
3076        // OSC followed by MAX_OSC_LEN+100 content bytes then ST.
3077        let mut input = vec![0x1B, b']'];
3078        input.extend(std::iter::repeat_n(b'a', MAX_OSC_LEN + 100));
3079        input.push(0x07); // BEL terminator
3080
3081        let _ = parser.parse(&input);
3082
3083        // Parser must still be usable.
3084        let events = parser.parse(b"y");
3085        assert_eq!(events.len(), 1);
3086        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('y')));
3087    }
3088
3089    /// Rapid ESC toggling doesn't corrupt state.
3090    #[test]
3091    fn rapid_esc_toggle() {
3092        let mut parser = InputParser::new();
3093
3094        // 1000 bare ESCs in a row.
3095        let input: Vec<u8> = vec![0x1B; 1000];
3096        let _ = parser.parse(&input);
3097
3098        // Must recover for a normal key.
3099        let events = parser.parse(b"k");
3100        assert!(!events.is_empty());
3101    }
3102
3103    /// Interleaved paste start sequences without end.
3104    #[test]
3105    fn unterminated_paste_recovery() {
3106        let mut parser = InputParser::new();
3107
3108        // Start paste, but never end it — feed lots of data.
3109        let mut input = b"\x1b[200~".to_vec();
3110        input.extend(std::iter::repeat_n(b'x', 2048));
3111
3112        let _ = parser.parse(&input);
3113
3114        // Now end the paste.
3115        let events = parser.parse(b"\x1b[201~");
3116        assert!(
3117            !events.is_empty(),
3118            "Parser should emit paste event on terminator"
3119        );
3120    }
3121
3122    /// UTF-8 boundary: all possible lead bytes followed by truncation.
3123    #[test]
3124    fn truncated_utf8_lead_bytes() {
3125        let mut parser = InputParser::new();
3126
3127        // Two-byte lead (0xC0..0xDF), three-byte (0xE0..0xEF), four-byte (0xF0..0xF7)
3128        for lead in [0xC2, 0xE0, 0xF0] {
3129            let _ = parser.parse(&[lead]);
3130            // Feed a normal ASCII after the truncated lead.
3131            let events = parser.parse(b"a");
3132            // Must not panic; 'a' should eventually appear.
3133            let _ = events;
3134        }
3135    }
3136
3137    /// Null bytes mixed with valid input.
3138    #[test]
3139    fn null_bytes_interleaved() {
3140        let mut parser = InputParser::new();
3141
3142        let input = b"\x00A\x00\x1b[A\x00B\x00";
3143        let events = parser.parse(input);
3144        // Should get events for 'A', Up arrow, and 'B' (nulls handled gracefully).
3145        assert!(
3146            events.len() >= 2,
3147            "Expected at least 2 events, got {}",
3148            events.len()
3149        );
3150    }
3151
3152    // ── Additional fuzz invariant tests (bd-10i.11.3) ─────────────────
3153
3154    /// Generate an OSC 52 clipboard sequence with arbitrary base64 payload.
3155    fn osc52_sequence() -> impl Strategy<Value = Vec<u8>> {
3156        let selector = prop_oneof![Just(b'c'), Just(b'p'), Just(b's')];
3157        // Generate valid base64 characters with occasional invalid ones
3158        let payload = prop::collection::vec(
3159            prop_oneof![
3160                0x41u8..=0x5A, // A-Z
3161                0x61u8..=0x7A, // a-z
3162                0x30u8..=0x39, // 0-9
3163                Just(b'+'),
3164                Just(b'/'),
3165                Just(b'='),
3166            ],
3167            0..=128,
3168        );
3169        let terminator = prop_oneof![
3170            Just(vec![0x1B, b'\\']), // ESC backslash (ST)
3171            Just(vec![0x07]),        // BEL
3172        ];
3173        (selector, payload, terminator).prop_map(|(sel, pay, term)| {
3174            let mut buf = vec![0x1B, b']', b'5', b'2', b';', sel, b';'];
3175            buf.extend_from_slice(&pay);
3176            buf.extend_from_slice(&term);
3177            buf
3178        })
3179    }
3180
3181    /// Generate an SGR mouse sequence.
3182    fn sgr_mouse_sequence() -> impl Strategy<Value = Vec<u8>> {
3183        let button_code = 0u16..128;
3184        let x = 1u16..300;
3185        let y = 1u16..100;
3186        let final_byte = prop_oneof![Just(b'M'), Just(b'm')];
3187        (button_code, x, y, final_byte)
3188            .prop_map(|(btn, x, y, fb)| format!("\x1b[<{btn};{x};{y}{}", fb as char).into_bytes())
3189    }
3190
3191    /// Generate Kitty keyboard protocol sequences.
3192    fn kitty_keyboard_sequence() -> impl Strategy<Value = Vec<u8>> {
3193        let keycode = prop_oneof![
3194            0x20u32..0x7F,       // ASCII range
3195            0x57344u32..0x57400, // Kitty special keys
3196            0x100u32..0x200,     // Extended range
3197        ];
3198        let modifier = 1u32..16;
3199        let kind = prop_oneof![Just(1u32), Just(2u32), Just(3u32)]; // press/repeat/release
3200        (keycode, prop::option::of(modifier), prop::option::of(kind)).prop_map(
3201            |(kc, mods, kind)| match (mods, kind) {
3202                (Some(m), Some(k)) => format!("\x1b[{kc};{m}:{k}u").into_bytes(),
3203                (Some(m), None) => format!("\x1b[{kc};{m}u").into_bytes(),
3204                _ => format!("\x1b[{kc}u").into_bytes(),
3205            },
3206        )
3207    }
3208
3209    proptest! {
3210        // --- OSC 52 clipboard tests ---
3211
3212        /// OSC 52 clipboard sequences never panic.
3213        #[test]
3214        fn osc52_never_panics(seq in osc52_sequence()) {
3215            let mut parser = InputParser::new();
3216            let events = parser.parse(&seq);
3217            // If parsed, should be a Clipboard event
3218            for event in &events {
3219                if let Event::Clipboard(c) = event {
3220                    prop_assert!(!c.content.is_empty() || c.content.is_empty(),
3221                        "Clipboard event must have a content field");
3222                }
3223            }
3224        }
3225
3226        /// OSC 52 with corrupt base64 doesn't panic.
3227        #[test]
3228        fn osc52_corrupt_base64_safe(payload in arb_byte_vec(128)) {
3229            let mut parser = InputParser::new();
3230            let mut input = b"\x1b]52;c;".to_vec();
3231            input.extend_from_slice(&payload);
3232            input.push(0x07); // BEL terminator
3233            let _ = parser.parse(&input);
3234        }
3235
3236        // --- SGR mouse tests ---
3237
3238        /// All SGR mouse sequences parse without panicking.
3239        #[test]
3240        fn sgr_mouse_never_panics(seq in sgr_mouse_sequence()) {
3241            let mut parser = InputParser::new();
3242            let events = parser.parse(&seq);
3243            for event in &events {
3244                // Verify events are well-formed (exercises Debug impl)
3245                let _ = format!("{event:?}");
3246            }
3247        }
3248
3249        /// SGR mouse with extreme coordinates doesn't overflow.
3250        #[test]
3251        fn sgr_mouse_extreme_coords(
3252            btn in 0u16..128,
3253            x in 0u16..=65535,
3254            y in 0u16..=65535,
3255        ) {
3256            let mut parser = InputParser::new();
3257            let input = format!("\x1b[<{btn};{x};{y}M").into_bytes();
3258            let events = parser.parse(&input);
3259            for event in &events {
3260                if let Event::Mouse(m) = event {
3261                    prop_assert!(m.x <= x, "Mouse x {} > input x {}", m.x, x);
3262                    prop_assert!(m.y <= y, "Mouse y {} > input y {}", m.y, y);
3263                }
3264            }
3265        }
3266
3267        // --- Kitty keyboard protocol tests ---
3268
3269        /// Kitty keyboard sequences never panic.
3270        #[test]
3271        fn kitty_keyboard_never_panics(seq in kitty_keyboard_sequence()) {
3272            let mut parser = InputParser::new();
3273            let _ = parser.parse(&seq);
3274        }
3275
3276        // --- State boundary tests ---
3277
3278        /// Truncated CSI followed by new valid sequence works correctly.
3279        #[test]
3280        fn truncated_csi_then_valid(
3281            params in prop::collection::vec(0x30u8..=0x3F, 1..=10),
3282            valid_char in 0x20u8..0x7F,
3283        ) {
3284            let mut parser = InputParser::new();
3285
3286            // Send truncated CSI (no final byte)
3287            let mut partial = vec![0x1B, b'['];
3288            partial.extend_from_slice(&params);
3289            let _ = parser.parse(&partial);
3290
3291            // Now send a fresh ESC sequence that should reset state
3292            let events = parser.parse(&[0x1B, b'[', b'A']); // Up arrow
3293            // Parser should eventually emit events (possibly including
3294            // interpretation of partial as complete)
3295            let _ = events;
3296
3297            // Verify recovery with a simple key
3298            let events = parser.parse(&[valid_char]);
3299            let _ = events;
3300        }
3301
3302        /// Truncated OSC followed by new valid sequence works.
3303        #[test]
3304        fn truncated_osc_then_valid(
3305            content in prop::collection::vec(0x20u8..=0x7E, 1..=32),
3306        ) {
3307            let mut parser = InputParser::new();
3308
3309            // Send unterminated OSC
3310            let mut partial = vec![0x1B, b']'];
3311            partial.extend_from_slice(&content);
3312            let _ = parser.parse(&partial);
3313
3314            // Send a new ESC to interrupt, then a valid key
3315            let events = parser.parse(b"\x1bz");
3316            let _ = events;
3317        }
3318
3319        // --- Near-limit tests ---
3320
3321        /// CSI sequence just under MAX_CSI_LEN produces events.
3322        #[test]
3323        fn csi_near_limit_produces_event(
3324            fill_byte in 0x30u8..=0x39, // digit parameter bytes
3325        ) {
3326            let mut parser = InputParser::new();
3327
3328            let mut input = vec![0x1B, b'['];
3329            // Fill to just under limit
3330            input.extend(std::iter::repeat_n(fill_byte, MAX_CSI_LEN - 1));
3331            input.push(b'm'); // final byte (SGR)
3332
3333            let events = parser.parse(&input);
3334            // Should NOT have been ignored (under limit)
3335            // The sequence is valid structurally even if params are nonsensical
3336            let _ = events;
3337
3338            // Parser should still work
3339            let events = parser.parse(b"a");
3340            prop_assert!(!events.is_empty(), "Parser stuck after near-limit CSI");
3341        }
3342
3343        /// OSC sequence just under MAX_OSC_LEN still processes.
3344        #[test]
3345        fn osc_near_limit_processes(
3346            fill_byte in 0x20u8..=0x7E,
3347        ) {
3348            let mut parser = InputParser::new();
3349
3350            let mut input = vec![0x1B, b']'];
3351            input.extend(std::iter::repeat_n(fill_byte, MAX_OSC_LEN - 1));
3352            input.push(0x07); // BEL terminator
3353
3354            let _ = parser.parse(&input);
3355
3356            // Parser should still work
3357            let events = parser.parse(b"b");
3358            prop_assert!(!events.is_empty(), "Parser stuck after near-limit OSC");
3359        }
3360
3361        // --- Consecutive paste tests ---
3362
3363        /// Multiple back-to-back paste sequences all emit events.
3364        #[test]
3365        fn consecutive_pastes_emit_events(count in 2usize..=5) {
3366            let mut parser = InputParser::new();
3367            let mut input = Vec::new();
3368
3369            for i in 0..count {
3370                input.extend_from_slice(b"\x1b[200~");
3371                input.extend_from_slice(format!("paste_{i}").as_bytes());
3372                input.extend_from_slice(b"\x1b[201~");
3373            }
3374
3375            let events = parser.parse(&input);
3376            let paste_events: Vec<_> = events.iter()
3377                .filter(|e| matches!(e, Event::Paste(_)))
3378                .collect();
3379
3380            prop_assert_eq!(paste_events.len(), count,
3381                "Expected {} paste events, got {}", count, paste_events.len());
3382        }
3383
3384        /// Paste with invalid UTF-8 bytes doesn't panic.
3385        #[test]
3386        fn paste_with_invalid_utf8(content in arb_byte_vec(256)) {
3387            let mut parser = InputParser::new();
3388            let mut input = b"\x1b[200~".to_vec();
3389            input.extend_from_slice(&content);
3390            input.extend_from_slice(b"\x1b[201~");
3391
3392            let events = parser.parse(&input);
3393            for event in &events {
3394                if let Event::Paste(p) = event {
3395                    // Text should be valid UTF-8 (lossy conversion happens internally)
3396                    prop_assert!(p.text.is_char_boundary(0), "Paste text is not valid UTF-8");
3397                }
3398            }
3399        }
3400
3401        // --- Recovery invariants ---
3402
3403        /// After any arbitrary input, feeding ESC then a known key recovers.
3404        #[test]
3405        fn recovery_via_esc_reset(garbage in arb_byte_vec(256)) {
3406            let mut parser = InputParser::new();
3407            let _ = parser.parse(&garbage);
3408
3409            // Terminate any pending OSC (BEL works from any OSC sub-state),
3410            // then ESC to flush any other intermediate state.
3411            let _ = parser.parse(b"\x07\x1b\\\x1b");
3412            let _ = parser.parse(b"\x1b");
3413
3414            // Now feed a clean character.
3415            let _ = parser.parse(b"z");
3416
3417            // Feed one more clean character to verify.
3418            let events = parser.parse(b"q");
3419            // After terminating all pending sequences and feeding clean input,
3420            // the parser must produce events.
3421            prop_assert!(!events.is_empty(),
3422                "Parser did not recover after garbage + reset");
3423        }
3424    }
3425}