Skip to main content

ftui_core/
input_parser.rs

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