Skip to main content

ftui_core/
input_parser.rs

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