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