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 key_code_str = key_part.split(':').next().unwrap_or("");
754        let key_code: u32 = key_code_str.parse().ok()?;
755
756        let mod_part = parts.next().unwrap_or("");
757        let (modifiers, kind) = Self::kitty_modifiers_and_kind(mod_part);
758
759        let code = Self::kitty_keycode_to_keycode(key_code)?;
760        Some(Event::Key(
761            KeyEvent::new(code)
762                .with_modifiers(modifiers)
763                .with_kind(kind),
764        ))
765    }
766
767    fn kitty_modifiers_and_kind(mod_part: &str) -> (Modifiers, KeyEventKind) {
768        if mod_part.is_empty() {
769            return (Modifiers::NONE, KeyEventKind::Press);
770        }
771
772        let mut parts = mod_part.split(':');
773        let mod_value: u32 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
774        let kind_value: u32 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
775
776        let modifiers = Self::modifiers_from_xterm(mod_value);
777        let kind = match kind_value {
778            2 => KeyEventKind::Repeat,
779            3 => KeyEventKind::Release,
780            _ => KeyEventKind::Press,
781        };
782
783        (modifiers, kind)
784    }
785
786    fn kitty_keycode_to_keycode(key_code: u32) -> Option<KeyCode> {
787        match key_code {
788            // Standard ASCII keys
789            9 => Some(KeyCode::Tab),
790            13 => Some(KeyCode::Enter),
791            27 => Some(KeyCode::Escape),
792            8 | 127 => Some(KeyCode::Backspace),
793            // Kitty keyboard protocol extended keys (CSI u)
794            57_344 => Some(KeyCode::Escape),
795            57_345 => Some(KeyCode::Enter),
796            57_346 => Some(KeyCode::Tab),
797            57_347 => Some(KeyCode::Backspace),
798            57_348 => Some(KeyCode::Insert),
799            57_349 => Some(KeyCode::Delete),
800            57_350 => Some(KeyCode::Left),
801            57_351 => Some(KeyCode::Right),
802            57_352 => Some(KeyCode::Up),
803            57_353 => Some(KeyCode::Down),
804            57_354 => Some(KeyCode::PageUp),
805            57_355 => Some(KeyCode::PageDown),
806            57_356 => Some(KeyCode::Home),
807            57_357 => Some(KeyCode::End),
808            // F1-F24 (57_364-57_387)
809            57_364..=57_387 => {
810                // Safety: range is [57_364, 57_387], so (key_code - 57_364 + 1) is [1, 24]
811                // which fits in u8. We use debug_assert to catch any future range changes.
812                let f_num = key_code - 57_364 + 1;
813                debug_assert!(f_num <= 24, "F-key number {f_num} exceeds F24");
814                Some(KeyCode::F(f_num as u8))
815            }
816            // Reserved/unhandled Kitty keycodes return None
817            57_358..=57_363 | 57_388..=63_743 => None,
818            // Unicode codepoints
819            _ => char::from_u32(key_code).map(KeyCode::Char),
820        }
821    }
822
823    fn modifiers_from_xterm(value: u32) -> Modifiers {
824        // xterm modifier encoding: value = 1 + modifier_bits
825        // Shift=1, Alt=2, Ctrl=4, Super=8
826        let bits = value.saturating_sub(1);
827        let mut mods = Modifiers::NONE;
828        if bits & 1 != 0 {
829            mods |= Modifiers::SHIFT;
830        }
831        if bits & 2 != 0 {
832            mods |= Modifiers::ALT;
833        }
834        if bits & 4 != 0 {
835            mods |= Modifiers::CTRL;
836        }
837        if bits & 8 != 0 {
838            mods |= Modifiers::SUPER;
839        }
840        mods
841    }
842
843    /// Create a key event with modifiers (and kitty event kind) from CSI params.
844    fn key_with_modifiers(&self, code: KeyCode, params: &[u8]) -> KeyEvent {
845        let (mods, kind) = self.parse_modifier_param(params);
846        KeyEvent::new(code).with_modifiers(mods).with_kind(kind)
847    }
848
849    /// Parse SGR mouse protocol events.
850    fn parse_sgr_mouse(&self, params: &[u8], final_byte: u8) -> Option<Event> {
851        // Format: CSI < button ; x ; y M|m
852        // Skip the leading '<'
853        let params = &params[1..];
854        let s = std::str::from_utf8(params).ok()?;
855        let mut parts = s.split(';');
856
857        // Accept numeric prefixes in each token so sequences with sub-params
858        // (e.g. `10:0`) still decode to their base coordinate/button values.
859        let button_code_u32 = Self::parse_u32_prefix(parts.next()?)?;
860        let button_code = button_code_u32.min(u16::MAX as u32) as u16;
861        let x_raw = Self::parse_i32_prefix(parts.next()?)?;
862        let y_raw = Self::parse_i32_prefix(parts.next()?)?;
863
864        // Decode button and modifiers
865        let (button, mods) = self.decode_mouse_button(button_code);
866
867        let kind = if final_byte == b'M' {
868            if button_code & 64 != 0 {
869                // Scroll event: bit 6 (64) is set
870                // bits 0-1 determine direction: 0=up, 1=down, 2=left, 3=right
871                match button_code & 3 {
872                    0 => MouseEventKind::ScrollUp,
873                    1 => MouseEventKind::ScrollDown,
874                    2 => MouseEventKind::ScrollLeft,
875                    _ => MouseEventKind::ScrollRight,
876                }
877            } else if button_code & 32 != 0 {
878                // Motion event (bit 5 set)
879                // bits 0-1: 0=left, 1=middle, 2=right, 3=no button (moved)
880                if button_code & 3 == 3 {
881                    MouseEventKind::Moved
882                } else {
883                    MouseEventKind::Drag(button)
884                }
885            } else if (button_code & 3) == 3 {
886                // Compatibility: some terminals emit release as uppercase 'M'
887                // with button code 3 instead of lowercase 'm'.
888                MouseEventKind::Up(MouseButton::Left)
889            } else {
890                MouseEventKind::Down(button)
891            }
892        } else {
893            MouseEventKind::Up(button)
894        };
895
896        Some(Event::Mouse(MouseEvent {
897            kind,
898            x: Self::normalize_sgr_coord(x_raw),
899            y: Self::normalize_sgr_coord(y_raw),
900            modifiers: mods,
901        }))
902    }
903
904    #[inline]
905    fn parse_u32_prefix(token: &str) -> Option<u32> {
906        let bytes = token.as_bytes();
907        let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
908        if digits == 0 {
909            return None;
910        }
911        token[..digits].parse().ok()
912    }
913
914    #[inline]
915    fn parse_i32_prefix(token: &str) -> Option<i32> {
916        let bytes = token.as_bytes();
917        if bytes.is_empty() {
918            return None;
919        }
920        let start = if bytes[0] == b'-' || bytes[0] == b'+' {
921            1
922        } else {
923            0
924        };
925        let digits = bytes[start..]
926            .iter()
927            .take_while(|b| b.is_ascii_digit())
928            .count();
929        if digits == 0 {
930            return None;
931        }
932        token[..start + digits].parse().ok()
933    }
934
935    #[inline]
936    fn normalize_sgr_coord(raw: i32) -> u16 {
937        if raw <= 1 {
938            return 0;
939        }
940        let zero_indexed = raw - 1;
941        zero_indexed.min(i32::from(u16::MAX)) as u16
942    }
943
944    /// Parse legacy xterm/rxvt 1015 mouse events: `CSI Cb;Cx;Cy M`.
945    ///
946    /// This acts as a compatibility fallback for terminals that don't emit SGR
947    /// mouse (`CSI < ... M/m`) despite mouse capture being enabled.
948    fn parse_legacy_mouse(&self, params: &[u8]) -> Option<Event> {
949        if params.is_empty() || params.starts_with(b"<") {
950            return None;
951        }
952
953        let s = std::str::from_utf8(params).ok()?;
954        let mut parts = s.split(';');
955        let button_code: u16 = parts.next()?.parse().ok()?;
956        let x: u16 = parts.next()?.parse().ok()?;
957        let y: u16 = parts.next()?.parse().ok()?;
958        // Reject if shape doesn't match exactly Cb;Cx;Cy.
959        if parts.next().is_some() {
960            return None;
961        }
962
963        let (button, mods) = self.decode_mouse_button(button_code);
964        let kind = if button_code & 64 != 0 {
965            // Scroll: bit 6 set, direction in low bits.
966            match button_code & 3 {
967                0 => MouseEventKind::ScrollUp,
968                1 => MouseEventKind::ScrollDown,
969                2 => MouseEventKind::ScrollLeft,
970                _ => MouseEventKind::ScrollRight,
971            }
972        } else if button_code & 32 != 0 {
973            // Motion: bit 5 set.
974            if button_code & 3 == 3 {
975                MouseEventKind::Moved
976            } else {
977                MouseEventKind::Drag(button)
978            }
979        } else if (button_code & 3) == 3 {
980            // Legacy release doesn't identify which button was released.
981            MouseEventKind::Up(MouseButton::Left)
982        } else {
983            MouseEventKind::Down(button)
984        };
985
986        Some(Event::Mouse(MouseEvent {
987            kind,
988            x: x.saturating_sub(1),
989            y: y.saturating_sub(1),
990            modifiers: mods,
991        }))
992    }
993
994    /// Decode mouse button code to button and modifiers.
995    fn decode_mouse_button(&self, code: u16) -> (MouseButton, Modifiers) {
996        let button = match code & 0b11 {
997            0 => MouseButton::Left,
998            1 => MouseButton::Middle,
999            2 => MouseButton::Right,
1000            _ => MouseButton::Left,
1001        };
1002
1003        let mut mods = Modifiers::NONE;
1004        if code & 4 != 0 {
1005            mods |= Modifiers::SHIFT;
1006        }
1007        if code & 8 != 0 {
1008            mods |= Modifiers::ALT;
1009        }
1010        if code & 16 != 0 {
1011            mods |= Modifiers::CTRL;
1012        }
1013
1014        (button, mods)
1015    }
1016
1017    /// Process SS3 (ESC O) sequences.
1018    fn process_ss3(&mut self, byte: u8) -> Option<Event> {
1019        // Robustness: ESC restarts sequence
1020        if byte == 0x1B {
1021            self.state = ParserState::Escape;
1022            return None;
1023        }
1024
1025        self.state = ParserState::Ground;
1026
1027        let code = match byte {
1028            b'P' => KeyCode::F(1),
1029            b'Q' => KeyCode::F(2),
1030            b'R' => KeyCode::F(3),
1031            b'S' => KeyCode::F(4),
1032            b'A' => KeyCode::Up,
1033            b'B' => KeyCode::Down,
1034            b'C' => KeyCode::Right,
1035            b'D' => KeyCode::Left,
1036            b'H' => KeyCode::Home,
1037            b'F' => KeyCode::End,
1038            _ => return None,
1039        };
1040
1041        Some(Event::Key(KeyEvent::new(code)))
1042    }
1043
1044    /// Process OSC start.
1045    fn process_osc(&mut self, byte: u8) -> Option<Event> {
1046        // Handle ESC as potential ST terminator (ESC \) - don't add to buffer
1047        if byte == 0x1B {
1048            self.state = ParserState::OscEscape;
1049            return None;
1050        }
1051
1052        self.buffer.push(byte);
1053
1054        match byte {
1055            // BEL terminates immediately
1056            0x07 => {
1057                self.state = ParserState::Ground;
1058                self.parse_osc_sequence()
1059            }
1060            // Continue collecting
1061            _ => {
1062                self.state = ParserState::OscContent;
1063                None
1064            }
1065        }
1066    }
1067
1068    /// Process OSC content.
1069    fn process_osc_content(&mut self, byte: u8) -> Option<Event> {
1070        // Handle ESC (0x1B) as potential terminator or reset
1071        if byte == 0x1B {
1072            self.state = ParserState::OscEscape;
1073            return None;
1074        }
1075
1076        // Robustness: Abort on control characters (except BEL) to prevent swallowing logs
1077        if byte < 0x20 && byte != 0x07 {
1078            self.state = ParserState::Ground;
1079            self.buffer.clear();
1080            return self.process_ground(byte);
1081        }
1082
1083        match byte {
1084            // BEL terminates. Checked before the DoS cap (mirroring the ESC/ST
1085            // path above): a terminator arriving exactly at the cap must still
1086            // end the sequence instead of being dropped into OscIgnore, which
1087            // would swallow all subsequent printable input.
1088            0x07 => {
1089                self.state = ParserState::Ground;
1090                self.parse_osc_sequence()
1091            }
1092            // Continue collecting (content accumulation is capped)
1093            _ => {
1094                if self.buffer.len() >= MAX_OSC_LEN {
1095                    self.state = ParserState::OscIgnore;
1096                    self.buffer.clear();
1097                    return None;
1098                }
1099                self.buffer.push(byte);
1100                None
1101            }
1102        }
1103    }
1104
1105    /// Process ESC inside OSC (checking for ST terminator).
1106    fn process_osc_escape(&mut self, byte: u8) -> Option<Event> {
1107        if byte == b'\\' {
1108            // ST (String Terminator) found
1109            self.state = ParserState::Ground;
1110            self.parse_osc_sequence()
1111        } else if byte == 0x1B {
1112            // ESC ESC - treat second ESC as start of new sequence (restart)
1113            self.state = ParserState::Escape;
1114            self.buffer.clear();
1115            None
1116        } else {
1117            // ESC followed by something else.
1118            // Strict ANSI would say the OSC is cancelled by the ESC.
1119            // We treat this as a restart of parsing at the *current* byte,
1120            // effectively interpreting the previous ESC as a cancel.
1121
1122            self.buffer.clear();
1123            self.state = ParserState::Escape;
1124            self.process_escape(byte)
1125        }
1126    }
1127
1128    /// Ignore bytes until end of OSC sequence.
1129    fn process_osc_ignore(&mut self, byte: u8) -> Option<Event> {
1130        match byte {
1131            // BEL terminates
1132            0x07 => {
1133                self.state = ParserState::Ground;
1134                None
1135            }
1136            // ESC might start terminator or new sequence
1137            0x1B => {
1138                self.state = ParserState::OscEscape;
1139                None
1140            }
1141            // Abort on control characters to prevent swallowing logs (except DEL 0x7F)
1142            _ if byte < 0x20 => {
1143                self.state = ParserState::Ground;
1144                self.process_ground(byte)
1145            }
1146            // Continue ignoring
1147            _ => None,
1148        }
1149    }
1150
1151    /// Ignore bytes inside a DCS (`ESC P …`) string until its terminator.
1152    ///
1153    /// The content (e.g. an XTGETTCAP reply `1+r524742=8/8/8`) is discarded — a
1154    /// DCS never carries key input. Like [`Self::process_osc_ignore`], a
1155    /// non-ESC/BEL control byte aborts the string and is reprocessed, so a
1156    /// truncated/never-terminated string cannot permanently swallow real input.
1157    fn process_dcs_ignore(&mut self, byte: u8) -> Option<Event> {
1158        match byte {
1159            // BEL terminates (lenient: some terminals close strings with BEL).
1160            0x07 => {
1161                self.state = ParserState::Ground;
1162                None
1163            }
1164            // ESC may begin the ST (ESC \) terminator.
1165            0x1B => {
1166                self.state = ParserState::DcsEscape;
1167                None
1168            }
1169            // Abort on other control characters so a malformed string can't
1170            // swallow subsequent legitimate input (matches OSC-ignore).
1171            _ if byte < 0x20 => {
1172                self.state = ParserState::Ground;
1173                self.process_ground(byte)
1174            }
1175            // Consume DCS payload (hex, '=', '/', etc.).
1176            _ => None,
1177        }
1178    }
1179
1180    /// After an ESC inside a DCS string: complete on `\` (ST) or recover.
1181    fn process_dcs_escape(&mut self, byte: u8) -> Option<Event> {
1182        if byte == b'\\' {
1183            // ST found — the control string is complete and discarded (no event).
1184            self.state = ParserState::Ground;
1185            None
1186        } else if byte == 0x1B {
1187            // ESC ESC — treat the second ESC as the start of a new sequence.
1188            self.state = ParserState::Escape;
1189            None
1190        } else {
1191            // ESC followed by something else cancels the string; reprocess the
1192            // byte as a fresh escape sequence (matches OSC-escape recovery).
1193            self.state = ParserState::Escape;
1194            self.process_escape(byte)
1195        }
1196    }
1197
1198    /// Parse a complete OSC sequence.
1199    fn parse_osc_sequence(&mut self) -> Option<Event> {
1200        let seq = std::mem::take(&mut self.buffer);
1201
1202        // OSC 52 clipboard response: OSC 52 ; c ; <base64> BEL/ST
1203        if seq.starts_with(b"52;") {
1204            return self.parse_osc52_clipboard(&seq);
1205        }
1206
1207        // Other OSC sequences (e.g., OSC 8 hyperlinks) are not parsed as events
1208        None
1209    }
1210
1211    /// Parse OSC 52 clipboard response.
1212    fn parse_osc52_clipboard(&self, seq: &[u8]) -> Option<Event> {
1213        // Format: 52;c;<base64> or 52;p;<base64>
1214        let content = &seq[3..]; // Skip "52;"
1215        if content.is_empty() {
1216            return None;
1217        }
1218
1219        // OSC 52 uses clipboard selectors: c=clipboard, p=primary, s=secondary
1220        // We map all to Osc52 source type since that's how we received it
1221        let source = ClipboardSource::Osc52;
1222
1223        // Skip "c;" prefix
1224        let base64_start = content.iter().position(|&b| b == b';').map(|i| i + 1)?;
1225        let base64_data = &content[base64_start..];
1226
1227        // Decode base64 (simple implementation)
1228        let decoded = self.decode_base64(base64_data)?;
1229
1230        Some(Event::Clipboard(ClipboardEvent::new(
1231            String::from_utf8_lossy(&decoded).into_owned(),
1232            source,
1233        )))
1234    }
1235
1236    /// Simple base64 decoder.
1237    fn decode_base64(&self, input: &[u8]) -> Option<Vec<u8>> {
1238        const DECODE_TABLE: [i8; 256] = {
1239            let mut table = [-1i8; 256];
1240            let mut i = 0u8;
1241            while i < 26 {
1242                table[(b'A' + i) as usize] = i as i8;
1243                table[(b'a' + i) as usize] = (i + 26) as i8;
1244                i += 1;
1245            }
1246            let mut i = 0u8;
1247            while i < 10 {
1248                table[(b'0' + i) as usize] = (i + 52) as i8;
1249                i += 1;
1250            }
1251            table[b'+' as usize] = 62;
1252            table[b'/' as usize] = 63;
1253            table
1254        };
1255
1256        let mut output = Vec::with_capacity(input.len() * 3 / 4);
1257        let mut buffer = 0u32;
1258        let mut bits = 0u8;
1259
1260        for &byte in input {
1261            if byte == b'=' {
1262                break;
1263            }
1264            let value = DECODE_TABLE[byte as usize];
1265            if value < 0 {
1266                continue; // Skip whitespace/invalid
1267            }
1268            buffer = (buffer << 6) | (value as u32);
1269            bits += 6;
1270            if bits >= 8 {
1271                bits -= 8;
1272                output.push((buffer >> bits) as u8);
1273                buffer &= (1 << bits) - 1;
1274            }
1275        }
1276
1277        Some(output)
1278    }
1279
1280    /// Process UTF-8 continuation bytes.
1281    fn process_utf8(&mut self, byte: u8, collected: u8, expected: u8, alt: bool) -> Option<Event> {
1282        let alt_mods = if alt { Modifiers::ALT } else { Modifiers::NONE };
1283
1284        // Check for valid continuation byte
1285        if (byte & 0xC0) != 0x80 {
1286            // Invalid - return to ground and re-process the unexpected byte.
1287            // Also emit a replacement character for the invalid sequence we just aborted.
1288            self.state = ParserState::Ground;
1289
1290            // Queue the replacement event for the next iteration of the parse loop
1291            self.pending_event = self.process_ground(byte);
1292
1293            return Some(Event::Key(
1294                KeyEvent::new(KeyCode::Char(std::char::REPLACEMENT_CHARACTER))
1295                    .with_modifiers(alt_mods),
1296            ));
1297        }
1298
1299        self.utf8_buffer[collected as usize] = byte;
1300        let new_collected = collected + 1;
1301
1302        if new_collected == expected {
1303            // Complete - decode and emit
1304            self.state = ParserState::Ground;
1305            match std::str::from_utf8(&self.utf8_buffer[..expected as usize]) {
1306                Ok(s) => {
1307                    let c = s.chars().next()?;
1308                    Some(Event::Key(
1309                        KeyEvent::new(KeyCode::Char(c)).with_modifiers(alt_mods),
1310                    ))
1311                }
1312                Err(_) => Some(Event::Key(
1313                    KeyEvent::new(KeyCode::Char(std::char::REPLACEMENT_CHARACTER))
1314                        .with_modifiers(alt_mods),
1315                )),
1316            }
1317        } else {
1318            // Need more bytes
1319            self.state = ParserState::Utf8 {
1320                collected: new_collected,
1321                expected,
1322                alt,
1323            };
1324            None
1325        }
1326    }
1327
1328    /// Process bytes while in X10 mouse mode.
1329    fn process_mouse_x10(&mut self, byte: u8) -> Option<Event> {
1330        if let ParserState::MouseX10 {
1331            ref mut collected,
1332            ref mut buffer,
1333        } = self.state
1334        {
1335            buffer[*collected as usize] = byte;
1336            *collected += 1;
1337
1338            if *collected == 3 {
1339                // Copy buffer before reassigning state (borrow of self.state).
1340                let buf = *buffer;
1341                self.state = ParserState::Ground;
1342
1343                // X10 encoding: byte = value + 32.
1344                // Reject malformed packets so noise bytes do not become bogus
1345                // pointer events.
1346                if buf[0] < 32 || buf[1] < 33 || buf[2] < 33 {
1347                    return None;
1348                }
1349                let cb = buf[0].saturating_sub(32) as u16;
1350                let cx = buf[1].saturating_sub(33) as u16; // 1-based -> 0-based
1351                let cy = buf[2].saturating_sub(33) as u16;
1352
1353                let (button, mods) = self.decode_mouse_button(cb);
1354
1355                // Decode order matches the SGR path: the scroll bit (64) must
1356                // be tested BEFORE the release bits, because scroll codes 66
1357                // (left) and 67 (right) also have (cb & 3) == 2/3 and would
1358                // otherwise decode as button events.
1359                // Low 2 bits when not scrolling: 0=Btn1, 1=Btn2, 2=Btn3,
1360                // 3=Release (X10 release doesn't say which button; Left).
1361                let kind = if cb & 64 != 0 {
1362                    // Scroll event (bit 6 set); direction in bits 0-1.
1363                    match cb & 3 {
1364                        0 => MouseEventKind::ScrollUp,
1365                        1 => MouseEventKind::ScrollDown,
1366                        2 => MouseEventKind::ScrollLeft,
1367                        _ => MouseEventKind::ScrollRight,
1368                    }
1369                } else if (cb & 3) == 3 {
1370                    // Release event
1371                    MouseEventKind::Up(MouseButton::Left)
1372                } else {
1373                    // Press event
1374                    MouseEventKind::Down(button)
1375                };
1376
1377                return Some(Event::Mouse(MouseEvent {
1378                    kind,
1379                    x: cx,
1380                    y: cy,
1381                    modifiers: mods,
1382                }));
1383            }
1384        }
1385        None
1386    }
1387
1388    /// Process bytes while in paste mode.
1389    fn process_paste_byte(&mut self, byte: u8) -> Option<Event> {
1390        const END_SEQ: &[u8] = b"\x1b[201~";
1391
1392        // Logic:
1393        // 1. If we have room in paste_buffer, push it.
1394        // 2. If we are full, push to self.buffer (used as a tail tracker) to detect END_SEQ.
1395        // 3. Always check if the effective stream ends with END_SEQ.
1396
1397        if self.paste_buffer.len() < MAX_PASTE_LEN {
1398            self.paste_buffer.push(byte);
1399
1400            // Check for end sequence in paste_buffer
1401            if self.paste_buffer.ends_with(END_SEQ) {
1402                self.in_paste = false;
1403                // Remove the end sequence from content
1404                let content_len = self.paste_buffer.len() - END_SEQ.len();
1405                let content =
1406                    String::from_utf8_lossy(&self.paste_buffer[..content_len]).into_owned();
1407                self.paste_buffer.clear();
1408                return Some(Event::Paste(PasteEvent::bracketed(content)));
1409            }
1410        } else {
1411            // Buffer is full. DoS protection active.
1412            // We stop collecting content, but we MUST track the end sequence.
1413            // Use self.buffer as a sliding window for the tail.
1414
1415            self.buffer.push(byte);
1416            if self.buffer.len() > END_SEQ.len() {
1417                self.buffer.remove(0);
1418            }
1419
1420            // Check if we found the end sequence.
1421            // The sequence might be split between paste_buffer and buffer.
1422            // We only need to check the last 6 bytes.
1423            // Since `buffer` contains the most recent bytes (up to 6), and `paste_buffer` is full...
1424
1425            // Construct a view of the last 6 bytes
1426            let mut last_bytes = [0u8; 6];
1427            let tail_len = self.buffer.len();
1428            let paste_len = self.paste_buffer.len();
1429
1430            // Only proceed if we have enough total bytes to form the end sequence
1431            if tail_len + paste_len >= 6 {
1432                // Fill from buffer (reverse order)
1433                for i in 0..tail_len {
1434                    last_bytes[6 - tail_len + i] = self.buffer[i];
1435                }
1436                // Fill remaining from paste_buffer
1437                let remaining = 6 - tail_len;
1438                if remaining > 0 {
1439                    let start = paste_len - remaining;
1440                    last_bytes[..remaining]
1441                        .copy_from_slice(&self.paste_buffer[start..(remaining + start)]);
1442                }
1443
1444                if last_bytes == END_SEQ {
1445                    self.in_paste = false;
1446
1447                    // We found the end sequence.
1448                    // The content is `paste_buffer` MINUS the part of END_SEQ that was in it.
1449                    // `remaining` bytes of END_SEQ were in paste_buffer.
1450
1451                    let content_len = paste_len - remaining;
1452                    let content =
1453                        String::from_utf8_lossy(&self.paste_buffer[..content_len]).into_owned();
1454
1455                    self.paste_buffer.clear();
1456                    self.buffer.clear();
1457
1458                    return Some(Event::Paste(PasteEvent::bracketed(content)));
1459                }
1460            }
1461        }
1462
1463        None
1464    }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469    use super::*;
1470
1471    #[test]
1472    fn csi_ignore_handles_final_bytes() {
1473        let mut parser = InputParser::new();
1474
1475        // Create a very long CSI sequence terminated by '@' (0x40)
1476        // 0x40 is a valid Final Byte (ECMA-48), but our parser currently only checks A-Za-z~
1477        let mut seq = vec![0x1B, b'['];
1478        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100)); // Trigger CsiIgnore
1479        seq.push(b'@'); // Final byte
1480
1481        let events = parser.parse(&seq);
1482        assert_eq!(events.len(), 0);
1483
1484        // Feed 'a'. If '@' was correctly treated as final byte, 'a' should be parsed as 'a'.
1485        // If '@' was ignored (stayed in CsiIgnore), 'a' terminates the sequence and is swallowed.
1486        let events = parser.parse(b"a");
1487        assert_eq!(events.len(), 1, "Subsequent char 'a' was swallowed");
1488        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
1489    }
1490
1491    #[test]
1492    fn legacy_key_with_kitty_event_type_subparam_decodes_release_and_mods() {
1493        // With kitty "report event types" active, legacy functional keys carry
1494        // `modifiers:event_type`. CSI 1;1:3 A = Up RELEASE (no modifiers).
1495        let mut parser = InputParser::new();
1496        let events = parser.parse(b"\x1b[1;1:3A");
1497        assert_eq!(events.len(), 1);
1498        assert!(matches!(
1499            events[0],
1500            Event::Key(k) if k.code == KeyCode::Up
1501                && k.modifiers == Modifiers::NONE
1502                && k.kind == KeyEventKind::Release
1503        ));
1504
1505        // CSI 3;5:3 ~ = Ctrl+Delete RELEASE: Ctrl must survive the sub-param.
1506        let events = parser.parse(b"\x1b[3;5:3~");
1507        assert_eq!(events.len(), 1);
1508        assert!(matches!(
1509            events[0],
1510            Event::Key(k) if k.code == KeyCode::Delete
1511                && k.modifiers == Modifiers::CTRL
1512                && k.kind == KeyEventKind::Release
1513        ));
1514
1515        // Plain legacy form is unchanged: CSI 1;5 A = Ctrl+Up press.
1516        let events = parser.parse(b"\x1b[1;5A");
1517        assert_eq!(events.len(), 1);
1518        assert!(matches!(
1519            events[0],
1520            Event::Key(k) if k.code == KeyCode::Up
1521                && k.modifiers == Modifiers::CTRL
1522                && k.kind == KeyEventKind::Press
1523        ));
1524    }
1525
1526    #[test]
1527    fn csi_final_byte_at_exact_dos_boundary_terminates() {
1528        // The real final byte arriving when the buffer holds exactly
1529        // MAX_CSI_LEN params must terminate the sequence, not be dropped
1530        // into CsiIgnore (which would then eat the next keystroke).
1531        let mut parser = InputParser::new();
1532        let mut seq = vec![0x1B, b'['];
1533        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN));
1534        seq.push(b'A'); // final byte exactly at the cap
1535
1536        let events = parser.parse(&seq);
1537        // The oversized sequence itself yields at most a garbage-param event;
1538        // the crucial part is the parser is back in Ground.
1539        drop(events);
1540        let events = parser.parse(b"a");
1541        assert_eq!(events.len(), 1, "keystroke after boundary CSI swallowed");
1542        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
1543    }
1544
1545    #[test]
1546    fn osc_bel_terminator_at_exact_dos_boundary_terminates() {
1547        // BEL arriving when the OSC buffer is exactly at the cap must end the
1548        // sequence; entering OscIgnore here would swallow all later printables.
1549        let mut parser = InputParser::new();
1550        let mut seq = vec![0x1B, b']'];
1551        seq.extend(std::iter::repeat_n(b'x', MAX_OSC_LEN));
1552        seq.push(0x07); // BEL exactly at the cap
1553
1554        let events = parser.parse(&seq);
1555        drop(events);
1556        let events = parser.parse(b"a");
1557        assert_eq!(events.len(), 1, "keystroke after boundary OSC swallowed");
1558        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
1559    }
1560
1561    #[test]
1562    fn stray_paste_terminator_is_ignored() {
1563        // CSI 201~ without a preceding CSI 200~ must not emit an empty Paste.
1564        let mut parser = InputParser::new();
1565        let events = parser.parse(b"\x1b[201~");
1566        assert_eq!(events.len(), 0, "stray end-paste produced {events:?}");
1567
1568        // Parser still works normally afterwards.
1569        let events = parser.parse(b"\x1b[200~hi\x1b[201~");
1570        assert_eq!(events.len(), 1);
1571        assert!(matches!(&events[0], Event::Paste(p) if p.text == "hi"));
1572    }
1573
1574    #[test]
1575    fn x10_mouse_scroll_codes_not_misread_as_button_events() {
1576        // X10 cb 64+3 = 67 (scroll-right in SGR terms) has (cb & 3) == 3 and
1577        // must decode as a scroll event, not Up(Left). Encoded byte = cb + 32.
1578        let mut parser = InputParser::new();
1579        parser.set_expect_x10_mouse(true);
1580
1581        // ESC [ M (67+32) (0+33) (0+33)
1582        let events = parser.parse(&[0x1B, b'[', b'M', 67 + 32, 33, 33]);
1583        assert_eq!(events.len(), 1);
1584        assert!(matches!(
1585            events[0],
1586            Event::Mouse(m) if m.kind == MouseEventKind::ScrollRight && m.x == 0 && m.y == 0
1587        ));
1588
1589        // Scroll-up (64) still decodes as scroll.
1590        let events = parser.parse(&[0x1B, b'[', b'M', 64 + 32, 33, 33]);
1591        assert_eq!(events.len(), 1);
1592        assert!(matches!(
1593            events[0],
1594            Event::Mouse(m) if m.kind == MouseEventKind::ScrollUp
1595        ));
1596
1597        // Plain release (3) still decodes as Up(Left).
1598        let events = parser.parse(&[0x1B, b'[', b'M', 3 + 32, 33, 33]);
1599        assert_eq!(events.len(), 1);
1600        assert!(matches!(
1601            events[0],
1602            Event::Mouse(m) if m.kind == MouseEventKind::Up(MouseButton::Left)
1603        ));
1604    }
1605
1606    #[test]
1607    fn csi_first_byte_control_char_is_reprocessed_not_swallowed() {
1608        // ESC [ CR: the CR aborts the CSI and must still deliver Enter,
1609        // matching the CsiParam/CsiIgnore anti-swallow behavior.
1610        let mut parser = InputParser::new();
1611        let events = parser.parse(b"\x1b[\r");
1612        assert_eq!(events.len(), 1);
1613        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Enter));
1614    }
1615
1616    #[test]
1617    fn alt_non_ascii_decodes_as_alt_char() {
1618        // metaSendsEscape + UTF-8: Alt+é arrives as ESC 0xC3 0xA9.
1619        let mut parser = InputParser::new();
1620        let events = parser.parse(b"\x1b\xc3\xa9");
1621        assert_eq!(events.len(), 1, "Alt+\u{e9} dropped: {events:?}");
1622        assert!(matches!(
1623            events[0],
1624            Event::Key(k) if k.code == KeyCode::Char('\u{e9}')
1625                && k.modifiers == Modifiers::ALT
1626        ));
1627
1628        // Split across feeds: state must persist.
1629        let events1 = parser.parse(b"\x1b\xc3");
1630        assert_eq!(events1.len(), 0);
1631        let events2 = parser.parse(b"\xa9");
1632        assert_eq!(events2.len(), 1);
1633        assert!(matches!(
1634            events2[0],
1635            Event::Key(k) if k.code == KeyCode::Char('\u{e9}')
1636                && k.modifiers == Modifiers::ALT
1637        ));
1638
1639        // Plain UTF-8 (no ESC) stays unmodified.
1640        let events = parser.parse(b"\xc3\xa9");
1641        assert_eq!(events.len(), 1);
1642        assert!(matches!(
1643            events[0],
1644            Event::Key(k) if k.code == KeyCode::Char('\u{e9}')
1645                && k.modifiers == Modifiers::NONE
1646        ));
1647    }
1648
1649    #[test]
1650    fn ascii_characters_parsed() {
1651        let mut parser = InputParser::new();
1652
1653        let events = parser.parse(b"abc");
1654        assert_eq!(events.len(), 3);
1655        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('a')));
1656        assert!(matches!(events[1], Event::Key(k) if k.code == KeyCode::Char('b')));
1657        assert!(matches!(events[2], Event::Key(k) if k.code == KeyCode::Char('c')));
1658    }
1659
1660    #[test]
1661    fn control_characters() {
1662        let mut parser = InputParser::new();
1663
1664        // Ctrl+A
1665        let events = parser.parse(&[0x01]);
1666        assert_eq!(events.len(), 1);
1667        assert!(matches!(
1668            events[0],
1669            Event::Key(k) if k.code == KeyCode::Char('a') && k.modifiers.contains(Modifiers::CTRL)
1670        ));
1671
1672        // Backspace
1673        let events = parser.parse(&[0x7F]);
1674        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Backspace));
1675    }
1676
1677    #[test]
1678    fn arrow_keys() {
1679        let mut parser = InputParser::new();
1680
1681        assert!(matches!(
1682            parser.parse(b"\x1b[A").first(),
1683            Some(Event::Key(k)) if k.code == KeyCode::Up
1684        ));
1685        assert!(matches!(
1686            parser.parse(b"\x1b[B").first(),
1687            Some(Event::Key(k)) if k.code == KeyCode::Down
1688        ));
1689        assert!(matches!(
1690            parser.parse(b"\x1b[C").first(),
1691            Some(Event::Key(k)) if k.code == KeyCode::Right
1692        ));
1693        assert!(matches!(
1694            parser.parse(b"\x1b[D").first(),
1695            Some(Event::Key(k)) if k.code == KeyCode::Left
1696        ));
1697    }
1698
1699    #[test]
1700    fn c1_csi_arrow_keys() {
1701        let mut parser = InputParser::new();
1702
1703        assert!(matches!(
1704            parser.parse(&[0x9B, b'A']).first(),
1705            Some(Event::Key(k)) if k.code == KeyCode::Up
1706        ));
1707        assert!(matches!(
1708            parser.parse(&[0x9B, b'B']).first(),
1709            Some(Event::Key(k)) if k.code == KeyCode::Down
1710        ));
1711    }
1712
1713    #[test]
1714    fn c1_csi_mouse_sgr_protocol() {
1715        let mut parser = InputParser::new();
1716
1717        let events = parser.parse(&[0x9B, b'<', b'0', b';', b'1', b'0', b';', b'2', b'0', b'M']);
1718        assert!(matches!(
1719            events.first(),
1720            Some(Event::Mouse(m)) if m.x == 9 && m.y == 19
1721        ));
1722    }
1723
1724    #[test]
1725    fn function_keys_ss3() {
1726        let mut parser = InputParser::new();
1727
1728        assert!(matches!(
1729            parser.parse(b"\x1bOP").first(),
1730            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1731        ));
1732        assert!(matches!(
1733            parser.parse(b"\x1bOQ").first(),
1734            Some(Event::Key(k)) if k.code == KeyCode::F(2)
1735        ));
1736        assert!(matches!(
1737            parser.parse(b"\x1bOR").first(),
1738            Some(Event::Key(k)) if k.code == KeyCode::F(3)
1739        ));
1740        assert!(matches!(
1741            parser.parse(b"\x1bOS").first(),
1742            Some(Event::Key(k)) if k.code == KeyCode::F(4)
1743        ));
1744    }
1745
1746    #[test]
1747    fn function_keys_csi() {
1748        let mut parser = InputParser::new();
1749
1750        assert!(matches!(
1751            parser.parse(b"\x1b[15~").first(),
1752            Some(Event::Key(k)) if k.code == KeyCode::F(5)
1753        ));
1754        assert!(matches!(
1755            parser.parse(b"\x1b[17~").first(),
1756            Some(Event::Key(k)) if k.code == KeyCode::F(6)
1757        ));
1758    }
1759
1760    #[test]
1761    fn modifiers_in_csi() {
1762        let mut parser = InputParser::new();
1763
1764        // Shift+Up: CSI 1;2 A
1765        let events = parser.parse(b"\x1b[1;2A");
1766        assert!(matches!(
1767            events.first(),
1768            Some(Event::Key(k)) if k.code == KeyCode::Up && k.modifiers.contains(Modifiers::SHIFT)
1769        ));
1770
1771        // Ctrl+Up: CSI 1;5 A
1772        let events = parser.parse(b"\x1b[1;5A");
1773        assert!(matches!(
1774            events.first(),
1775            Some(Event::Key(k)) if k.code == KeyCode::Up && k.modifiers.contains(Modifiers::CTRL)
1776        ));
1777    }
1778
1779    #[test]
1780    fn modifiers_in_csi_alt_ctrl() {
1781        let mut parser = InputParser::new();
1782
1783        // Alt+Ctrl+Up: CSI 1;7 A (1 + ALT(2) + CTRL(4) = 7)
1784        let events = parser.parse(b"\x1b[1;7A");
1785        assert!(matches!(
1786            events.first(),
1787            Some(Event::Key(k))
1788                if k.code == KeyCode::Up
1789                    && k.modifiers.contains(Modifiers::ALT)
1790                    && k.modifiers.contains(Modifiers::CTRL)
1791        ));
1792    }
1793
1794    #[test]
1795    fn kitty_keyboard_basic_char() {
1796        let mut parser = InputParser::new();
1797
1798        let events = parser.parse(b"\x1b[97u");
1799        assert!(matches!(
1800            events.first(),
1801            Some(Event::Key(k))
1802                if k.code == KeyCode::Char('a')
1803                    && k.modifiers == Modifiers::NONE
1804                    && k.kind == KeyEventKind::Press
1805        ));
1806    }
1807
1808    #[test]
1809    fn kitty_keyboard_with_modifiers_and_kind() {
1810        let mut parser = InputParser::new();
1811
1812        // Ctrl+repeat for 'a' (modifiers=5, event_type=2)
1813        let events = parser.parse(b"\x1b[97;5:2u");
1814        assert!(matches!(
1815            events.first(),
1816            Some(Event::Key(k))
1817                if k.code == KeyCode::Char('a')
1818                    && k.modifiers.contains(Modifiers::CTRL)
1819                    && k.kind == KeyEventKind::Repeat
1820        ));
1821    }
1822
1823    #[test]
1824    fn kitty_keyboard_function_key() {
1825        let mut parser = InputParser::new();
1826
1827        let events = parser.parse(b"\x1b[57364;1u");
1828        assert!(matches!(
1829            events.first(),
1830            Some(Event::Key(k)) if k.code == KeyCode::F(1)
1831        ));
1832    }
1833
1834    #[test]
1835    fn alt_key_escapes() {
1836        let mut parser = InputParser::new();
1837
1838        let events = parser.parse(b"\x1ba");
1839        assert!(matches!(
1840            events.first(),
1841            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.modifiers.contains(Modifiers::ALT)
1842        ));
1843    }
1844
1845    #[test]
1846    fn alt_backspace() {
1847        let mut parser = InputParser::new();
1848
1849        let events = parser.parse(b"\x1b\x7f");
1850        assert!(matches!(
1851            events.first(),
1852            Some(Event::Key(k))
1853                if k.code == KeyCode::Backspace && k.modifiers.contains(Modifiers::ALT)
1854        ));
1855    }
1856
1857    #[test]
1858    fn escape_escape_resets_state() {
1859        let mut parser = InputParser::new();
1860
1861        let events = parser.parse(b"\x1b\x1b");
1862        assert!(matches!(
1863            events.first(),
1864            Some(Event::Key(k)) if k.code == KeyCode::Escape && k.modifiers.contains(Modifiers::ALT)
1865        ));
1866
1867        let events = parser.parse(b"a");
1868        assert!(matches!(
1869            events.first(),
1870            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.modifiers == Modifiers::NONE
1871        ));
1872    }
1873
1874    #[test]
1875    fn focus_events() {
1876        let mut parser = InputParser::new();
1877
1878        assert!(matches!(
1879            parser.parse(b"\x1b[I").first(),
1880            Some(Event::Focus(true))
1881        ));
1882        assert!(matches!(
1883            parser.parse(b"\x1b[O").first(),
1884            Some(Event::Focus(false))
1885        ));
1886    }
1887
1888    #[test]
1889    fn bracketed_paste() {
1890        let mut parser = InputParser::new();
1891
1892        // Start paste mode, paste content, end paste mode
1893        let events = parser.parse(b"\x1b[200~hello world\x1b[201~");
1894        assert_eq!(events.len(), 1);
1895        assert!(matches!(
1896            &events[0],
1897            Event::Paste(p) if p.text == "hello world"
1898        ));
1899    }
1900
1901    #[test]
1902    fn mouse_sgr_protocol() {
1903        let mut parser = InputParser::new();
1904
1905        // Left click at (10, 20)
1906        let events = parser.parse(b"\x1b[<0;10;20M");
1907        assert!(matches!(
1908            events.first(),
1909            Some(Event::Mouse(m)) if m.x == 9 && m.y == 19 // 0-indexed
1910        ));
1911    }
1912
1913    #[test]
1914    fn mouse_sgr_protocol_with_subparams() {
1915        let mut parser = InputParser::new();
1916
1917        // Accept numeric prefixes when terminals include sub-params.
1918        let events = parser.parse(b"\x1b[<0:0;10:0;20:0M");
1919        assert!(matches!(
1920            events.first(),
1921            Some(Event::Mouse(m))
1922                if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1923                    && m.x == 9
1924                    && m.y == 19
1925        ));
1926    }
1927
1928    #[test]
1929    fn mouse_sgr_protocol_large_coords_clamped() {
1930        let mut parser = InputParser::new();
1931
1932        // Coordinates beyond u16 range should be clamped instead of dropped.
1933        let events = parser.parse(b"\x1b[<0;70000;80000M");
1934        assert!(matches!(
1935            events.first(),
1936            Some(Event::Mouse(m))
1937                if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1938                    && m.x == u16::MAX
1939                    && m.y == u16::MAX
1940        ));
1941    }
1942
1943    #[test]
1944    fn mouse_sgr_protocol_negative_coords_clamped() {
1945        let mut parser = InputParser::new();
1946
1947        // Some pixel-mouse emitters can report negative coords near edges.
1948        // Clamp to origin rather than dropping the event.
1949        let events = parser.parse(b"\x1b[<0;-12;-3M");
1950        assert!(matches!(
1951            events.first(),
1952            Some(Event::Mouse(m))
1953                if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
1954                    && m.x == 0
1955                    && m.y == 0
1956        ));
1957    }
1958
1959    #[test]
1960    fn mouse_sgr_modifiers() {
1961        let mut parser = InputParser::new();
1962
1963        // Shift+Alt+Ctrl + left button (0 + 4 + 8 + 16 = 28)
1964        let events = parser.parse(b"\x1b[<28;3;4M");
1965        assert!(matches!(
1966            events.first(),
1967            Some(Event::Mouse(m))
1968                if m.modifiers.contains(Modifiers::SHIFT)
1969                    && m.modifiers.contains(Modifiers::ALT)
1970                    && m.modifiers.contains(Modifiers::CTRL)
1971        ));
1972    }
1973
1974    #[test]
1975    fn mouse_sgr_scroll_up() {
1976        let mut parser = InputParser::new();
1977
1978        // Scroll up: button code 64
1979        let events = parser.parse(b"\x1b[<64;5;5M");
1980        assert!(matches!(
1981            events.first(),
1982            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollUp)
1983        ));
1984    }
1985
1986    #[test]
1987    fn mouse_sgr_scroll_down() {
1988        let mut parser = InputParser::new();
1989
1990        // Scroll down: button code 65
1991        let events = parser.parse(b"\x1b[<65;5;5M");
1992        assert!(matches!(
1993            events.first(),
1994            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollDown)
1995        ));
1996    }
1997
1998    #[test]
1999    fn mouse_sgr_scroll_left() {
2000        let mut parser = InputParser::new();
2001
2002        // Scroll left: button code 66
2003        let events = parser.parse(b"\x1b[<66;5;5M");
2004        assert!(matches!(
2005            events.first(),
2006            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollLeft)
2007        ));
2008    }
2009
2010    #[test]
2011    fn mouse_sgr_scroll_right() {
2012        let mut parser = InputParser::new();
2013
2014        // Scroll right: button code 67
2015        let events = parser.parse(b"\x1b[<67;5;5M");
2016        assert!(matches!(
2017            events.first(),
2018            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::ScrollRight)
2019        ));
2020    }
2021
2022    #[test]
2023    fn mouse_sgr_drag_left() {
2024        let mut parser = InputParser::new();
2025
2026        // Drag with left button: button code 32
2027        let events = parser.parse(b"\x1b[<32;10;20M");
2028        assert!(matches!(
2029            events.first(),
2030            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Drag(MouseButton::Left))
2031        ));
2032    }
2033
2034    #[test]
2035    fn utf8_characters() {
2036        let mut parser = InputParser::new();
2037
2038        // é (U+00E9) = 0xC3 0xA9
2039        let events = parser.parse(&[0xC3, 0xA9]);
2040        assert!(matches!(
2041            events.first(),
2042            Some(Event::Key(k)) if k.code == KeyCode::Char('é')
2043        ));
2044    }
2045
2046    #[test]
2047    fn invalid_utf8_emits_replacement_then_reprocesses_byte() {
2048        let mut parser = InputParser::new();
2049
2050        // 0xE2 expects a 3-byte sequence, 0x28 is invalid continuation.
2051        let events = parser.parse(&[0xE2, 0x28]);
2052        assert_eq!(events.len(), 2);
2053        assert!(matches!(
2054            events[0],
2055            Event::Key(k) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)
2056        ));
2057        assert!(matches!(
2058            events[1],
2059            Event::Key(k) if k.code == KeyCode::Char('(')
2060        ));
2061    }
2062
2063    #[test]
2064    fn dos_protection_csi() {
2065        let mut parser = InputParser::new();
2066
2067        // Create a very long CSI sequence
2068        let mut seq = vec![0x1B, b'['];
2069        seq.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100));
2070        seq.push(b'A');
2071
2072        // DoS protection kicks in and switches to CsiIgnore
2073        // Excess bytes should be ignored, NOT leaked as characters
2074        let events = parser.parse(&seq);
2075        assert_eq!(
2076            events.len(),
2077            0,
2078            "Oversized CSI sequence should produce no events"
2079        );
2080
2081        // The key invariant: parser should be back in ground state and functional
2082        // Verify by parsing a normal sequence after the attack
2083        let events = parser.parse(b"\x1b[A");
2084        assert!(matches!(
2085            events.first(),
2086            Some(Event::Key(k)) if k.code == KeyCode::Up
2087        ));
2088    }
2089
2090    #[test]
2091    fn incomplete_csi_sequence_emits_no_event() {
2092        let mut parser = InputParser::new();
2093        let events = parser.parse(b"\x1b[");
2094        assert!(events.is_empty());
2095    }
2096
2097    #[test]
2098    fn dos_protection_paste() {
2099        let mut parser = InputParser::new();
2100
2101        // Start paste mode
2102        parser.parse(b"\x1b[200~");
2103
2104        // Paste content up to the limit
2105        let content = vec![b'x'; MAX_PASTE_LEN - 100]; // Leave room for end sequence
2106        parser.parse(&content);
2107
2108        // End paste mode
2109        let events = parser.parse(b"\x1b[201~");
2110
2111        // Should have collected content up to limit
2112        assert!(matches!(
2113            events.first(),
2114            Some(Event::Paste(p)) if p.text.len() <= MAX_PASTE_LEN
2115        ));
2116    }
2117
2118    #[test]
2119    fn dos_protection_paste_overflow_terminator() {
2120        let mut parser = InputParser::new();
2121
2122        // Start paste mode
2123        parser.parse(b"\x1b[200~");
2124
2125        // Overflow the buffer by pushing more than MAX_PASTE_LEN bytes.
2126        // DoS protection stops collecting content once buffer is full,
2127        // but continues tracking the end sequence to properly exit paste mode.
2128        let overflow = 100;
2129        let content = vec![b'a'; MAX_PASTE_LEN + overflow];
2130        parser.parse(&content);
2131
2132        // Send terminator - parser MUST detect it and exit paste mode.
2133        // Even though the buffer overflowed, the terminator detection still works.
2134        let events = parser.parse(b"\x1b[201~");
2135
2136        assert_eq!(events.len(), 1, "Should emit paste event");
2137        match &events[0] {
2138            Event::Paste(p) => {
2139                // Content is capped at MAX_PASTE_LEN due to DoS protection.
2140                // Overflow bytes are discarded but terminator is still detected.
2141                assert_eq!(
2142                    p.text.len(),
2143                    MAX_PASTE_LEN,
2144                    "Paste should be capped at MAX_PASTE_LEN bytes"
2145                );
2146                // The content should be all 'a' since we filled with 'a'
2147                assert!(p.text.chars().all(|c| c == 'a'));
2148            }
2149            _ => unreachable!("Expected Paste event"),
2150        }
2151
2152        // Verify we are back in ground state by parsing a key
2153        let events = parser.parse(b"b");
2154        assert_eq!(events.len(), 1);
2155        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('b')));
2156    }
2157
2158    #[test]
2159    fn no_panic_on_invalid_input() {
2160        let mut parser = InputParser::new();
2161
2162        // Random bytes that might trip up the parser
2163        let garbage = [0xFF, 0xFE, 0x00, 0x1B, 0x1B, 0x1B, b'[', 0xFF, b']', 0x00];
2164
2165        // Should not panic
2166        let _ = parser.parse(&garbage);
2167    }
2168
2169    #[test]
2170    fn dos_protection_paste_boundary() {
2171        let mut parser = InputParser::new();
2172        // Start paste mode
2173        parser.parse(b"\x1b[200~");
2174
2175        // Fill buffer exactly to limit
2176        let content = vec![b'x'; MAX_PASTE_LEN];
2177        parser.parse(&content);
2178
2179        // Send end sequence
2180        // This will be processed by the DoS protection fallback logic
2181        let events = parser.parse(b"\x1b[201~");
2182
2183        assert!(
2184            !events.is_empty(),
2185            "Parser trapped in paste mode after hitting limit"
2186        );
2187        assert!(matches!(events[0], Event::Paste(_)));
2188    }
2189
2190    // ── Navigation keys via CSI ~ sequences ──────────────────────────
2191
2192    #[test]
2193    fn csi_tilde_home() {
2194        let mut parser = InputParser::new();
2195        let events = parser.parse(b"\x1b[1~");
2196        assert!(matches!(
2197            events.first(),
2198            Some(Event::Key(k)) if k.code == KeyCode::Home
2199        ));
2200    }
2201
2202    #[test]
2203    fn csi_tilde_insert() {
2204        let mut parser = InputParser::new();
2205        let events = parser.parse(b"\x1b[2~");
2206        assert!(matches!(
2207            events.first(),
2208            Some(Event::Key(k)) if k.code == KeyCode::Insert
2209        ));
2210    }
2211
2212    #[test]
2213    fn csi_tilde_delete() {
2214        let mut parser = InputParser::new();
2215        let events = parser.parse(b"\x1b[3~");
2216        assert!(matches!(
2217            events.first(),
2218            Some(Event::Key(k)) if k.code == KeyCode::Delete
2219        ));
2220    }
2221
2222    #[test]
2223    fn csi_tilde_end() {
2224        let mut parser = InputParser::new();
2225        let events = parser.parse(b"\x1b[4~");
2226        assert!(matches!(
2227            events.first(),
2228            Some(Event::Key(k)) if k.code == KeyCode::End
2229        ));
2230    }
2231
2232    #[test]
2233    fn csi_tilde_page_up() {
2234        let mut parser = InputParser::new();
2235        let events = parser.parse(b"\x1b[5~");
2236        assert!(matches!(
2237            events.first(),
2238            Some(Event::Key(k)) if k.code == KeyCode::PageUp
2239        ));
2240    }
2241
2242    #[test]
2243    fn csi_tilde_page_down() {
2244        let mut parser = InputParser::new();
2245        let events = parser.parse(b"\x1b[6~");
2246        assert!(matches!(
2247            events.first(),
2248            Some(Event::Key(k)) if k.code == KeyCode::PageDown
2249        ));
2250    }
2251
2252    // ── Navigation keys via CSI H/F (xterm-style) ───────────────────
2253
2254    #[test]
2255    fn csi_home_and_end() {
2256        let mut parser = InputParser::new();
2257        assert!(matches!(
2258            parser.parse(b"\x1b[H").first(),
2259            Some(Event::Key(k)) if k.code == KeyCode::Home
2260        ));
2261        assert!(matches!(
2262            parser.parse(b"\x1b[F").first(),
2263            Some(Event::Key(k)) if k.code == KeyCode::End
2264        ));
2265    }
2266
2267    // ── SS3 Home/End ─────────────────────────────────────────────────
2268
2269    #[test]
2270    fn ss3_home_and_end() {
2271        let mut parser = InputParser::new();
2272        assert!(matches!(
2273            parser.parse(b"\x1bOH").first(),
2274            Some(Event::Key(k)) if k.code == KeyCode::Home
2275        ));
2276        assert!(matches!(
2277            parser.parse(b"\x1bOF").first(),
2278            Some(Event::Key(k)) if k.code == KeyCode::End
2279        ));
2280    }
2281
2282    // ── BackTab (Shift+Tab via CSI Z) ────────────────────────────────
2283
2284    #[test]
2285    fn backtab_csi_z() {
2286        let mut parser = InputParser::new();
2287        let events = parser.parse(b"\x1b[Z");
2288        assert!(matches!(
2289            events.first(),
2290            Some(Event::Key(k)) if k.code == KeyCode::BackTab
2291        ));
2292    }
2293
2294    // ── F7-F12 keys via CSI tilde ────────────────────────────────────
2295
2296    #[test]
2297    fn function_keys_f7_to_f12() {
2298        let mut parser = InputParser::new();
2299        assert!(matches!(
2300            parser.parse(b"\x1b[18~").first(),
2301            Some(Event::Key(k)) if k.code == KeyCode::F(7)
2302        ));
2303        assert!(matches!(
2304            parser.parse(b"\x1b[19~").first(),
2305            Some(Event::Key(k)) if k.code == KeyCode::F(8)
2306        ));
2307        assert!(matches!(
2308            parser.parse(b"\x1b[20~").first(),
2309            Some(Event::Key(k)) if k.code == KeyCode::F(9)
2310        ));
2311        assert!(matches!(
2312            parser.parse(b"\x1b[21~").first(),
2313            Some(Event::Key(k)) if k.code == KeyCode::F(10)
2314        ));
2315        assert!(matches!(
2316            parser.parse(b"\x1b[23~").first(),
2317            Some(Event::Key(k)) if k.code == KeyCode::F(11)
2318        ));
2319        assert!(matches!(
2320            parser.parse(b"\x1b[24~").first(),
2321            Some(Event::Key(k)) if k.code == KeyCode::F(12)
2322        ));
2323    }
2324
2325    // ── Modifier combinations on navigation keys ─────────────────────
2326
2327    #[test]
2328    fn ctrl_home_and_alt_end() {
2329        let mut parser = InputParser::new();
2330
2331        // Ctrl+Home: CSI 1;5 H
2332        let events = parser.parse(b"\x1b[1;5H");
2333        assert!(matches!(
2334            events.first(),
2335            Some(Event::Key(k)) if k.code == KeyCode::Home && k.modifiers.contains(Modifiers::CTRL)
2336        ));
2337
2338        // Alt+End: CSI 1;3 F
2339        let events = parser.parse(b"\x1b[1;3F");
2340        assert!(matches!(
2341            events.first(),
2342            Some(Event::Key(k)) if k.code == KeyCode::End && k.modifiers.contains(Modifiers::ALT)
2343        ));
2344    }
2345
2346    #[test]
2347    fn shift_ctrl_arrow() {
2348        let mut parser = InputParser::new();
2349
2350        // Shift+Ctrl+Right: CSI 1;6 C (modifier value 6 = 1 + Shift|Ctrl = 1 + 5)
2351        let events = parser.parse(b"\x1b[1;6C");
2352        assert!(matches!(
2353            events.first(),
2354            Some(Event::Key(k)) if k.code == KeyCode::Right
2355                && k.modifiers.contains(Modifiers::SHIFT)
2356                && k.modifiers.contains(Modifiers::CTRL)
2357        ));
2358    }
2359
2360    #[test]
2361    fn modifiers_on_tilde_keys() {
2362        let mut parser = InputParser::new();
2363
2364        // Ctrl+Delete: CSI 3;5 ~
2365        let events = parser.parse(b"\x1b[3;5~");
2366        assert!(matches!(
2367            events.first(),
2368            Some(Event::Key(k)) if k.code == KeyCode::Delete && k.modifiers.contains(Modifiers::CTRL)
2369        ));
2370
2371        // Shift+PageUp: CSI 5;2 ~
2372        let events = parser.parse(b"\x1b[5;2~");
2373        assert!(matches!(
2374            events.first(),
2375            Some(Event::Key(k)) if k.code == KeyCode::PageUp && k.modifiers.contains(Modifiers::SHIFT)
2376        ));
2377    }
2378
2379    // ── Mouse right/middle click and release ─────────────────────────
2380
2381    #[test]
2382    fn mouse_sgr_right_click() {
2383        let mut parser = InputParser::new();
2384        // Right click: button code 2
2385        let events = parser.parse(b"\x1b[<2;15;10M");
2386        assert!(matches!(
2387            events.first(),
2388            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Right))
2389                && m.x == 14 && m.y == 9
2390        ));
2391    }
2392
2393    #[test]
2394    fn mouse_sgr_middle_click() {
2395        let mut parser = InputParser::new();
2396        // Middle click: button code 1
2397        let events = parser.parse(b"\x1b[<1;5;5M");
2398        assert!(matches!(
2399            events.first(),
2400            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Middle))
2401        ));
2402    }
2403
2404    #[test]
2405    fn mouse_sgr_button_release() {
2406        let mut parser = InputParser::new();
2407        // Left button release: final byte 'm'
2408        let events = parser.parse(b"\x1b[<0;10;20m");
2409        assert!(matches!(
2410            events.first(),
2411            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Up(MouseButton::Left))
2412        ));
2413    }
2414
2415    #[test]
2416    fn mouse_sgr_button_release_uppercase_m_compat() {
2417        let mut parser = InputParser::new();
2418        // Compatibility release encoding used by some terminals:
2419        // final byte 'M' with low bits == 3.
2420        let events = parser.parse(b"\x1b[<3;10;20M");
2421        assert!(matches!(
2422            events.first(),
2423            Some(Event::Mouse(m))
2424                if matches!(m.kind, MouseEventKind::Up(MouseButton::Left))
2425                    && m.x == 9
2426                    && m.y == 19
2427        ));
2428    }
2429
2430    #[test]
2431    fn mouse_sgr_moved() {
2432        let mut parser = InputParser::new();
2433        // Mouse move (no button): button code 35 (32 | 3, bit 5 set + bits 0-1 = 3)
2434        let events = parser.parse(b"\x1b[<35;10;20M");
2435        assert!(matches!(
2436            events.first(),
2437            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Moved)
2438        ));
2439    }
2440
2441    #[test]
2442    fn mouse_sgr_with_modifiers() {
2443        let mut parser = InputParser::new();
2444        // Shift+Left click: button_code bit 2 set (shift) = 4
2445        let events = parser.parse(b"\x1b[<4;5;5M");
2446        assert!(matches!(
2447            events.first(),
2448            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2449                && m.modifiers.contains(Modifiers::SHIFT)
2450        ));
2451
2452        // Ctrl+Left click: button_code bit 4 set (ctrl) = 16
2453        let events = parser.parse(b"\x1b[<16;5;5M");
2454        assert!(matches!(
2455            events.first(),
2456            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2457                && m.modifiers.contains(Modifiers::CTRL)
2458        ));
2459
2460        // Alt+Left click: button_code bit 3 set (alt) = 8
2461        let events = parser.parse(b"\x1b[<8;5;5M");
2462        assert!(matches!(
2463            events.first(),
2464            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2465                && m.modifiers.contains(Modifiers::ALT)
2466        ));
2467    }
2468
2469    #[test]
2470    fn mouse_legacy_1015_when_enabled() {
2471        let mut parser = InputParser::new();
2472        parser.set_expect_x10_mouse(true);
2473
2474        let events = parser.parse(b"\x1b[0;10;20M");
2475        assert!(matches!(
2476            events.first(),
2477            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2478                && m.x == 9 && m.y == 19
2479        ));
2480    }
2481
2482    #[test]
2483    fn mouse_legacy_1015_with_fallback_enabled() {
2484        let mut parser = InputParser::new();
2485        parser.set_allow_legacy_mouse(true);
2486
2487        let events = parser.parse(b"\x1b[0;10;20M");
2488        assert!(matches!(
2489            events.first(),
2490            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2491                && m.x == 9 && m.y == 19
2492        ));
2493    }
2494
2495    #[test]
2496    fn mouse_legacy_1015_ignored_when_disabled() {
2497        let mut parser = InputParser::new();
2498        let events = parser.parse(b"\x1b[0;10;20M");
2499        assert!(
2500            events.is_empty(),
2501            "legacy mouse should require explicit opt-in"
2502        );
2503    }
2504
2505    #[test]
2506    fn mouse_x10_when_enabled() {
2507        let mut parser = InputParser::new();
2508        parser.set_expect_x10_mouse(true);
2509
2510        let events = parser.parse(&[0x1B, b'[', b'M', 32, 42, 52]);
2511        assert!(matches!(
2512            events.first(),
2513            Some(Event::Mouse(m)) if matches!(m.kind, MouseEventKind::Down(MouseButton::Left))
2514                && m.x == 9 && m.y == 19
2515        ));
2516    }
2517
2518    #[test]
2519    fn mouse_x10_malformed_packet_ignored() {
2520        let mut parser = InputParser::new();
2521        parser.set_expect_x10_mouse(true);
2522
2523        // Invalid X10 payload bytes (<32 / <33) should be dropped.
2524        let events = parser.parse(&[0x1B, b'[', b'M', 31, 0, 10]);
2525        assert!(
2526            events.iter().all(|event| !matches!(event, Event::Mouse(_))),
2527            "malformed X10 payload must not emit mouse events"
2528        );
2529    }
2530
2531    // ── Kitty keyboard release events and special keys ───────────────
2532
2533    #[test]
2534    fn kitty_keyboard_release_event() {
2535        let mut parser = InputParser::new();
2536        // Release event: kind=3
2537        let events = parser.parse(b"\x1b[97;1:3u");
2538        assert!(matches!(
2539            events.first(),
2540            Some(Event::Key(k)) if k.code == KeyCode::Char('a') && k.kind == KeyEventKind::Release
2541        ));
2542
2543        // Ctrl+release for 'A' (modifiers=5, event_type=3)
2544        let events = parser.parse(b"\x1b[65;5:3u");
2545        assert!(matches!(
2546            events.first(),
2547            Some(Event::Key(k))
2548                if k.code == KeyCode::Char('A')
2549                    && k.modifiers.contains(Modifiers::CTRL)
2550                    && k.kind == KeyEventKind::Release
2551        ));
2552    }
2553
2554    #[test]
2555    fn kitty_keyboard_special_keys() {
2556        let mut parser = InputParser::new();
2557
2558        // Escape: 57344
2559        assert!(matches!(
2560            parser.parse(b"\x1b[57344u").first(),
2561            Some(Event::Key(k)) if k.code == KeyCode::Escape
2562        ));
2563
2564        // Enter: 57345
2565        assert!(matches!(
2566            parser.parse(b"\x1b[57345u").first(),
2567            Some(Event::Key(k)) if k.code == KeyCode::Enter
2568        ));
2569
2570        // Tab: 57346
2571        assert!(matches!(
2572            parser.parse(b"\x1b[57346u").first(),
2573            Some(Event::Key(k)) if k.code == KeyCode::Tab
2574        ));
2575
2576        // Backspace: 57347
2577        assert!(matches!(
2578            parser.parse(b"\x1b[57347u").first(),
2579            Some(Event::Key(k)) if k.code == KeyCode::Backspace
2580        ));
2581
2582        // Insert: 57348
2583        assert!(matches!(
2584            parser.parse(b"\x1b[57348u").first(),
2585            Some(Event::Key(k)) if k.code == KeyCode::Insert
2586        ));
2587
2588        // Delete: 57349
2589        assert!(matches!(
2590            parser.parse(b"\x1b[57349u").first(),
2591            Some(Event::Key(k)) if k.code == KeyCode::Delete
2592        ));
2593    }
2594
2595    #[test]
2596    fn kitty_keyboard_navigation_keys() {
2597        let mut parser = InputParser::new();
2598
2599        // Left: 57350
2600        assert!(matches!(
2601            parser.parse(b"\x1b[57350u").first(),
2602            Some(Event::Key(k)) if k.code == KeyCode::Left
2603        ));
2604        // Right: 57351
2605        assert!(matches!(
2606            parser.parse(b"\x1b[57351u").first(),
2607            Some(Event::Key(k)) if k.code == KeyCode::Right
2608        ));
2609        // Up: 57352
2610        assert!(matches!(
2611            parser.parse(b"\x1b[57352u").first(),
2612            Some(Event::Key(k)) if k.code == KeyCode::Up
2613        ));
2614        // Down: 57353
2615        assert!(matches!(
2616            parser.parse(b"\x1b[57353u").first(),
2617            Some(Event::Key(k)) if k.code == KeyCode::Down
2618        ));
2619        // PageUp: 57354
2620        assert!(matches!(
2621            parser.parse(b"\x1b[57354u").first(),
2622            Some(Event::Key(k)) if k.code == KeyCode::PageUp
2623        ));
2624        // PageDown: 57355
2625        assert!(matches!(
2626            parser.parse(b"\x1b[57355u").first(),
2627            Some(Event::Key(k)) if k.code == KeyCode::PageDown
2628        ));
2629        // Home: 57356
2630        assert!(matches!(
2631            parser.parse(b"\x1b[57356u").first(),
2632            Some(Event::Key(k)) if k.code == KeyCode::Home
2633        ));
2634        // End: 57357
2635        assert!(matches!(
2636            parser.parse(b"\x1b[57357u").first(),
2637            Some(Event::Key(k)) if k.code == KeyCode::End
2638        ));
2639    }
2640
2641    #[test]
2642    fn kitty_keyboard_f_keys() {
2643        let mut parser = InputParser::new();
2644        // F1: 57364
2645        assert!(matches!(
2646            parser.parse(b"\x1b[57364u").first(),
2647            Some(Event::Key(k)) if k.code == KeyCode::F(1)
2648        ));
2649        // F12: 57375
2650        assert!(matches!(
2651            parser.parse(b"\x1b[57375u").first(),
2652            Some(Event::Key(k)) if k.code == KeyCode::F(12)
2653        ));
2654        // F24: 57387
2655        assert!(matches!(
2656            parser.parse(b"\x1b[57387u").first(),
2657            Some(Event::Key(k)) if k.code == KeyCode::F(24)
2658        ));
2659    }
2660
2661    #[test]
2662    fn kitty_keyboard_ascii_as_standard() {
2663        let mut parser = InputParser::new();
2664        // Tab (9), Enter (13), Escape (27), Backspace (127)
2665        assert!(matches!(
2666            parser.parse(b"\x1b[9u").first(),
2667            Some(Event::Key(k)) if k.code == KeyCode::Tab
2668        ));
2669        assert!(matches!(
2670            parser.parse(b"\x1b[13u").first(),
2671            Some(Event::Key(k)) if k.code == KeyCode::Enter
2672        ));
2673        assert!(matches!(
2674            parser.parse(b"\x1b[27u").first(),
2675            Some(Event::Key(k)) if k.code == KeyCode::Escape
2676        ));
2677        assert!(matches!(
2678            parser.parse(b"\x1b[127u").first(),
2679            Some(Event::Key(k)) if k.code == KeyCode::Backspace
2680        ));
2681        // Backspace alternate: 8
2682        assert!(matches!(
2683            parser.parse(b"\x1b[8u").first(),
2684            Some(Event::Key(k)) if k.code == KeyCode::Backspace
2685        ));
2686    }
2687
2688    // ── OSC 52 clipboard ─────────────────────────────────────────────
2689
2690    #[test]
2691    fn osc52_clipboard_bel_terminated() {
2692        let mut parser = InputParser::new();
2693        // OSC 52;c;<base64 "hello"> BEL
2694        // "hello" in base64 is "aGVsbG8="
2695        let events = parser.parse(b"\x1b]52;c;aGVsbG8=\x07");
2696        assert!(matches!(
2697            events.first(),
2698            Some(Event::Clipboard(c)) if c.content == "hello" && c.source == ClipboardSource::Osc52
2699        ));
2700    }
2701
2702    #[test]
2703    fn osc52_clipboard_st_terminated() {
2704        let mut parser = InputParser::new();
2705        // OSC 52;c;<base64 "hello"> ESC \
2706        let events = parser.parse(b"\x1b]52;c;aGVsbG8=\x1b\\");
2707        assert!(matches!(
2708            events.first(),
2709            Some(Event::Clipboard(c)) if c.content == "hello"
2710        ));
2711    }
2712
2713    // --- DCS / control-string handling (XTGETTCAP-reply leak guard) ---
2714
2715    #[test]
2716    fn dcs_xtgettcap_reply_produces_no_events() {
2717        let mut parser = InputParser::new();
2718        // A leaked XTGETTCAP `RGB` reply (e.g. a truecolor probe whose answer
2719        // arrives after the probe timed out on a slow ssh link) MUST be consumed
2720        // silently — not decoded as `Alt+P` then the payload as literal keys.
2721        let events = parser.parse(b"\x1bP1+r524742=8/8/8\x1b\\");
2722        assert!(
2723            events.is_empty(),
2724            "a DCS reply must produce no events, got: {events:?}"
2725        );
2726    }
2727
2728    #[test]
2729    fn dcs_then_real_key_recovers_to_ground() {
2730        let mut parser = InputParser::new();
2731        // After an ST-terminated DCS the parser must be back in Ground so the
2732        // following real keypress parses normally.
2733        let events = parser.parse(b"\x1bP1+r524742=8/8/8\x1b\\a");
2734        assert!(
2735            matches!(events.as_slice(), [Event::Key(k)] if k.code == KeyCode::Char('a')),
2736            "parser must recover to Ground after a DCS, got: {events:?}"
2737        );
2738    }
2739
2740    #[test]
2741    fn dcs_bel_terminated_is_ignored() {
2742        let mut parser = InputParser::new();
2743        let events = parser.parse(b"\x1bPsome-payload\x07b");
2744        assert!(
2745            matches!(events.as_slice(), [Event::Key(k)] if k.code == KeyCode::Char('b')),
2746            "BEL-terminated DCS ignored, then key parses, got: {events:?}"
2747        );
2748    }
2749
2750    #[test]
2751    fn sos_pm_apc_introducers_stay_alt_keys() {
2752        // We deliberately intercept ONLY DCS (ESC P), not the sibling C1 string
2753        // introducers SOS/PM/APC — terminals never send those as responses, so
2754        // they remain `Alt+Shift+X` / `Alt+^` / `Alt+_` keypresses.
2755        for &introducer in b"X^_" {
2756            let mut parser = InputParser::new();
2757            let events = parser.parse(&[0x1b, introducer]);
2758            assert!(
2759                matches!(
2760                    events.as_slice(),
2761                    [Event::Key(k)]
2762                        if k.code == KeyCode::Char(introducer as char)
2763                            && k.modifiers.contains(Modifiers::ALT)
2764                ),
2765                "ESC {} must stay an Alt key, got: {events:?}",
2766                introducer as char
2767            );
2768        }
2769    }
2770
2771    #[test]
2772    fn dcs_esc_then_csi_recovers_to_arrow_key() {
2773        let mut parser = InputParser::new();
2774        // ESC inside the DCS payload, followed not by `\` (ST) but by a CSI
2775        // (`[A` = Up): the string is cancelled and the CSI parses cleanly.
2776        let events = parser.parse(b"\x1bP payload \x1b[A");
2777        assert!(
2778            matches!(events.as_slice(), [Event::Key(k)] if k.code == KeyCode::Up),
2779            "ESC-mid-DCS then a CSI must recover and parse the arrow, got: {events:?}"
2780        );
2781    }
2782
2783    #[test]
2784    fn dcs_aborts_on_control_char_so_input_is_not_swallowed() {
2785        let mut parser = InputParser::new();
2786        // A never-terminated DCS followed by Enter (CR, a control byte): the
2787        // control char must abort the string and be reprocessed, so a malformed
2788        // string cannot swallow subsequent real input forever.
2789        let events = parser.parse(b"\x1bPunterminated\r");
2790        assert!(
2791            !events.is_empty(),
2792            "a control char must abort a stuck DCS and emit the key, got: {events:?}"
2793        );
2794    }
2795
2796    #[test]
2797    fn osc52_clipboard_primary_selection() {
2798        let mut parser = InputParser::new();
2799        // Primary selection: p instead of c
2800        // "abc" in base64 is "YWJj"
2801        let events = parser.parse(b"\x1b]52;p;YWJj\x07");
2802        assert!(matches!(
2803            events.first(),
2804            Some(Event::Clipboard(c)) if c.content == "abc"
2805        ));
2806    }
2807
2808    // ── Control keys ─────────────────────────────────────────────────
2809
2810    #[test]
2811    fn ctrl_space_is_null() {
2812        let mut parser = InputParser::new();
2813        let events = parser.parse(&[0x00]);
2814        assert!(matches!(
2815            events.first(),
2816            Some(Event::Key(k)) if k.code == KeyCode::Null
2817        ));
2818    }
2819
2820    #[test]
2821    fn all_ctrl_letter_keys() {
2822        let mut parser = InputParser::new();
2823        // Ctrl+A (0x01) through Ctrl+Z (0x1A), skipping Backspace (0x08), Tab (0x09), and Enter (0x0D)
2824        for byte in 0x01..=0x1Au8 {
2825            let events = parser.parse(&[byte]);
2826            assert_eq!(
2827                events.len(),
2828                1,
2829                "Ctrl+{} should produce one event",
2830                (byte + b'a' - 1) as char
2831            );
2832            match byte {
2833                0x08 => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Backspace)),
2834                0x09 => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Tab)),
2835                0x0D => assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Enter)),
2836                _ => {
2837                    let expected_char = (byte + b'a' - 1) as char;
2838                    match &events[0] {
2839                        Event::Key(k) => {
2840                            assert_eq!(
2841                                k.code,
2842                                KeyCode::Char(expected_char),
2843                                "Byte 0x{byte:02X} should produce Ctrl+{expected_char}"
2844                            );
2845                            assert!(
2846                                k.modifiers.contains(Modifiers::CTRL),
2847                                "Byte 0x{byte:02X} should have Ctrl modifier"
2848                            );
2849                        }
2850                        other => {
2851                            panic!("Byte 0x{byte:02X}: expected Key event, got {other:?}");
2852                        }
2853                    }
2854                }
2855            }
2856        }
2857    }
2858
2859    // ── UTF-8 multi-byte: 3-byte and 4-byte ─────────────────────────
2860
2861    #[test]
2862    fn utf8_3byte_cjk() {
2863        let mut parser = InputParser::new();
2864        // 中 (U+4E2D) = 0xE4 0xB8 0xAD
2865        let events = parser.parse(&[0xE4, 0xB8, 0xAD]);
2866        assert!(matches!(
2867            events.first(),
2868            Some(Event::Key(k)) if k.code == KeyCode::Char('中')
2869        ));
2870    }
2871
2872    #[test]
2873    fn utf8_4byte_emoji() {
2874        let mut parser = InputParser::new();
2875        // 🦀 (U+1F980) = 0xF0 0x9F 0xA6 0x80
2876        let events = parser.parse(&[0xF0, 0x9F, 0xA6, 0x80]);
2877        assert!(matches!(
2878            events.first(),
2879            Some(Event::Key(k)) if k.code == KeyCode::Char('🦀')
2880        ));
2881    }
2882
2883    // ── Empty input ──────────────────────────────────────────────────
2884
2885    #[test]
2886    fn empty_input_returns_no_events() {
2887        let mut parser = InputParser::new();
2888        let events = parser.parse(b"");
2889        assert!(events.is_empty());
2890    }
2891
2892    // ── Unknown CSI tilde values ─────────────────────────────────────
2893
2894    #[test]
2895    fn unknown_csi_tilde_ignored() {
2896        let mut parser = InputParser::new();
2897        // Code 99 is not a known tilde key
2898        let events = parser.parse(b"\x1b[99~");
2899        assert!(events.is_empty());
2900
2901        // Parser should still work
2902        let events = parser.parse(b"a");
2903        assert!(matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char('a')));
2904    }
2905
2906    // ── Alt+various characters ───────────────────────────────────────
2907
2908    #[test]
2909    fn alt_special_chars() {
2910        let mut parser = InputParser::new();
2911
2912        // Alt+space
2913        let events = parser.parse(b"\x1b ");
2914        assert!(matches!(
2915            events.first(),
2916            Some(Event::Key(k)) if k.code == KeyCode::Char(' ') && k.modifiers.contains(Modifiers::ALT)
2917        ));
2918
2919        // Alt+digit
2920        let events = parser.parse(b"\x1b5");
2921        assert!(matches!(
2922            events.first(),
2923            Some(Event::Key(k)) if k.code == KeyCode::Char('5') && k.modifiers.contains(Modifiers::ALT)
2924        ));
2925
2926        // Alt+bracket
2927        let events = parser.parse(b"\x1b}");
2928        assert!(matches!(
2929            events.first(),
2930            Some(Event::Key(k)) if k.code == KeyCode::Char('}') && k.modifiers.contains(Modifiers::ALT)
2931        ));
2932    }
2933
2934    #[test]
2935    fn alt_ctrl_key_combinations() {
2936        let mut parser = InputParser::new();
2937
2938        // ESC + Ctrl+A (0x01) -> Alt+Ctrl+A
2939        let events = parser.parse(&[0x1B, 0x01]);
2940        assert_eq!(events.len(), 1);
2941        match &events[0] {
2942            Event::Key(k) => {
2943                assert_eq!(k.code, KeyCode::Char('a'));
2944                assert!(k.modifiers.contains(Modifiers::ALT));
2945                assert!(k.modifiers.contains(Modifiers::CTRL));
2946            }
2947            _ => panic!("Expected Key event"),
2948        }
2949
2950        // ESC + Backspace (0x08) -> Alt+Backspace (Ctrl+H is Backspace)
2951        // Note: 0x08 is Backspace in process_ground.
2952        // So ESC + 0x08 should be Alt+Backspace.
2953        let events = parser.parse(&[0x1B, 0x08]);
2954        assert_eq!(events.len(), 1);
2955        match &events[0] {
2956            Event::Key(k) => {
2957                assert_eq!(k.code, KeyCode::Backspace);
2958                assert!(k.modifiers.contains(Modifiers::ALT));
2959            }
2960            _ => panic!("Expected Key event"),
2961        }
2962    }
2963
2964    // ── SS3 arrow keys ───────────────────────────────────────────────
2965
2966    #[test]
2967    fn ss3_arrow_keys() {
2968        let mut parser = InputParser::new();
2969        assert!(matches!(
2970            parser.parse(b"\x1bOA").first(),
2971            Some(Event::Key(k)) if k.code == KeyCode::Up
2972        ));
2973        assert!(matches!(
2974            parser.parse(b"\x1bOB").first(),
2975            Some(Event::Key(k)) if k.code == KeyCode::Down
2976        ));
2977        assert!(matches!(
2978            parser.parse(b"\x1bOC").first(),
2979            Some(Event::Key(k)) if k.code == KeyCode::Right
2980        ));
2981        assert!(matches!(
2982            parser.parse(b"\x1bOD").first(),
2983            Some(Event::Key(k)) if k.code == KeyCode::Left
2984        ));
2985    }
2986
2987    // ── Xterm modifier encoding ──────────────────────────────────────
2988
2989    #[test]
2990    fn xterm_modifier_encoding() {
2991        // Verify modifiers_from_xterm decoding (value = 1 + modifier_bits)
2992        assert_eq!(InputParser::modifiers_from_xterm(1), Modifiers::NONE);
2993        assert_eq!(InputParser::modifiers_from_xterm(2), Modifiers::SHIFT);
2994        assert_eq!(InputParser::modifiers_from_xterm(3), Modifiers::ALT);
2995        assert_eq!(
2996            InputParser::modifiers_from_xterm(4),
2997            Modifiers::SHIFT | Modifiers::ALT
2998        );
2999        assert_eq!(InputParser::modifiers_from_xterm(5), Modifiers::CTRL);
3000        assert_eq!(
3001            InputParser::modifiers_from_xterm(6),
3002            Modifiers::SHIFT | Modifiers::CTRL
3003        );
3004        assert_eq!(InputParser::modifiers_from_xterm(9), Modifiers::SUPER);
3005    }
3006
3007    // ── SS3 interrupted by ESC ───────────────────────────────────────
3008
3009    #[test]
3010    fn ss3_interrupted_by_esc() {
3011        let mut parser = InputParser::new();
3012        // ESC O ESC should restart into Escape state
3013        let events = parser.parse(b"\x1bO\x1b[A");
3014        // Should get Up arrow from the new ESC [ A sequence
3015        assert!(matches!(
3016            events.first(),
3017            Some(Event::Key(k)) if k.code == KeyCode::Up
3018        ));
3019    }
3020
3021    // ── Kitty keyboard: unhandled keycodes ───────────────────────────
3022
3023    #[test]
3024    fn kitty_keyboard_reserved_keycode_ignored() {
3025        let mut parser = InputParser::new();
3026        // Reserved range 57358..=57363 returns None
3027        let events = parser.parse(b"\x1b[57360u");
3028        assert!(events.is_empty());
3029
3030        // Parser still works
3031        let events = parser.parse(b"x");
3032        assert!(matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char('x')));
3033    }
3034    #[test]
3035    fn utf8_invalid_sequence_emits_replacement() {
3036        let mut parser = InputParser::new();
3037
3038        // 0xE0 is a start of 3-byte sequence.
3039        // 0x41 ('A') is not a valid continuation byte.
3040        // Should emit Replacement Character then 'A'.
3041        let events = parser.parse(&[0xE0, 0x41]);
3042        assert_eq!(events.len(), 2);
3043
3044        match &events[0] {
3045            Event::Key(k) => assert_eq!(k.code, KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
3046            _ => panic!("Expected replacement character"),
3047        }
3048
3049        match &events[1] {
3050            Event::Key(k) => assert_eq!(k.code, KeyCode::Char('A')),
3051            _ => panic!("Expected character 'A'"),
3052        }
3053    }
3054
3055    #[test]
3056    fn utf8_invalid_lead_emits_replacement() {
3057        let mut parser = InputParser::new();
3058
3059        // 0xC0 is an invalid UTF-8 lead byte (overlong sequence).
3060        let events = parser.parse(&[0xC0, b'a']);
3061        assert!(
3062            matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
3063            "Expected replacement for invalid lead"
3064        );
3065        assert!(
3066            events
3067                .iter()
3068                .any(|e| matches!(e, Event::Key(k) if k.code == KeyCode::Char('a'))),
3069            "Expected subsequent ASCII to be preserved"
3070        );
3071
3072        // 0xF5 is an out-of-range UTF-8 lead byte.
3073        let events = parser.parse(&[0xF5, b'b']);
3074        assert!(
3075            matches!(events.first(), Some(Event::Key(k)) if k.code == KeyCode::Char(std::char::REPLACEMENT_CHARACTER)),
3076            "Expected replacement for out-of-range lead"
3077        );
3078        assert!(
3079            events
3080                .iter()
3081                .any(|e| matches!(e, Event::Key(k) if k.code == KeyCode::Char('b'))),
3082            "Expected subsequent ASCII to be preserved"
3083        );
3084    }
3085}
3086
3087#[cfg(test)]
3088mod proptest_fuzz {
3089    use super::*;
3090    use proptest::prelude::*;
3091
3092    // ── Strategy helpers ────────────────────────────────────────────────
3093    // Avoid turbofish inside proptest! macro (Rust 2024 edition compat).
3094
3095    fn arb_byte() -> impl Strategy<Value = u8> {
3096        any::<u8>()
3097    }
3098
3099    fn arb_byte_vec(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
3100        prop::collection::vec(arb_byte(), 0..=max_len)
3101    }
3102
3103    /// Generate a well-formed CSI sequence: ESC [ <params> <final byte>.
3104    fn csi_sequence() -> impl Strategy<Value = Vec<u8>> {
3105        let params = prop::collection::vec(0x30u8..=0x3F, 0..=20);
3106        let final_byte = 0x40u8..=0x7E;
3107        (params, final_byte).prop_map(|(p, f)| {
3108            let mut buf = vec![0x1B, b'['];
3109            buf.extend_from_slice(&p);
3110            buf.push(f);
3111            buf
3112        })
3113    }
3114
3115    /// Generate an OSC sequence: ESC ] <content> ST.
3116    fn osc_sequence() -> impl Strategy<Value = Vec<u8>> {
3117        let content = prop::collection::vec(0x20u8..=0x7E, 0..=64);
3118        let terminator = prop_oneof![
3119            Just(vec![0x1B, b'\\']), // ESC backslash
3120            Just(vec![0x07]),        // BEL
3121        ];
3122        (content, terminator).prop_map(|(c, t)| {
3123            let mut buf = vec![0x1B, b']'];
3124            buf.extend_from_slice(&c);
3125            buf.extend_from_slice(&t);
3126            buf
3127        })
3128    }
3129
3130    /// Generate an SS3 sequence: ESC O <final byte>.
3131    fn ss3_sequence() -> impl Strategy<Value = Vec<u8>> {
3132        (0x40u8..=0x7E).prop_map(|f| vec![0x1B, b'O', f])
3133    }
3134
3135    /// Generate a bracketed paste: ESC[200~ <content> ESC[201~.
3136    fn paste_sequence() -> impl Strategy<Value = Vec<u8>> {
3137        prop::collection::vec(0x20u8..=0x7E, 0..=128).prop_map(|content| {
3138            let mut buf = vec![0x1B, b'[', b'2', b'0', b'0', b'~'];
3139            buf.extend_from_slice(&content);
3140            buf.extend_from_slice(b"\x1b[201~");
3141            buf
3142        })
3143    }
3144
3145    /// Generate structured adversarial input: mix of valid sequences and random bytes.
3146    fn mixed_adversarial() -> impl Strategy<Value = Vec<u8>> {
3147        let fragment = prop_oneof![
3148            csi_sequence(),
3149            osc_sequence(),
3150            ss3_sequence(),
3151            paste_sequence(),
3152            arb_byte_vec(16),                            // random bytes
3153            Just(vec![0x1B]),                            // bare ESC
3154            Just(vec![0x1B, b'[']),                      // unterminated CSI
3155            Just(vec![0x1B, b']']),                      // unterminated OSC
3156            prop::collection::vec(0x80u8..=0xFF, 1..=4), // high bytes
3157        ];
3158        prop::collection::vec(fragment, 1..=8)
3159            .prop_map(|frags| frags.into_iter().flatten().collect())
3160    }
3161
3162    // ── Property tests ─────────────────────────────────────────────────
3163
3164    proptest! {
3165        /// Random bytes must never panic.
3166        #[test]
3167        fn random_bytes_never_panic(input in arb_byte_vec(512)) {
3168            let mut parser = InputParser::new();
3169            let _ = parser.parse(&input);
3170        }
3171
3172        /// After parsing any input, the parser must be reusable for normal keys.
3173        #[test]
3174        fn parser_recovers_after_garbage(input in arb_byte_vec(256)) {
3175            let mut parser = InputParser::new();
3176            let _ = parser.parse(&input);
3177
3178            // Feed a clean known sequence (letter 'z') after the garbage.
3179            let events = parser.parse(b"z");
3180            // Parser must not panic. We can't assert exact events because
3181            // the parser may still be mid-sequence, but it must not panic.
3182            let _ = events;
3183        }
3184
3185        /// Structured mixed input (valid sequences + garbage) must never panic.
3186        #[test]
3187        fn mixed_sequences_never_panic(input in mixed_adversarial()) {
3188            let mut parser = InputParser::new();
3189            let _ = parser.parse(&input);
3190        }
3191
3192        /// All generated events must be valid (non-panicking Debug).
3193        #[test]
3194        fn events_are_well_formed(input in arb_byte_vec(256)) {
3195            let mut parser = InputParser::new();
3196            let events = parser.parse(&input);
3197            for event in &events {
3198                // Exercise Debug impl — catches inconsistent internal state.
3199                let _ = format!("{event:?}");
3200            }
3201        }
3202
3203        /// CSI sequences never produce more events than bytes fed.
3204        #[test]
3205        fn csi_event_count_bounded(seq in csi_sequence()) {
3206            let mut parser = InputParser::new();
3207            let events = parser.parse(&seq);
3208            prop_assert!(events.len() <= seq.len(),
3209                "Got {} events from {} bytes", events.len(), seq.len());
3210        }
3211
3212        /// OSC sequences never produce more events than bytes fed.
3213        #[test]
3214        fn osc_event_count_bounded(seq in osc_sequence()) {
3215            let mut parser = InputParser::new();
3216            let events = parser.parse(&seq);
3217            prop_assert!(events.len() <= seq.len(),
3218                "Got {} events from {} bytes", events.len(), seq.len());
3219        }
3220
3221        /// Paste content is always bounded by MAX_PASTE_LEN.
3222        #[test]
3223        fn paste_content_bounded(content in prop::collection::vec(arb_byte(), 0..=2048)) {
3224            let mut parser = InputParser::new();
3225            let mut input = vec![0x1B, b'[', b'2', b'0', b'0', b'~'];
3226            input.extend_from_slice(&content);
3227            input.extend_from_slice(b"\x1b[201~");
3228
3229            let events = parser.parse(&input);
3230            for event in &events {
3231                if let Event::Paste(p) = event {
3232                    prop_assert!(p.text.len() <= MAX_PASTE_LEN,
3233                        "Paste text {} exceeds limit {}", p.text.len(), MAX_PASTE_LEN);
3234                }
3235            }
3236        }
3237
3238        /// Feeding input byte-by-byte yields same events as feeding all at once.
3239        #[test]
3240        fn incremental_matches_bulk(input in arb_byte_vec(128)) {
3241            let mut bulk_parser = InputParser::new();
3242            let bulk_events = bulk_parser.parse(&input);
3243
3244            let mut incr_parser = InputParser::new();
3245            let mut incr_events = Vec::new();
3246            for byte in &input {
3247                incr_events.extend(incr_parser.parse(std::slice::from_ref(byte)));
3248            }
3249
3250            let bulk_dbg: Vec<String> = bulk_events.iter().map(|e| format!("{e:?}")).collect();
3251            let incr_dbg: Vec<String> = incr_events.iter().map(|e| format!("{e:?}")).collect();
3252            prop_assert_eq!(bulk_dbg, incr_dbg,
3253                "Bulk vs incremental mismatch for input {:?}", input);
3254        }
3255
3256        /// Repeated parsing of the same input must always produce the same result
3257        /// (parser is deterministic after reset).
3258        #[test]
3259        fn deterministic_output(input in arb_byte_vec(128)) {
3260            let mut parser1 = InputParser::new();
3261            let events1 = parser1.parse(&input);
3262
3263            let mut parser2 = InputParser::new();
3264            let events2 = parser2.parse(&input);
3265
3266            let dbg1: Vec<String> = events1.iter().map(|e| format!("{e:?}")).collect();
3267            let dbg2: Vec<String> = events2.iter().map(|e| format!("{e:?}")).collect();
3268            prop_assert_eq!(dbg1, dbg2);
3269        }
3270    }
3271
3272    // ── Targeted invariant tests (outside proptest! macro) ─────────────
3273
3274    /// After a long garbage run, parser handles a simple key within bounded time.
3275    #[test]
3276    fn no_quadratic_blowup() {
3277        let mut parser = InputParser::new();
3278
3279        // 64KB of random-ish bytes (repeating pattern).
3280        let garbage: Vec<u8> = (0..65536).map(|i| (i % 256) as u8).collect();
3281        let _ = parser.parse(&garbage);
3282
3283        // Follow with a clean key — must not take pathological time.
3284        let events = parser.parse(b"a");
3285        let _ = events; // primarily asserting no hang/panic
3286    }
3287
3288    /// Oversized CSI sequence triggers DoS protection without panic.
3289    #[test]
3290    fn oversized_csi_transitions_to_ignore() {
3291        let mut parser = InputParser::new();
3292
3293        // CSI followed by MAX_CSI_LEN+100 parameter bytes then a final byte.
3294        let mut input = vec![0x1B, b'['];
3295        input.extend(std::iter::repeat_n(b'0', MAX_CSI_LEN + 100));
3296        input.push(b'm');
3297
3298        let _ = parser.parse(&input);
3299
3300        // Parser must still be usable.
3301        let events = parser.parse(b"x");
3302        assert_eq!(events.len(), 1);
3303        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('x')));
3304    }
3305
3306    /// Oversized OSC sequence triggers DoS protection without panic.
3307    #[test]
3308    fn oversized_osc_transitions_to_ignore() {
3309        let mut parser = InputParser::new();
3310
3311        // OSC followed by MAX_OSC_LEN+100 content bytes then ST.
3312        let mut input = vec![0x1B, b']'];
3313        input.extend(std::iter::repeat_n(b'a', MAX_OSC_LEN + 100));
3314        input.push(0x07); // BEL terminator
3315
3316        let _ = parser.parse(&input);
3317
3318        // Parser must still be usable.
3319        let events = parser.parse(b"y");
3320        assert_eq!(events.len(), 1);
3321        assert!(matches!(events[0], Event::Key(k) if k.code == KeyCode::Char('y')));
3322    }
3323
3324    /// Rapid ESC toggling doesn't corrupt state.
3325    #[test]
3326    fn rapid_esc_toggle() {
3327        let mut parser = InputParser::new();
3328
3329        // 1000 bare ESCs in a row.
3330        let input: Vec<u8> = vec![0x1B; 1000];
3331        let _ = parser.parse(&input);
3332
3333        // Must recover for a normal key.
3334        let events = parser.parse(b"k");
3335        assert!(!events.is_empty());
3336    }
3337
3338    /// Interleaved paste start sequences without end.
3339    #[test]
3340    fn unterminated_paste_recovery() {
3341        let mut parser = InputParser::new();
3342
3343        // Start paste, but never end it — feed lots of data.
3344        let mut input = b"\x1b[200~".to_vec();
3345        input.extend(std::iter::repeat_n(b'x', 2048));
3346
3347        let _ = parser.parse(&input);
3348
3349        // Now end the paste.
3350        let events = parser.parse(b"\x1b[201~");
3351        assert!(
3352            !events.is_empty(),
3353            "Parser should emit paste event on terminator"
3354        );
3355    }
3356
3357    /// UTF-8 boundary: all possible lead bytes followed by truncation.
3358    #[test]
3359    fn truncated_utf8_lead_bytes() {
3360        let mut parser = InputParser::new();
3361
3362        // Two-byte lead (0xC0..0xDF), three-byte (0xE0..0xEF), four-byte (0xF0..0xF7)
3363        for lead in [0xC2, 0xE0, 0xF0] {
3364            let _ = parser.parse(&[lead]);
3365            // Feed a normal ASCII after the truncated lead.
3366            let events = parser.parse(b"a");
3367            // Must not panic; 'a' should eventually appear.
3368            let _ = events;
3369        }
3370    }
3371
3372    /// Null bytes mixed with valid input.
3373    #[test]
3374    fn null_bytes_interleaved() {
3375        let mut parser = InputParser::new();
3376
3377        let input = b"\x00A\x00\x1b[A\x00B\x00";
3378        let events = parser.parse(input);
3379        // Should get events for 'A', Up arrow, and 'B' (nulls handled gracefully).
3380        assert!(
3381            events.len() >= 2,
3382            "Expected at least 2 events, got {}",
3383            events.len()
3384        );
3385    }
3386
3387    // ── Additional fuzz invariant tests (bd-10i.11.3) ─────────────────
3388
3389    /// Generate an OSC 52 clipboard sequence with arbitrary base64 payload.
3390    fn osc52_sequence() -> impl Strategy<Value = Vec<u8>> {
3391        let selector = prop_oneof![Just(b'c'), Just(b'p'), Just(b's')];
3392        // Generate valid base64 characters with occasional invalid ones
3393        let payload = prop::collection::vec(
3394            prop_oneof![
3395                0x41u8..=0x5A, // A-Z
3396                0x61u8..=0x7A, // a-z
3397                0x30u8..=0x39, // 0-9
3398                Just(b'+'),
3399                Just(b'/'),
3400                Just(b'='),
3401            ],
3402            0..=128,
3403        );
3404        let terminator = prop_oneof![
3405            Just(vec![0x1B, b'\\']), // ESC backslash (ST)
3406            Just(vec![0x07]),        // BEL
3407        ];
3408        (selector, payload, terminator).prop_map(|(sel, pay, term)| {
3409            let mut buf = vec![0x1B, b']', b'5', b'2', b';', sel, b';'];
3410            buf.extend_from_slice(&pay);
3411            buf.extend_from_slice(&term);
3412            buf
3413        })
3414    }
3415
3416    /// Generate an SGR mouse sequence.
3417    fn sgr_mouse_sequence() -> impl Strategy<Value = Vec<u8>> {
3418        let button_code = 0u16..128;
3419        let x = 1u16..300;
3420        let y = 1u16..100;
3421        let final_byte = prop_oneof![Just(b'M'), Just(b'm')];
3422        (button_code, x, y, final_byte)
3423            .prop_map(|(btn, x, y, fb)| format!("\x1b[<{btn};{x};{y}{}", fb as char).into_bytes())
3424    }
3425
3426    /// Generate Kitty keyboard protocol sequences.
3427    fn kitty_keyboard_sequence() -> impl Strategy<Value = Vec<u8>> {
3428        let keycode = prop_oneof![
3429            0x20u32..0x7F,       // ASCII range
3430            0x57344u32..0x57400, // Kitty special keys
3431            0x100u32..0x200,     // Extended range
3432        ];
3433        let modifier = 1u32..16;
3434        let kind = prop_oneof![Just(1u32), Just(2u32), Just(3u32)]; // press/repeat/release
3435        (keycode, prop::option::of(modifier), prop::option::of(kind)).prop_map(
3436            |(kc, mods, kind)| match (mods, kind) {
3437                (Some(m), Some(k)) => format!("\x1b[{kc};{m}:{k}u").into_bytes(),
3438                (Some(m), None) => format!("\x1b[{kc};{m}u").into_bytes(),
3439                _ => format!("\x1b[{kc}u").into_bytes(),
3440            },
3441        )
3442    }
3443
3444    proptest! {
3445        // --- OSC 52 clipboard tests ---
3446
3447        /// OSC 52 clipboard sequences never panic.
3448        #[test]
3449        fn osc52_never_panics(seq in osc52_sequence()) {
3450            let mut parser = InputParser::new();
3451            let events = parser.parse(&seq);
3452            // If parsed, should be a Clipboard event
3453            for event in &events {
3454                if let Event::Clipboard(c) = event {
3455                    prop_assert!(!c.content.is_empty() || c.content.is_empty(),
3456                        "Clipboard event must have a content field");
3457                }
3458            }
3459        }
3460
3461        /// OSC 52 with corrupt base64 doesn't panic.
3462        #[test]
3463        fn osc52_corrupt_base64_safe(payload in arb_byte_vec(128)) {
3464            let mut parser = InputParser::new();
3465            let mut input = b"\x1b]52;c;".to_vec();
3466            input.extend_from_slice(&payload);
3467            input.push(0x07); // BEL terminator
3468            let _ = parser.parse(&input);
3469        }
3470
3471        // --- SGR mouse tests ---
3472
3473        /// All SGR mouse sequences parse without panicking.
3474        #[test]
3475        fn sgr_mouse_never_panics(seq in sgr_mouse_sequence()) {
3476            let mut parser = InputParser::new();
3477            let events = parser.parse(&seq);
3478            for event in &events {
3479                // Verify events are well-formed (exercises Debug impl)
3480                let _ = format!("{event:?}");
3481            }
3482        }
3483
3484        /// SGR mouse with extreme coordinates doesn't overflow.
3485        #[test]
3486        fn sgr_mouse_extreme_coords(
3487            btn in 0u16..128,
3488            x in 0u16..=65535,
3489            y in 0u16..=65535,
3490        ) {
3491            let mut parser = InputParser::new();
3492            let input = format!("\x1b[<{btn};{x};{y}M").into_bytes();
3493            let events = parser.parse(&input);
3494            for event in &events {
3495                if let Event::Mouse(m) = event {
3496                    prop_assert!(m.x <= x, "Mouse x {} > input x {}", m.x, x);
3497                    prop_assert!(m.y <= y, "Mouse y {} > input y {}", m.y, y);
3498                }
3499            }
3500        }
3501
3502        // --- Kitty keyboard protocol tests ---
3503
3504        /// Kitty keyboard sequences never panic.
3505        #[test]
3506        fn kitty_keyboard_never_panics(seq in kitty_keyboard_sequence()) {
3507            let mut parser = InputParser::new();
3508            let _ = parser.parse(&seq);
3509        }
3510
3511        // --- State boundary tests ---
3512
3513        /// Truncated CSI followed by new valid sequence works correctly.
3514        #[test]
3515        fn truncated_csi_then_valid(
3516            params in prop::collection::vec(0x30u8..=0x3F, 1..=10),
3517            valid_char in 0x20u8..0x7F,
3518        ) {
3519            let mut parser = InputParser::new();
3520
3521            // Send truncated CSI (no final byte)
3522            let mut partial = vec![0x1B, b'['];
3523            partial.extend_from_slice(&params);
3524            let _ = parser.parse(&partial);
3525
3526            // Now send a fresh ESC sequence that should reset state
3527            let events = parser.parse(&[0x1B, b'[', b'A']); // Up arrow
3528            // Parser should eventually emit events (possibly including
3529            // interpretation of partial as complete)
3530            let _ = events;
3531
3532            // Verify recovery with a simple key
3533            let events = parser.parse(&[valid_char]);
3534            let _ = events;
3535        }
3536
3537        /// Truncated OSC followed by new valid sequence works.
3538        #[test]
3539        fn truncated_osc_then_valid(
3540            content in prop::collection::vec(0x20u8..=0x7E, 1..=32),
3541        ) {
3542            let mut parser = InputParser::new();
3543
3544            // Send unterminated OSC
3545            let mut partial = vec![0x1B, b']'];
3546            partial.extend_from_slice(&content);
3547            let _ = parser.parse(&partial);
3548
3549            // Send a new ESC to interrupt, then a valid key
3550            let events = parser.parse(b"\x1bz");
3551            let _ = events;
3552        }
3553
3554        // --- Near-limit tests ---
3555
3556        /// CSI sequence just under MAX_CSI_LEN produces events.
3557        #[test]
3558        fn csi_near_limit_produces_event(
3559            fill_byte in 0x30u8..=0x39, // digit parameter bytes
3560        ) {
3561            let mut parser = InputParser::new();
3562
3563            let mut input = vec![0x1B, b'['];
3564            // Fill to just under limit
3565            input.extend(std::iter::repeat_n(fill_byte, MAX_CSI_LEN - 1));
3566            input.push(b'm'); // final byte (SGR)
3567
3568            let events = parser.parse(&input);
3569            // Should NOT have been ignored (under limit)
3570            // The sequence is valid structurally even if params are nonsensical
3571            let _ = events;
3572
3573            // Parser should still work
3574            let events = parser.parse(b"a");
3575            prop_assert!(!events.is_empty(), "Parser stuck after near-limit CSI");
3576        }
3577
3578        /// OSC sequence just under MAX_OSC_LEN still processes.
3579        #[test]
3580        fn osc_near_limit_processes(
3581            fill_byte in 0x20u8..=0x7E,
3582        ) {
3583            let mut parser = InputParser::new();
3584
3585            let mut input = vec![0x1B, b']'];
3586            input.extend(std::iter::repeat_n(fill_byte, MAX_OSC_LEN - 1));
3587            input.push(0x07); // BEL terminator
3588
3589            let _ = parser.parse(&input);
3590
3591            // Parser should still work
3592            let events = parser.parse(b"b");
3593            prop_assert!(!events.is_empty(), "Parser stuck after near-limit OSC");
3594        }
3595
3596        // --- Consecutive paste tests ---
3597
3598        /// Multiple back-to-back paste sequences all emit events.
3599        #[test]
3600        fn consecutive_pastes_emit_events(count in 2usize..=5) {
3601            let mut parser = InputParser::new();
3602            let mut input = Vec::new();
3603
3604            for i in 0..count {
3605                input.extend_from_slice(b"\x1b[200~");
3606                input.extend_from_slice(format!("paste_{i}").as_bytes());
3607                input.extend_from_slice(b"\x1b[201~");
3608            }
3609
3610            let events = parser.parse(&input);
3611            let paste_events: Vec<_> = events.iter()
3612                .filter(|e| matches!(e, Event::Paste(_)))
3613                .collect();
3614
3615            prop_assert_eq!(paste_events.len(), count,
3616                "Expected {} paste events, got {}", count, paste_events.len());
3617        }
3618
3619        /// Paste with invalid UTF-8 bytes doesn't panic.
3620        #[test]
3621        fn paste_with_invalid_utf8(content in arb_byte_vec(256)) {
3622            let mut parser = InputParser::new();
3623            let mut input = b"\x1b[200~".to_vec();
3624            input.extend_from_slice(&content);
3625            input.extend_from_slice(b"\x1b[201~");
3626
3627            let events = parser.parse(&input);
3628            for event in &events {
3629                if let Event::Paste(p) = event {
3630                    // Text should be valid UTF-8 (lossy conversion happens internally)
3631                    prop_assert!(p.text.is_char_boundary(0), "Paste text is not valid UTF-8");
3632                }
3633            }
3634        }
3635
3636        // --- Recovery invariants ---
3637
3638        /// After any arbitrary input, feeding ESC then a known key recovers.
3639        #[test]
3640        fn recovery_via_esc_reset(garbage in arb_byte_vec(256)) {
3641            let mut parser = InputParser::new();
3642            let _ = parser.parse(&garbage);
3643
3644            // Terminate any pending OSC (BEL works from any OSC sub-state),
3645            // then ESC to flush any other intermediate state.
3646            let _ = parser.parse(b"\x07\x1b\\\x1b");
3647            let _ = parser.parse(b"\x1b");
3648
3649            // Now feed a clean character.
3650            let _ = parser.parse(b"z");
3651
3652            // Feed one more clean character to verify.
3653            let events = parser.parse(b"q");
3654            // After terminating all pending sequences and feeding clean input,
3655            // the parser must produce events.
3656            prop_assert!(!events.is_empty(),
3657                "Parser did not recover after garbage + reset");
3658        }
3659    }
3660}