Skip to main content

frankenterm_core/
parser.rs

1//! VT/ANSI parser.
2//!
3//! This parser is a deterministic state machine that converts an output byte
4//! stream into a sequence of actions for the terminal engine. It covers:
5//!
6//! - printable characters (ASCII + full UTF-8) -> `Action::Print`
7//! - C0 controls -> dedicated actions
8//! - CSI sequences (cursor, erase, scroll, SGR, mode set/reset)
9//! - OSC sequences (title, hyperlinks)
10//! - ESC-level sequences (cursor save/restore, index, reset)
11//! - capture of unsupported sequences as `Action::Escape` for later decoding
12
13use smallvec::SmallVec;
14
15/// Inline capacity for CSI parameter lists.
16///
17/// Most SGR/DECSET sequences carry ≤ 4 parameters; this avoids a heap
18/// allocation for the common case while remaining transparent to consumers
19/// via `Deref<Target = [u16]>`.
20pub type CsiParams = SmallVec<[u16; 4]>;
21
22/// Parser output actions.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Action {
25    /// Print a single character (ASCII or multi-byte UTF-8).
26    Print(char),
27    /// Line feed / newline (`\n`).
28    Newline,
29    /// Carriage return (`\r`).
30    CarriageReturn,
31    /// Horizontal tab (`\t`).
32    Tab,
33    /// Backspace (`\x08`).
34    Backspace,
35    /// Bell (`\x07`).
36    Bell,
37    /// CUU (`CSI Ps A`): move cursor up by count (default 1).
38    CursorUp(u16),
39    /// CUD (`CSI Ps B`): move cursor down by count (default 1).
40    CursorDown(u16),
41    /// CUF (`CSI Ps C`): move cursor right by count (default 1).
42    CursorRight(u16),
43    /// CUB (`CSI Ps D`): move cursor left by count (default 1).
44    CursorLeft(u16),
45    /// CNL (`CSI Ps E`): move cursor down by count and to column 0.
46    CursorNextLine(u16),
47    /// CPL (`CSI Ps F`): move cursor up by count and to column 0.
48    CursorPrevLine(u16),
49    /// CHA (`CSI Ps G`): move cursor to absolute column (0-indexed).
50    CursorColumn(u16),
51    /// VPA (`CSI Ps d`): move cursor to absolute row (0-indexed).
52    CursorRow(u16),
53    /// DECSTBM (`CSI Pt ; Pb r`): set scrolling region. `bottom == 0` means
54    /// "use full height" (default), since the parser does not know the grid size.
55    ///
56    /// `top` is 0-indexed inclusive. `bottom` is 0-indexed exclusive when non-zero.
57    SetScrollRegion { top: u16, bottom: u16 },
58    /// SU (`CSI Ps S`): scroll the scroll region up by count (default 1).
59    ScrollUp(u16),
60    /// SD (`CSI Ps T`): scroll the scroll region down by count (default 1).
61    ScrollDown(u16),
62    /// IL (`CSI Ps L`): insert blank lines at cursor row within scroll region.
63    InsertLines(u16),
64    /// DL (`CSI Ps M`): delete lines at cursor row within scroll region.
65    DeleteLines(u16),
66    /// ICH (`CSI Ps @`): insert blank cells at cursor column.
67    InsertChars(u16),
68    /// DCH (`CSI Ps P`): delete cells at cursor column.
69    DeleteChars(u16),
70    /// CUP/HVP: move cursor to absolute 0-indexed row/col.
71    CursorPosition { row: u16, col: u16 },
72    /// ED mode (`CSI Ps J`): 0, 1, or 2.
73    EraseInDisplay(u8),
74    /// EL mode (`CSI Ps K`): 0, 1, or 2.
75    EraseInLine(u8),
76    /// SGR (`CSI ... m`): set graphics rendition parameters (attributes/colors).
77    ///
78    /// Parameters are returned as parsed numeric values; interpretation is
79    /// performed by the terminal engine (they are stateful/delta-based).
80    Sgr(CsiParams),
81    /// DECSET (`CSI ? Pm h`): enable DEC private mode(s).
82    DecSet(CsiParams),
83    /// DECRST (`CSI ? Pm l`): disable DEC private mode(s).
84    DecRst(CsiParams),
85    /// SM (`CSI Pm h`): enable ANSI standard mode(s).
86    AnsiSet(CsiParams),
87    /// RM (`CSI Pm l`): disable ANSI standard mode(s).
88    AnsiRst(CsiParams),
89    /// DECSC (`ESC 7`): save cursor state.
90    SaveCursor,
91    /// DECRC (`ESC 8`): restore cursor state.
92    RestoreCursor,
93    /// IND (`ESC D`): index — move cursor down one line, scrolling if at bottom.
94    Index,
95    /// RI (`ESC M`): reverse index — move cursor up one line, scrolling if at top.
96    ReverseIndex,
97    /// NEL (`ESC E`): next line — move cursor to start of next line.
98    NextLine,
99    /// RIS (`ESC c`): full reset to initial state.
100    FullReset,
101    /// OSC 0/2: set terminal title.
102    SetTitle(String),
103    /// OSC 8: start a hyperlink with the given URI.
104    HyperlinkStart(String),
105    /// OSC 8: end the current hyperlink.
106    HyperlinkEnd,
107    /// HTS (`ESC H`): set a tab stop at the current cursor column.
108    SetTabStop,
109    /// TBC (`CSI Ps g`): tab clear. 0 = at cursor, 3 = all tab stops.
110    ClearTabStop(u16),
111    /// CBT (`CSI Ps Z`): cursor backward tabulation by count (default 1).
112    BackTab(u16),
113    /// DECKPAM (`ESC =`): application keypad mode.
114    ApplicationKeypad,
115    /// DECKPNM (`ESC >`): normal keypad mode.
116    NormalKeypad,
117    /// ECH (`CSI Ps X`): erase characters at cursor position (replace with blanks).
118    EraseChars(u16),
119    /// DECALN (`ESC # 8`): DEC screen alignment test — fill entire grid with 'E'.
120    ScreenAlignment,
121    /// REP (`CSI Ps b`): repeat the most recently printed graphic character Ps times.
122    RepeatChar(u16),
123    /// DECSCUSR (`CSI Ps SP q`): set cursor shape.
124    ///
125    /// 0 = default, 1 = blinking block, 2 = steady block,
126    /// 3 = blinking underline, 4 = steady underline,
127    /// 5 = blinking bar, 6 = steady bar.
128    SetCursorShape(u8),
129    /// DECSTR (`CSI ! p`): soft terminal reset.
130    ///
131    /// Resets most terminal state (modes, SGR, scroll region, cursor
132    /// visibility, character sets) without clearing the screen or
133    /// scrollback — unlike RIS (`ESC c`) which is a full reset.
134    SoftReset,
135    /// ED mode 3 (`CSI 3 J`): erase the entire display and scrollback buffer.
136    EraseScrollback,
137    /// Focus gained (`CSI I`): terminal window received focus.
138    FocusIn,
139    /// Focus lost (`CSI O`): terminal window lost focus.
140    FocusOut,
141    /// Bracketed paste start (`CSI 200 ~`).
142    PasteStart,
143    /// Bracketed paste end (`CSI 201 ~`).
144    PasteEnd,
145    /// DA1 (`CSI c` / `CSI 0 c`): request primary device attributes.
146    DeviceAttributes,
147    /// DA2 (`CSI > c` / `CSI > 0 c`): request secondary device attributes.
148    DeviceAttributesSecondary,
149    /// DSR (`CSI 5 n`): device status report — "are you OK?" query.
150    DeviceStatusReport,
151    /// CPR (`CSI 6 n`): cursor position report — request current cursor position.
152    CursorPositionReport,
153    /// Designate character set for slot G0..G3.
154    ///
155    /// `slot`: 0 = G0 (ESC (), 1 = G1 (ESC )), 2 = G2 (ESC *), 3 = G3 (ESC +).
156    /// `charset`: b'B' = ASCII (USASCII), b'0' = DEC Special Graphics, etc.
157    DesignateCharset { slot: u8, charset: u8 },
158    /// SS2 (`ESC N`): single shift to G2 for the next printed character only.
159    SingleShift2,
160    /// SS3 (`ESC O`): single shift to G3 for the next printed character only.
161    SingleShift3,
162    /// Mouse event from SGR mode 1006 (`CSI < Pb ; Px ; Py M/m`) or
163    /// legacy X10 mode 1000 (`CSI M Cb Cx Cy`).
164    ///
165    /// `button`: 0 = left, 1 = middle, 2 = right, 3 = release (legacy),
166    /// 64 = scroll up, 65 = scroll down. Modifier bits: 4 = shift, 8 = meta, 16 = ctrl.
167    /// `col`/`row`: 0-based cell coordinates.
168    /// `pressed`: true for press/motion, false for release.
169    MouseEvent {
170        button: u16,
171        col: u16,
172        row: u16,
173        pressed: bool,
174    },
175    /// A raw escape/CSI/OSC sequence captured verbatim (starts with ESC).
176    Escape(Vec<u8>),
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180enum State {
181    Ground,
182    Esc,
183    /// ESC # intermediate — waiting for the final byte (e.g., '8' for DECALN).
184    EscHash,
185    /// ESC ( / ESC ) / ESC * / ESC + — waiting for charset designator byte.
186    /// `slot`: 0 = G0, 1 = G1, 2 = G2, 3 = G3.
187    EscCharset {
188        slot: u8,
189    },
190    Csi,
191    Osc,
192    OscEsc,
193    /// Accumulating a multi-byte UTF-8 character.
194    /// `bytes_remaining` counts how many continuation bytes are still expected.
195    Utf8 {
196        bytes_remaining: u8,
197    },
198}
199
200/// Upper bound for retained internal escape-buffer capacity between sequences.
201///
202/// This avoids pathological allocator churn after an unusually large escape
203/// payload inflates `self.buf` capacity.
204const ESC_BUF_RETAIN_CAP_MAX: usize = 16 * 1024;
205
206/// VT/ANSI parser state.
207#[derive(Debug, Clone)]
208pub struct Parser {
209    state: State,
210    buf: Vec<u8>,
211    /// Accumulator for multi-byte UTF-8 character assembly.
212    utf8_buf: [u8; 4],
213    /// Number of bytes accumulated so far in `utf8_buf`.
214    utf8_len: u8,
215}
216
217impl Default for Parser {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223impl Parser {
224    /// Create a new parser in ground state.
225    #[must_use]
226    pub fn new() -> Self {
227        Self {
228            state: State::Ground,
229            buf: Vec::new(),
230            utf8_buf: [0; 4],
231            utf8_len: 0,
232        }
233    }
234
235    /// Feed a chunk of bytes and return parsed actions.
236    #[must_use]
237    pub fn feed(&mut self, bytes: &[u8]) -> Vec<Action> {
238        let mut out = Vec::new();
239        self.feed_into(bytes, &mut out);
240        out
241    }
242
243    /// Feed a chunk of bytes, appending parsed actions to `out`.
244    ///
245    /// This avoids allocating a new Vec per call — callers on hot paths can
246    /// reuse the same output buffer across frames by calling `out.clear()`
247    /// before each invocation while retaining the underlying capacity.
248    pub fn feed_into(&mut self, bytes: &[u8], out: &mut Vec<Action>) {
249        for &b in bytes {
250            if let Some(action) = self.advance(b) {
251                out.push(action);
252            }
253        }
254    }
255
256    /// Advance the parser by one byte.
257    ///
258    /// Returns an action when a complete token is recognized.
259    pub fn advance(&mut self, b: u8) -> Option<Action> {
260        match self.state {
261            State::Ground => self.advance_ground(b),
262            State::Esc => self.advance_esc(b),
263            State::EscHash => self.advance_esc_hash(b),
264            State::EscCharset { slot } => self.advance_esc_charset(b, slot),
265            State::Csi => self.advance_csi(b),
266            State::Osc => self.advance_osc(b),
267            State::OscEsc => self.advance_osc_esc(b),
268            State::Utf8 { bytes_remaining } => self.advance_utf8(b, bytes_remaining),
269        }
270    }
271
272    fn advance_ground(&mut self, b: u8) -> Option<Action> {
273        match b {
274            b'\n' | 0x0B | 0x0C => Some(Action::Newline), // LF, VT, FF all treated as newline
275            b'\r' => Some(Action::CarriageReturn),
276            b'\t' => Some(Action::Tab),
277            0x08 => Some(Action::Backspace),
278            0x07 => Some(Action::Bell),
279            0x1b => {
280                self.state = State::Esc;
281                self.buf.clear();
282                self.buf.push(0x1b);
283                None
284            }
285            0x20..=0x7E => Some(Action::Print(b as char)),
286            // UTF-8 multi-byte sequence leading bytes:
287            0xC2..=0xDF => {
288                // 2-byte sequence (0xC0-0xC1 are overlong, rejected)
289                self.utf8_buf[0] = b;
290                self.utf8_len = 1;
291                self.state = State::Utf8 { bytes_remaining: 1 };
292                None
293            }
294            0xE0..=0xEF => {
295                // 3-byte sequence
296                self.utf8_buf[0] = b;
297                self.utf8_len = 1;
298                self.state = State::Utf8 { bytes_remaining: 2 };
299                None
300            }
301            0xF0..=0xF4 => {
302                // 4-byte sequence (0xF5-0xF7 are outside valid Unicode range)
303                self.utf8_buf[0] = b;
304                self.utf8_len = 1;
305                self.state = State::Utf8 { bytes_remaining: 3 };
306                None
307            }
308            _ => None, // ignore C0 controls (0x00-0x06, 0x0E-0x1A, 0x1C-0x1F)
309                       // and invalid UTF-8 leading bytes (0x80-0xBF, 0xC0-0xC1, 0xF5-0xFF)
310        }
311    }
312
313    /// Accumulate continuation bytes for a multi-byte UTF-8 character.
314    fn advance_utf8(&mut self, b: u8, bytes_remaining: u8) -> Option<Action> {
315        // Continuation bytes must be in 0x80..=0xBF.
316        if (0x80..=0xBF).contains(&b) {
317            let idx = self.utf8_len as usize;
318            if idx < 4 {
319                self.utf8_buf[idx] = b;
320                self.utf8_len += 1;
321            }
322            if bytes_remaining == 1 {
323                // Sequence complete — try to decode.
324                self.state = State::Ground;
325                let len = self.utf8_len as usize;
326                let ch = core::str::from_utf8(&self.utf8_buf[..len])
327                    .ok()
328                    .and_then(|s| s.chars().next());
329                self.utf8_len = 0;
330                ch.map(Action::Print)
331            } else {
332                self.state = State::Utf8 {
333                    bytes_remaining: bytes_remaining - 1,
334                };
335                None
336            }
337        } else {
338            // Invalid continuation byte — abort UTF-8, reprocess this byte
339            // in ground state (replacement character is omitted per VT semantics;
340            // terminal emulators typically drop malformed sequences).
341            self.state = State::Ground;
342            self.utf8_len = 0;
343            self.advance_ground(b)
344        }
345    }
346
347    fn advance_esc(&mut self, b: u8) -> Option<Action> {
348        self.buf.push(b);
349        match b {
350            b'[' => {
351                self.state = State::Csi;
352                None
353            }
354            b']' => {
355                self.state = State::Osc;
356                None
357            }
358            // DECSC: save cursor (ESC 7)
359            b'7' => {
360                self.state = State::Ground;
361                self.buf.clear();
362                Some(Action::SaveCursor)
363            }
364            // DECRC: restore cursor (ESC 8)
365            b'8' => {
366                self.state = State::Ground;
367                self.buf.clear();
368                Some(Action::RestoreCursor)
369            }
370            // IND: index — cursor down, scroll if at bottom margin (ESC D)
371            b'D' => {
372                self.state = State::Ground;
373                self.buf.clear();
374                Some(Action::Index)
375            }
376            // RI: reverse index — cursor up, scroll if at top margin (ESC M)
377            b'M' => {
378                self.state = State::Ground;
379                self.buf.clear();
380                Some(Action::ReverseIndex)
381            }
382            // NEL: next line — CR + LF (ESC E)
383            b'E' => {
384                self.state = State::Ground;
385                self.buf.clear();
386                Some(Action::NextLine)
387            }
388            // RIS: full reset to initial state (ESC c)
389            b'c' => {
390                self.state = State::Ground;
391                self.buf.clear();
392                Some(Action::FullReset)
393            }
394            // HTS: set tab stop at current column (ESC H)
395            b'H' => {
396                self.state = State::Ground;
397                self.buf.clear();
398                Some(Action::SetTabStop)
399            }
400            // DECKPAM: application keypad mode (ESC =)
401            b'=' => {
402                self.state = State::Ground;
403                self.buf.clear();
404                Some(Action::ApplicationKeypad)
405            }
406            // DECKPNM: normal keypad mode (ESC >)
407            b'>' => {
408                self.state = State::Ground;
409                self.buf.clear();
410                Some(Action::NormalKeypad)
411            }
412            // ESC # intermediate — wait for the final byte (e.g., DECALN).
413            b'#' => {
414                self.state = State::EscHash;
415                None
416            }
417            // Character set designation: ESC ( / ESC ) / ESC * / ESC +
418            b'(' => {
419                self.state = State::EscCharset { slot: 0 };
420                None
421            }
422            b')' => {
423                self.state = State::EscCharset { slot: 1 };
424                None
425            }
426            b'*' => {
427                self.state = State::EscCharset { slot: 2 };
428                None
429            }
430            b'+' => {
431                self.state = State::EscCharset { slot: 3 };
432                None
433            }
434            // SS2: single shift to G2 (ESC N)
435            b'N' => {
436                self.state = State::Ground;
437                self.buf.clear();
438                Some(Action::SingleShift2)
439            }
440            // SS3: single shift to G3 (ESC O)
441            b'O' => {
442                self.state = State::Ground;
443                self.buf.clear();
444                Some(Action::SingleShift3)
445            }
446            _ => {
447                self.state = State::Ground;
448                Some(Action::Escape(self.take_buf()))
449            }
450        }
451    }
452
453    fn advance_esc_hash(&mut self, b: u8) -> Option<Action> {
454        self.buf.push(b);
455        self.state = State::Ground;
456        match b {
457            // DECALN: DEC screen alignment test (ESC # 8).
458            b'8' => {
459                self.buf.clear();
460                Some(Action::ScreenAlignment)
461            }
462            _ => Some(Action::Escape(self.take_buf())),
463        }
464    }
465
466    fn advance_esc_charset(&mut self, b: u8, slot: u8) -> Option<Action> {
467        self.buf.push(b);
468        self.state = State::Ground;
469        // The final byte is the charset designator (e.g., 'B' for ASCII, '0' for DEC Special).
470        self.buf.clear();
471        Some(Action::DesignateCharset { slot, charset: b })
472    }
473
474    fn advance_csi(&mut self, b: u8) -> Option<Action> {
475        self.buf.push(b);
476        // Final byte for CSI is in the 0x40..=0x7E range (ECMA-48).
477        if (0x40..=0x7E).contains(&b) {
478            self.state = State::Ground;
479            let seq = self.take_buf();
480            return Some(Self::decode_csi(&seq).unwrap_or(Action::Escape(seq)));
481        }
482        None
483    }
484
485    fn advance_osc(&mut self, b: u8) -> Option<Action> {
486        self.buf.push(b);
487        match b {
488            0x07 => {
489                // BEL terminator.
490                self.state = State::Ground;
491                let seq = self.take_buf();
492                Some(Self::decode_osc(&seq).unwrap_or(Action::Escape(seq)))
493            }
494            0x1b => {
495                // ESC, possibly starting ST terminator (ESC \).
496                self.state = State::OscEsc;
497                None
498            }
499            _ => None,
500        }
501    }
502
503    fn advance_osc_esc(&mut self, b: u8) -> Option<Action> {
504        self.buf.push(b);
505        if b == b'\\' {
506            // ST terminator.
507            self.state = State::Ground;
508            let seq = self.take_buf();
509            return Some(Self::decode_osc(&seq).unwrap_or(Action::Escape(seq)));
510        }
511        // False alarm; continue OSC.
512        self.state = State::Osc;
513        None
514    }
515
516    fn take_buf(&mut self) -> Vec<u8> {
517        // Retain capacity for common-case reuse, but cap retained capacity so a
518        // single very large escape payload doesn't force repeated oversized
519        // allocations on subsequent small sequences.
520        let retained_cap = self.buf.capacity().min(ESC_BUF_RETAIN_CAP_MAX);
521        core::mem::replace(&mut self.buf, Vec::with_capacity(retained_cap))
522    }
523
524    fn decode_csi(seq: &[u8]) -> Option<Action> {
525        if seq.len() < 3 || seq[0] != 0x1b || seq[1] != b'[' {
526            return None;
527        }
528        let final_byte = *seq.last()?;
529        let param_bytes = &seq[2..seq.len().saturating_sub(1)];
530
531        // Check for DEC private mode indicator `?` prefix.
532        if param_bytes.first() == Some(&b'?') {
533            let params = Self::parse_csi_params(&param_bytes[1..])?;
534            return match final_byte {
535                b'h' => Some(Action::DecSet(params)),
536                b'l' => Some(Action::DecRst(params)),
537                _ => None,
538            };
539        }
540
541        // Check for `>` prefix (secondary device attributes).
542        if param_bytes.first() == Some(&b'>') {
543            return match final_byte {
544                // DA2 (CSI > c / CSI > 0 c): secondary device attributes.
545                b'c' => Some(Action::DeviceAttributesSecondary),
546                _ => None,
547            };
548        }
549
550        // Check for `<` prefix (SGR mouse mode 1006).
551        // Format: CSI < Pb ; Px ; Py M (press) or CSI < Pb ; Px ; Py m (release).
552        if param_bytes.first() == Some(&b'<') {
553            if final_byte == b'M' || final_byte == b'm' {
554                let params = Self::parse_csi_params(&param_bytes[1..])?;
555                if params.len() == 3 {
556                    let button = params[0];
557                    // SGR mouse coords are 1-based; convert to 0-based.
558                    let col = params[1].saturating_sub(1);
559                    let row = params[2].saturating_sub(1);
560                    let pressed = final_byte == b'M';
561                    return Some(Action::MouseEvent {
562                        button,
563                        col,
564                        row,
565                        pressed,
566                    });
567                }
568            }
569            return None;
570        }
571
572        // Separate intermediate bytes (0x20..=0x2F per ECMA-48) from parameter
573        // bytes. Intermediates follow parameters and precede the final byte.
574        let intermediate_start = param_bytes
575            .iter()
576            .position(|&b| (0x20..=0x2F).contains(&b))
577            .unwrap_or(param_bytes.len());
578        let (numeric_bytes, intermediates) = param_bytes.split_at(intermediate_start);
579
580        // Dispatch sequences with intermediate bytes first.
581        match (intermediates, final_byte) {
582            // DECSCUSR (CSI Ps SP q): set cursor shape.
583            (b" ", b'q') => {
584                let params = Self::parse_csi_params(numeric_bytes)?;
585                let shape = params.first().copied().unwrap_or(0);
586                return Some(Action::SetCursorShape(shape.min(6) as u8));
587            }
588            // DECSTR (CSI ! p): soft terminal reset.
589            (b"!", b'p') => return Some(Action::SoftReset),
590            // Unknown intermediate+final combination.
591            _ if !intermediates.is_empty() => return None,
592            _ => {}
593        }
594
595        // Standard CSI dispatch (no intermediate bytes).
596        let params = Self::parse_csi_params(numeric_bytes)?;
597
598        match final_byte {
599            b'A' => Some(Action::CursorUp(Self::csi_count_or_one(
600                params.first().copied(),
601            ))),
602            b'B' => Some(Action::CursorDown(Self::csi_count_or_one(
603                params.first().copied(),
604            ))),
605            b'C' => Some(Action::CursorRight(Self::csi_count_or_one(
606                params.first().copied(),
607            ))),
608            b'D' => Some(Action::CursorLeft(Self::csi_count_or_one(
609                params.first().copied(),
610            ))),
611            b'E' => Some(Action::CursorNextLine(Self::csi_count_or_one(
612                params.first().copied(),
613            ))),
614            b'F' => Some(Action::CursorPrevLine(Self::csi_count_or_one(
615                params.first().copied(),
616            ))),
617            b'G' => Some(Action::CursorColumn(
618                Self::csi_count_or_one(params.first().copied()).saturating_sub(1),
619            )),
620            b'H' | b'f' => {
621                // CUP/HVP use 1-indexed coordinates; 0 is treated as 1.
622                let row = params
623                    .first()
624                    .copied()
625                    .unwrap_or(1)
626                    .max(1)
627                    .saturating_sub(1);
628                let col = params.get(1).copied().unwrap_or(1).max(1).saturating_sub(1);
629                Some(Action::CursorPosition { row, col })
630            }
631            b'J' => {
632                let mode = params.first().copied().unwrap_or(0);
633                match mode {
634                    0..=2 => Some(Action::EraseInDisplay(mode as u8)),
635                    3 => Some(Action::EraseScrollback),
636                    _ => None,
637                }
638            }
639            b'K' => {
640                let mode = params.first().copied().unwrap_or(0);
641                if mode <= 2 {
642                    Some(Action::EraseInLine(mode as u8))
643                } else {
644                    None
645                }
646            }
647            b'd' => Some(Action::CursorRow(
648                Self::csi_count_or_one(params.first().copied()).saturating_sub(1),
649            )),
650            b'L' => Some(Action::InsertLines(Self::csi_count_or_one(
651                params.first().copied(),
652            ))),
653            b'M' => Some(Action::DeleteLines(Self::csi_count_or_one(
654                params.first().copied(),
655            ))),
656            b'@' => Some(Action::InsertChars(Self::csi_count_or_one(
657                params.first().copied(),
658            ))),
659            b'P' => Some(Action::DeleteChars(Self::csi_count_or_one(
660                params.first().copied(),
661            ))),
662            b'S' => Some(Action::ScrollUp(Self::csi_count_or_one(
663                params.first().copied(),
664            ))),
665            b'T' => Some(Action::ScrollDown(Self::csi_count_or_one(
666                params.first().copied(),
667            ))),
668            b'r' => {
669                let top = params
670                    .first()
671                    .copied()
672                    .unwrap_or(0)
673                    .max(1)
674                    .saturating_sub(1);
675                let bottom = params.get(1).copied().unwrap_or(0);
676                Some(Action::SetScrollRegion { top, bottom })
677            }
678            b'm' => Some(Action::Sgr(params)),
679            // TBC: tab clear (CSI Ps g)
680            b'g' => {
681                let mode = params.first().copied().unwrap_or(0);
682                Some(Action::ClearTabStop(mode))
683            }
684            // CBT: cursor backward tabulation (CSI Ps Z)
685            b'Z' => Some(Action::BackTab(Self::csi_count_or_one(
686                params.first().copied(),
687            ))),
688            // ECH: erase characters at cursor (CSI Ps X)
689            b'X' => Some(Action::EraseChars(Self::csi_count_or_one(
690                params.first().copied(),
691            ))),
692            // REP: repeat the most recently printed graphic character (CSI Ps b)
693            b'b' => Some(Action::RepeatChar(Self::csi_count_or_one(
694                params.first().copied(),
695            ))),
696            // SCOSC: save cursor position (ANSI-style, CSI s)
697            b's' => {
698                if params.is_empty() || (params.len() == 1 && params[0] == 0) {
699                    Some(Action::SaveCursor)
700                } else {
701                    // CSI with params on 's' could be DECSLRM; ignore for now.
702                    None
703                }
704            }
705            // SCORC: restore cursor position (ANSI-style, CSI u)
706            b'u' => {
707                if params.is_empty() || (params.len() == 1 && params[0] == 0) {
708                    Some(Action::RestoreCursor)
709                } else {
710                    None
711                }
712            }
713            // Focus events (CSI I / CSI O)
714            b'I' => Some(Action::FocusIn),
715            b'O' => Some(Action::FocusOut),
716            // Bracketed paste + function keys (CSI Ps ~)
717            b'~' => {
718                let param = params.first().copied().unwrap_or(0);
719                match param {
720                    200 => Some(Action::PasteStart),
721                    201 => Some(Action::PasteEnd),
722                    _ => None,
723                }
724            }
725            // DA1: primary device attributes (CSI c / CSI 0 c)
726            b'c' => {
727                let p = params.first().copied().unwrap_or(0);
728                if p == 0 {
729                    Some(Action::DeviceAttributes)
730                } else {
731                    None
732                }
733            }
734            // DSR / CPR: device status report (CSI Ps n)
735            b'n' => {
736                let p = params.first().copied().unwrap_or(0);
737                match p {
738                    5 => Some(Action::DeviceStatusReport),
739                    6 => Some(Action::CursorPositionReport),
740                    _ => None,
741                }
742            }
743            // SM: set ANSI mode(s)
744            b'h' => Some(Action::AnsiSet(params)),
745            // RM: reset ANSI mode(s)
746            b'l' => Some(Action::AnsiRst(params)),
747            _ => None,
748        }
749    }
750
751    fn decode_osc(seq: &[u8]) -> Option<Action> {
752        if seq.len() < 4 || seq[0] != 0x1b || seq[1] != b']' {
753            return None;
754        }
755
756        // Strip terminator (BEL or ST).
757        let content = if *seq.last()? == 0x07 {
758            &seq[2..seq.len().saturating_sub(1)]
759        } else if seq.len() >= 4 && seq[seq.len() - 2] == 0x1b && seq[seq.len() - 1] == b'\\' {
760            &seq[2..seq.len().saturating_sub(2)]
761        } else {
762            return None;
763        };
764
765        let first_semi = content.iter().position(|&b| b == b';')?;
766        let cmd = core::str::from_utf8(&content[..first_semi]).ok()?;
767        let cmd: u16 = cmd.parse().ok()?;
768        let rest = &content[first_semi + 1..];
769
770        match cmd {
771            0 | 2 => {
772                let title = String::from_utf8_lossy(rest).to_string();
773                Some(Action::SetTitle(title))
774            }
775            8 => {
776                // OSC 8 ; params ; uri ST/BEL
777                let second_semi = rest.iter().position(|&b| b == b';')?;
778                let uri = &rest[second_semi + 1..];
779                if uri.is_empty() {
780                    Some(Action::HyperlinkEnd)
781                } else {
782                    Some(Action::HyperlinkStart(
783                        String::from_utf8_lossy(uri).to_string(),
784                    ))
785                }
786            }
787            _ => None,
788        }
789    }
790
791    fn parse_csi_params(params: &[u8]) -> Option<CsiParams> {
792        if params.is_empty() {
793            return Some(CsiParams::new());
794        }
795        let s = core::str::from_utf8(params).ok()?;
796        let mut out = CsiParams::new();
797        for part in s.split(';') {
798            if part.is_empty() {
799                out.push(0);
800                continue;
801            }
802            let value = part.parse::<u32>().ok()?;
803            out.push(value.min(u16::MAX as u32) as u16);
804        }
805        Some(out)
806    }
807
808    fn csi_count_or_one(value: Option<u16>) -> u16 {
809        value.unwrap_or(1).max(1)
810    }
811}
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816    use smallvec::smallvec;
817
818    // ── ASCII / Ground ─────────────────────────────────────────────
819
820    #[test]
821    fn printable_ascii_emits_print() {
822        let mut p = Parser::new();
823        let actions = p.feed(b"hi");
824        assert_eq!(actions, vec![Action::Print('h'), Action::Print('i')]);
825    }
826
827    #[test]
828    fn c0_controls_emit_actions() {
829        let mut p = Parser::new();
830        let actions = p.feed(b"\t\r\n");
831        assert_eq!(
832            actions,
833            vec![Action::Tab, Action::CarriageReturn, Action::Newline]
834        );
835    }
836
837    #[test]
838    fn vt_and_ff_treated_as_newline() {
839        let mut p = Parser::new();
840        // VT (0x0B) and FF (0x0C) both produce Newline
841        assert_eq!(p.feed(b"\x0b"), vec![Action::Newline]);
842        assert_eq!(p.feed(b"\x0c"), vec![Action::Newline]);
843    }
844
845    // ── UTF-8 multi-byte characters ────────────────────────────────
846
847    #[test]
848    fn utf8_two_byte_character() {
849        let mut p = Parser::new();
850        // é = U+00E9 = 0xC3 0xA9
851        let actions = p.feed("é".as_bytes());
852        assert_eq!(actions, vec![Action::Print('é')]);
853    }
854
855    #[test]
856    fn utf8_three_byte_character() {
857        let mut p = Parser::new();
858        // 中 = U+4E2D = 0xE4 0xB8 0xAD
859        let actions = p.feed("中".as_bytes());
860        assert_eq!(actions, vec![Action::Print('中')]);
861    }
862
863    #[test]
864    fn utf8_four_byte_character() {
865        let mut p = Parser::new();
866        // 🎉 = U+1F389 = 0xF0 0x9F 0x8E 0x89
867        let actions = p.feed("🎉".as_bytes());
868        assert_eq!(actions, vec![Action::Print('🎉')]);
869    }
870
871    #[test]
872    fn utf8_mixed_with_ascii() {
873        let mut p = Parser::new();
874        let actions = p.feed("aé中🎉b".as_bytes());
875        assert_eq!(
876            actions,
877            vec![
878                Action::Print('a'),
879                Action::Print('é'),
880                Action::Print('中'),
881                Action::Print('🎉'),
882                Action::Print('b'),
883            ]
884        );
885    }
886
887    #[test]
888    fn utf8_split_across_feeds() {
889        let mut p = Parser::new();
890        // Feed é (0xC3 0xA9) byte by byte
891        assert_eq!(p.feed(&[0xC3]), Vec::<Action>::new());
892        assert_eq!(p.feed(&[0xA9]), vec![Action::Print('é')]);
893    }
894
895    #[test]
896    fn utf8_split_four_byte_across_feeds() {
897        let mut p = Parser::new();
898        // 🎉 = 0xF0 0x9F 0x8E 0x89
899        assert!(p.feed(&[0xF0]).is_empty());
900        assert!(p.feed(&[0x9F]).is_empty());
901        assert!(p.feed(&[0x8E]).is_empty());
902        assert_eq!(p.feed(&[0x89]), vec![Action::Print('🎉')]);
903    }
904
905    #[test]
906    fn utf8_invalid_continuation_aborts_and_reprocesses() {
907        let mut p = Parser::new();
908        // Start a 2-byte sequence (0xC3) then send ASCII 'a' instead of continuation
909        let actions = p.feed(&[0xC3, b'a']);
910        // The invalid sequence is dropped, 'a' is reprocessed as ASCII
911        assert_eq!(actions, vec![Action::Print('a')]);
912    }
913
914    #[test]
915    fn utf8_overlong_leading_bytes_are_ignored() {
916        let mut p = Parser::new();
917        // 0xC0 and 0xC1 are overlong leading bytes — should be ignored
918        assert!(p.feed(&[0xC0]).is_empty());
919        assert!(p.feed(&[0xC1]).is_empty());
920    }
921
922    #[test]
923    fn utf8_invalid_leading_bytes_above_f4_ignored() {
924        let mut p = Parser::new();
925        // 0xF5-0xFF are above valid Unicode range
926        assert!(p.feed(&[0xF5]).is_empty());
927        assert!(p.feed(&[0xFF]).is_empty());
928    }
929
930    #[test]
931    fn utf8_interrupted_by_escape() {
932        let mut p = Parser::new();
933        // Start UTF-8, then get ESC — should abort UTF-8 and process ESC
934        let actions = p.feed(&[0xC3, 0x1b, b'c']);
935        // 0xC3 starts UTF-8, 0x1b is not a valid continuation so abort,
936        // reprocess 0x1b as ESC, then 'c' completes ESC c -> FullReset
937        assert_eq!(actions, vec![Action::FullReset]);
938    }
939
940    #[test]
941    fn utf8_japanese_text() {
942        let mut p = Parser::new();
943        let actions = p.feed("こんにちは".as_bytes());
944        assert_eq!(
945            actions,
946            vec![
947                Action::Print('こ'),
948                Action::Print('ん'),
949                Action::Print('に'),
950                Action::Print('ち'),
951                Action::Print('は'),
952            ]
953        );
954    }
955
956    // ── DECSET / DECRST ────────────────────────────────────────────
957
958    #[test]
959    fn decset_cursor_hide() {
960        let mut p = Parser::new();
961        let actions = p.feed(b"\x1b[?25l");
962        assert_eq!(actions, vec![Action::DecRst(smallvec![25])]);
963    }
964
965    #[test]
966    fn decset_cursor_show() {
967        let mut p = Parser::new();
968        let actions = p.feed(b"\x1b[?25h");
969        assert_eq!(actions, vec![Action::DecSet(smallvec![25])]);
970    }
971
972    #[test]
973    fn decset_multiple_modes() {
974        let mut p = Parser::new();
975        // Enable alt screen + bracketed paste + mouse SGR in one sequence
976        let actions = p.feed(b"\x1b[?1049;2004;1006h");
977        assert_eq!(actions, vec![Action::DecSet(smallvec![1049, 2004, 1006])]);
978    }
979
980    #[test]
981    fn decrst_multiple_modes() {
982        let mut p = Parser::new();
983        let actions = p.feed(b"\x1b[?1049;2004l");
984        assert_eq!(actions, vec![Action::DecRst(smallvec![1049, 2004])]);
985    }
986
987    #[test]
988    fn decset_sync_output() {
989        let mut p = Parser::new();
990        assert_eq!(
991            p.feed(b"\x1b[?2026h"),
992            vec![Action::DecSet(smallvec![2026])]
993        );
994        assert_eq!(
995            p.feed(b"\x1b[?2026l"),
996            vec![Action::DecRst(smallvec![2026])]
997        );
998    }
999
1000    #[test]
1001    fn decset_autowrap() {
1002        let mut p = Parser::new();
1003        assert_eq!(p.feed(b"\x1b[?7h"), vec![Action::DecSet(smallvec![7])]);
1004        assert_eq!(p.feed(b"\x1b[?7l"), vec![Action::DecRst(smallvec![7])]);
1005    }
1006
1007    // ── ANSI SM / RM ──────────────────────────────────────────────
1008
1009    #[test]
1010    fn ansi_set_insert_mode() {
1011        let mut p = Parser::new();
1012        // SM (CSI 4 h) — set insert mode
1013        assert_eq!(p.feed(b"\x1b[4h"), vec![Action::AnsiSet(smallvec![4])]);
1014        // RM (CSI 4 l) — reset insert mode
1015        assert_eq!(p.feed(b"\x1b[4l"), vec![Action::AnsiRst(smallvec![4])]);
1016    }
1017
1018    #[test]
1019    fn ansi_set_newline_mode() {
1020        let mut p = Parser::new();
1021        assert_eq!(p.feed(b"\x1b[20h"), vec![Action::AnsiSet(smallvec![20])]);
1022        assert_eq!(p.feed(b"\x1b[20l"), vec![Action::AnsiRst(smallvec![20])]);
1023    }
1024
1025    // ── Cursor save/restore (DECSC/DECRC) ──────────────────────────
1026
1027    #[test]
1028    fn esc_7_saves_cursor() {
1029        let mut p = Parser::new();
1030        assert_eq!(p.feed(b"\x1b7"), vec![Action::SaveCursor]);
1031    }
1032
1033    #[test]
1034    fn esc_8_restores_cursor() {
1035        let mut p = Parser::new();
1036        assert_eq!(p.feed(b"\x1b8"), vec![Action::RestoreCursor]);
1037    }
1038
1039    #[test]
1040    fn save_restore_roundtrip_sequence() {
1041        let mut p = Parser::new();
1042        let actions = p.feed(b"\x1b7\x1b[5;10H\x1b8");
1043        assert_eq!(
1044            actions,
1045            vec![
1046                Action::SaveCursor,
1047                Action::CursorPosition { row: 4, col: 9 },
1048                Action::RestoreCursor,
1049            ]
1050        );
1051    }
1052
1053    // ── ESC-level sequences (IND, RI, NEL, RIS) ───────────────────
1054
1055    #[test]
1056    fn esc_d_is_index() {
1057        let mut p = Parser::new();
1058        assert_eq!(p.feed(b"\x1bD"), vec![Action::Index]);
1059    }
1060
1061    #[test]
1062    fn esc_m_is_reverse_index() {
1063        let mut p = Parser::new();
1064        assert_eq!(p.feed(b"\x1bM"), vec![Action::ReverseIndex]);
1065    }
1066
1067    #[test]
1068    fn esc_e_is_next_line() {
1069        let mut p = Parser::new();
1070        assert_eq!(p.feed(b"\x1bE"), vec![Action::NextLine]);
1071    }
1072
1073    #[test]
1074    fn esc_c_is_full_reset() {
1075        let mut p = Parser::new();
1076        assert_eq!(p.feed(b"\x1bc"), vec![Action::FullReset]);
1077    }
1078
1079    // ── Original CSI tests (preserved) ────────────────────────────
1080
1081    #[test]
1082    fn csi_sgr_is_decoded() {
1083        let mut p = Parser::new();
1084        assert_eq!(p.feed(b"\x1b[31m"), vec![Action::Sgr(smallvec![31])]);
1085        assert_eq!(p.feed(b"\x1b[m"), vec![Action::Sgr(smallvec![])]);
1086    }
1087
1088    #[test]
1089    fn csi_cup_is_decoded_to_cursor_position() {
1090        let mut p = Parser::new();
1091        let actions = p.feed(b"\x1b[5;10H");
1092        assert_eq!(
1093            actions,
1094            vec![Action::CursorPosition { row: 4, col: 9 }],
1095            "CUP should decode as 0-indexed cursor position"
1096        );
1097
1098        let actions = p.feed(b"\x1b[0;0H");
1099        assert_eq!(
1100            actions,
1101            vec![Action::CursorPosition { row: 0, col: 0 }],
1102            "CUP zero params should default to 1;1"
1103        );
1104    }
1105
1106    #[test]
1107    fn csi_ed_and_el_are_decoded() {
1108        let mut p = Parser::new();
1109        assert_eq!(p.feed(b"\x1b[2J"), vec![Action::EraseInDisplay(2)]);
1110        assert_eq!(p.feed(b"\x1b[K"), vec![Action::EraseInLine(0)]);
1111    }
1112
1113    #[test]
1114    fn csi_cursor_relative_moves_are_decoded() {
1115        let mut p = Parser::new();
1116        assert_eq!(
1117            p.feed(b"\x1b[2A\x1b[B\x1b[3C\x1b[0D"),
1118            vec![
1119                Action::CursorUp(2),
1120                Action::CursorDown(1),
1121                Action::CursorRight(3),
1122                Action::CursorLeft(1),
1123            ]
1124        );
1125    }
1126
1127    #[test]
1128    fn csi_cha_is_decoded_to_absolute_column() {
1129        let mut p = Parser::new();
1130        assert_eq!(p.feed(b"\x1b[5G"), vec![Action::CursorColumn(4)]);
1131        assert_eq!(p.feed(b"\x1b[0G"), vec![Action::CursorColumn(0)]);
1132    }
1133
1134    #[test]
1135    fn csi_cnl_cpl_and_vpa_are_decoded() {
1136        let mut p = Parser::new();
1137        assert_eq!(
1138            p.feed(b"\x1b[2E\x1b[F\x1b[3d\x1b[0d\x1b[d"),
1139            vec![
1140                Action::CursorNextLine(2),
1141                Action::CursorPrevLine(1),
1142                Action::CursorRow(2),
1143                Action::CursorRow(0),
1144                Action::CursorRow(0),
1145            ]
1146        );
1147    }
1148
1149    #[test]
1150    fn csi_scroll_region_and_insert_delete_are_decoded() {
1151        let mut p = Parser::new();
1152        assert_eq!(
1153            p.feed(b"\x1b[2;4r\x1b[r\x1b[2S\x1b[T\x1b[3L\x1b[M\x1b[4@\x1b[P"),
1154            vec![
1155                Action::SetScrollRegion { top: 1, bottom: 4 },
1156                Action::SetScrollRegion { top: 0, bottom: 0 },
1157                Action::ScrollUp(2),
1158                Action::ScrollDown(1),
1159                Action::InsertLines(3),
1160                Action::DeleteLines(1),
1161                Action::InsertChars(4),
1162                Action::DeleteChars(1),
1163            ]
1164        );
1165    }
1166
1167    #[test]
1168    fn osc_sequence_bel_terminated_is_captured() {
1169        let mut p = Parser::new();
1170        assert_eq!(
1171            p.feed(b"\x1b]0;title\x07"),
1172            vec![Action::SetTitle("title".to_string())]
1173        );
1174        assert_eq!(
1175            p.feed(b"\x1b]2;hi\x1b\\"),
1176            vec![Action::SetTitle("hi".to_string())]
1177        );
1178    }
1179
1180    #[test]
1181    fn osc8_hyperlink_is_decoded() {
1182        let mut p = Parser::new();
1183        assert_eq!(
1184            p.feed(b"\x1b]8;;https://example.com\x07"),
1185            vec![Action::HyperlinkStart("https://example.com".to_string())]
1186        );
1187        assert_eq!(p.feed(b"\x1b]8;;\x07"), vec![Action::HyperlinkEnd]);
1188        assert_eq!(
1189            p.feed(b"\x1b]8;;https://a.test\x1b\\"),
1190            vec![Action::HyperlinkStart("https://a.test".to_string())]
1191        );
1192        assert_eq!(p.feed(b"\x1b]8;;\x1b\\"), vec![Action::HyperlinkEnd]);
1193    }
1194
1195    // ── Integration: mixed sequences ───────────────────────────────
1196
1197    #[test]
1198    fn mixed_utf8_csi_osc_sequence() {
1199        let mut p = Parser::new();
1200        // "Hello" in Japanese, then set red, then move cursor
1201        let mut input = Vec::new();
1202        input.extend_from_slice("日本語".as_bytes());
1203        input.extend_from_slice(b"\x1b[31m");
1204        input.extend_from_slice(b"\x1b[5;1H");
1205        let actions = p.feed(&input);
1206        assert_eq!(
1207            actions,
1208            vec![
1209                Action::Print('日'),
1210                Action::Print('本'),
1211                Action::Print('語'),
1212                Action::Sgr(smallvec![31]),
1213                Action::CursorPosition { row: 4, col: 0 },
1214            ]
1215        );
1216    }
1217
1218    #[test]
1219    fn typical_terminal_setup_sequence() {
1220        let mut p = Parser::new();
1221        // Typical terminal init: alt screen + bracketed paste + mouse + hide cursor
1222        let actions = p.feed(b"\x1b[?1049h\x1b[?2004h\x1b[?1006h\x1b[?25l");
1223        assert_eq!(
1224            actions,
1225            vec![
1226                Action::DecSet(smallvec![1049]),
1227                Action::DecSet(smallvec![2004]),
1228                Action::DecSet(smallvec![1006]),
1229                Action::DecRst(smallvec![25]),
1230            ]
1231        );
1232    }
1233
1234    #[test]
1235    fn typical_terminal_teardown_sequence() {
1236        let mut p = Parser::new();
1237        // Typical terminal cleanup: show cursor + disable mouse + disable bracketed paste + exit alt screen
1238        let actions = p.feed(b"\x1b[?25h\x1b[?1006l\x1b[?2004l\x1b[?1049l");
1239        assert_eq!(
1240            actions,
1241            vec![
1242                Action::DecSet(smallvec![25]),
1243                Action::DecRst(smallvec![1006]),
1244                Action::DecRst(smallvec![2004]),
1245                Action::DecRst(smallvec![1049]),
1246            ]
1247        );
1248    }
1249
1250    // ── HTS / TBC / CBT (tab stop management) ────────────────────
1251
1252    #[test]
1253    fn esc_h_is_set_tab_stop() {
1254        let mut p = Parser::new();
1255        assert_eq!(p.feed(b"\x1bH"), vec![Action::SetTabStop]);
1256    }
1257
1258    #[test]
1259    fn csi_g_is_clear_tab_stop_at_cursor() {
1260        let mut p = Parser::new();
1261        assert_eq!(p.feed(b"\x1b[g"), vec![Action::ClearTabStop(0)]);
1262        assert_eq!(p.feed(b"\x1b[0g"), vec![Action::ClearTabStop(0)]);
1263    }
1264
1265    #[test]
1266    fn csi_3g_is_clear_all_tab_stops() {
1267        let mut p = Parser::new();
1268        assert_eq!(p.feed(b"\x1b[3g"), vec![Action::ClearTabStop(3)]);
1269    }
1270
1271    #[test]
1272    fn csi_z_is_back_tab() {
1273        let mut p = Parser::new();
1274        assert_eq!(p.feed(b"\x1b[Z"), vec![Action::BackTab(1)]);
1275        assert_eq!(p.feed(b"\x1b[3Z"), vec![Action::BackTab(3)]);
1276    }
1277
1278    // ── DECKPAM / DECKPNM (keypad modes) ─────────────────────────
1279
1280    #[test]
1281    fn esc_eq_is_application_keypad() {
1282        let mut p = Parser::new();
1283        assert_eq!(p.feed(b"\x1b="), vec![Action::ApplicationKeypad]);
1284    }
1285
1286    #[test]
1287    fn esc_gt_is_normal_keypad() {
1288        let mut p = Parser::new();
1289        assert_eq!(p.feed(b"\x1b>"), vec![Action::NormalKeypad]);
1290    }
1291
1292    // ── ECH (erase characters) ────────────────────────────────────
1293
1294    #[test]
1295    fn csi_x_is_erase_chars() {
1296        let mut p = Parser::new();
1297        assert_eq!(p.feed(b"\x1b[X"), vec![Action::EraseChars(1)]);
1298        assert_eq!(p.feed(b"\x1b[5X"), vec![Action::EraseChars(5)]);
1299    }
1300
1301    // ── Mixed new sequences integration ───────────────────────────
1302
1303    #[test]
1304    fn tab_stop_setup_and_clear_sequence() {
1305        let mut p = Parser::new();
1306        // Move to col 4, set tab, move to col 12, set tab, then clear all
1307        let actions = p.feed(b"\x1b[5G\x1bH\x1b[13G\x1bH\x1b[3g");
1308        assert_eq!(
1309            actions,
1310            vec![
1311                Action::CursorColumn(4),
1312                Action::SetTabStop,
1313                Action::CursorColumn(12),
1314                Action::SetTabStop,
1315                Action::ClearTabStop(3),
1316            ]
1317        );
1318    }
1319
1320    #[test]
1321    fn esc_hash_8_is_screen_alignment() {
1322        let mut p = Parser::new();
1323        assert_eq!(p.feed(b"\x1b#8"), vec![Action::ScreenAlignment]);
1324    }
1325
1326    #[test]
1327    fn esc_hash_unknown_is_escape() {
1328        let mut p = Parser::new();
1329        let actions = p.feed(b"\x1b#3");
1330        assert_eq!(actions.len(), 1);
1331        assert!(matches!(actions[0], Action::Escape(_)));
1332    }
1333
1334    #[test]
1335    fn csi_b_is_repeat_char() {
1336        let mut p = Parser::new();
1337        assert_eq!(p.feed(b"\x1b[5b"), vec![Action::RepeatChar(5)]);
1338    }
1339
1340    #[test]
1341    fn csi_b_default_is_one() {
1342        let mut p = Parser::new();
1343        assert_eq!(p.feed(b"\x1b[b"), vec![Action::RepeatChar(1)]);
1344    }
1345
1346    // ── DECSCUSR (cursor shape) ──────────────────────────────────
1347
1348    #[test]
1349    fn csi_sp_q_is_set_cursor_shape() {
1350        let mut p = Parser::new();
1351        // CSI 2 SP q = steady block
1352        assert_eq!(p.feed(b"\x1b[2 q"), vec![Action::SetCursorShape(2)]);
1353    }
1354
1355    #[test]
1356    fn csi_sp_q_all_shapes() {
1357        let mut p = Parser::new();
1358        // 0=default, 1=blinking block, 2=steady block, 3=blinking underline,
1359        // 4=steady underline, 5=blinking bar, 6=steady bar
1360        for shape in 0u8..=6 {
1361            let seq = format!("\x1b[{shape} q");
1362            let actions = p.feed(seq.as_bytes());
1363            assert_eq!(
1364                actions,
1365                vec![Action::SetCursorShape(shape)],
1366                "DECSCUSR shape {shape}"
1367            );
1368        }
1369    }
1370
1371    #[test]
1372    fn csi_sp_q_default_is_zero() {
1373        let mut p = Parser::new();
1374        // CSI SP q (no parameter) = default cursor shape (0)
1375        assert_eq!(p.feed(b"\x1b[ q"), vec![Action::SetCursorShape(0)]);
1376    }
1377
1378    #[test]
1379    fn csi_sp_q_clamps_to_six() {
1380        let mut p = Parser::new();
1381        // Values above 6 are clamped
1382        assert_eq!(p.feed(b"\x1b[99 q"), vec![Action::SetCursorShape(6)]);
1383    }
1384
1385    // ── DECSTR (soft reset) ──────────────────────────────────────
1386
1387    #[test]
1388    fn csi_bang_p_is_soft_reset() {
1389        let mut p = Parser::new();
1390        assert_eq!(p.feed(b"\x1b[!p"), vec![Action::SoftReset]);
1391    }
1392
1393    #[test]
1394    fn vim_cursor_shape_sequence() {
1395        let mut p = Parser::new();
1396        // Typical vim: set steady bar on insert, steady block on normal
1397        let actions = p.feed(b"\x1b[6 q\x1b[2 q");
1398        assert_eq!(
1399            actions,
1400            vec![
1401                Action::SetCursorShape(6), // steady bar (insert mode)
1402                Action::SetCursorShape(2), // steady block (normal mode)
1403            ]
1404        );
1405    }
1406
1407    #[test]
1408    fn soft_reset_in_typical_sequence() {
1409        let mut p = Parser::new();
1410        // Soft reset, then set up modes
1411        let actions = p.feed(b"\x1b[!p\x1b[?7h\x1b[?25h");
1412        assert_eq!(
1413            actions,
1414            vec![
1415                Action::SoftReset,
1416                Action::DecSet(smallvec![7]),  // enable autowrap
1417                Action::DecSet(smallvec![25]), // show cursor
1418            ]
1419        );
1420    }
1421
1422    // ── ED mode 3 (erase scrollback) ─────────────────────────────
1423
1424    #[test]
1425    fn csi_3_j_is_erase_scrollback() {
1426        let mut p = Parser::new();
1427        assert_eq!(p.feed(b"\x1b[3J"), vec![Action::EraseScrollback]);
1428    }
1429
1430    #[test]
1431    fn csi_j_mode_0_1_2_still_work() {
1432        let mut p = Parser::new();
1433        assert_eq!(p.feed(b"\x1b[J"), vec![Action::EraseInDisplay(0)]);
1434        assert_eq!(p.feed(b"\x1b[1J"), vec![Action::EraseInDisplay(1)]);
1435        assert_eq!(p.feed(b"\x1b[2J"), vec![Action::EraseInDisplay(2)]);
1436    }
1437
1438    // ── SCOSC / SCORC (CSI s / CSI u) ───────────────────────────
1439
1440    #[test]
1441    fn csi_s_is_save_cursor() {
1442        let mut p = Parser::new();
1443        assert_eq!(p.feed(b"\x1b[s"), vec![Action::SaveCursor]);
1444    }
1445
1446    #[test]
1447    fn csi_u_is_restore_cursor() {
1448        let mut p = Parser::new();
1449        assert_eq!(p.feed(b"\x1b[u"), vec![Action::RestoreCursor]);
1450    }
1451
1452    #[test]
1453    fn csi_s_with_params_is_none() {
1454        let mut p = Parser::new();
1455        // CSI 1;2 s could be DECSLRM — should not emit SaveCursor.
1456        // Unrecognised CSI sequences fall through as Action::Escape(raw).
1457        let result = p.feed(b"\x1b[1;2s");
1458        assert_eq!(result, vec![Action::Escape(b"\x1b[1;2s".to_vec())]);
1459    }
1460
1461    // ── Focus events (CSI I / CSI O) ─────────────────────────────
1462
1463    #[test]
1464    fn csi_i_is_focus_in() {
1465        let mut p = Parser::new();
1466        assert_eq!(p.feed(b"\x1b[I"), vec![Action::FocusIn]);
1467    }
1468
1469    #[test]
1470    fn csi_o_is_focus_out() {
1471        let mut p = Parser::new();
1472        assert_eq!(p.feed(b"\x1b[O"), vec![Action::FocusOut]);
1473    }
1474
1475    // ── Bracketed paste (CSI 200~/201~) ──────────────────────────
1476
1477    #[test]
1478    fn csi_200_tilde_is_paste_start() {
1479        let mut p = Parser::new();
1480        assert_eq!(p.feed(b"\x1b[200~"), vec![Action::PasteStart]);
1481    }
1482
1483    #[test]
1484    fn csi_201_tilde_is_paste_end() {
1485        let mut p = Parser::new();
1486        assert_eq!(p.feed(b"\x1b[201~"), vec![Action::PasteEnd]);
1487    }
1488
1489    #[test]
1490    fn csi_unknown_tilde_is_none() {
1491        let mut p = Parser::new();
1492        // CSI 99 ~ is an unknown function-key — not dispatched, falls through
1493        // as Action::Escape(raw).
1494        let result = p.feed(b"\x1b[99~");
1495        assert_eq!(result, vec![Action::Escape(b"\x1b[99~".to_vec())]);
1496    }
1497
1498    // ── DA1: primary device attributes (CSI c) ──────────────────
1499
1500    #[test]
1501    fn csi_c_is_device_attributes() {
1502        let mut p = Parser::new();
1503        assert_eq!(p.feed(b"\x1b[c"), vec![Action::DeviceAttributes]);
1504    }
1505
1506    #[test]
1507    fn csi_0_c_is_device_attributes() {
1508        let mut p = Parser::new();
1509        assert_eq!(p.feed(b"\x1b[0c"), vec![Action::DeviceAttributes]);
1510    }
1511
1512    #[test]
1513    fn csi_1_c_is_not_device_attributes() {
1514        let mut p = Parser::new();
1515        // CSI 1 c has a non-zero param — not DA1.
1516        let actions = p.feed(b"\x1b[1c");
1517        assert!(!actions.contains(&Action::DeviceAttributes));
1518    }
1519
1520    // ── DA2: secondary device attributes (CSI > c) ──────────────
1521
1522    #[test]
1523    fn csi_gt_c_is_da2() {
1524        let mut p = Parser::new();
1525        assert_eq!(p.feed(b"\x1b[>c"), vec![Action::DeviceAttributesSecondary]);
1526    }
1527
1528    #[test]
1529    fn csi_gt_0_c_is_da2() {
1530        let mut p = Parser::new();
1531        assert_eq!(p.feed(b"\x1b[>0c"), vec![Action::DeviceAttributesSecondary]);
1532    }
1533
1534    // ── DSR / CPR (CSI n) ────────────────────────────────────────
1535
1536    #[test]
1537    fn csi_5n_is_device_status_report() {
1538        let mut p = Parser::new();
1539        assert_eq!(p.feed(b"\x1b[5n"), vec![Action::DeviceStatusReport]);
1540    }
1541
1542    #[test]
1543    fn csi_6n_is_cursor_position_report() {
1544        let mut p = Parser::new();
1545        assert_eq!(p.feed(b"\x1b[6n"), vec![Action::CursorPositionReport]);
1546    }
1547
1548    #[test]
1549    fn csi_0n_is_not_dsr() {
1550        let mut p = Parser::new();
1551        let actions = p.feed(b"\x1b[0n");
1552        assert!(!actions.contains(&Action::DeviceStatusReport));
1553        assert!(!actions.contains(&Action::CursorPositionReport));
1554    }
1555
1556    // ── Character set designation (ESC ( / ESC )) ────────────────
1557
1558    #[test]
1559    fn esc_paren_b_is_g0_ascii() {
1560        let mut p = Parser::new();
1561        assert_eq!(
1562            p.feed(b"\x1b(B"),
1563            vec![Action::DesignateCharset {
1564                slot: 0,
1565                charset: b'B'
1566            }]
1567        );
1568    }
1569
1570    #[test]
1571    fn esc_paren_0_is_g0_dec_graphics() {
1572        let mut p = Parser::new();
1573        assert_eq!(
1574            p.feed(b"\x1b(0"),
1575            vec![Action::DesignateCharset {
1576                slot: 0,
1577                charset: b'0'
1578            }]
1579        );
1580    }
1581
1582    #[test]
1583    fn esc_rparen_b_is_g1_ascii() {
1584        let mut p = Parser::new();
1585        assert_eq!(
1586            p.feed(b"\x1b)B"),
1587            vec![Action::DesignateCharset {
1588                slot: 1,
1589                charset: b'B'
1590            }]
1591        );
1592    }
1593
1594    #[test]
1595    fn esc_star_0_is_g2_dec_graphics() {
1596        let mut p = Parser::new();
1597        assert_eq!(
1598            p.feed(b"\x1b*0"),
1599            vec![Action::DesignateCharset {
1600                slot: 2,
1601                charset: b'0'
1602            }]
1603        );
1604    }
1605
1606    #[test]
1607    fn esc_plus_b_is_g3_ascii() {
1608        let mut p = Parser::new();
1609        assert_eq!(
1610            p.feed(b"\x1b+B"),
1611            vec![Action::DesignateCharset {
1612                slot: 3,
1613                charset: b'B'
1614            }]
1615        );
1616    }
1617
1618    // ── Single Shift (ESC N / ESC O) ─────────────────────────────
1619
1620    #[test]
1621    fn esc_n_is_single_shift_2() {
1622        let mut p = Parser::new();
1623        assert_eq!(p.feed(b"\x1bN"), vec![Action::SingleShift2]);
1624    }
1625
1626    #[test]
1627    fn esc_o_is_single_shift_3() {
1628        let mut p = Parser::new();
1629        assert_eq!(p.feed(b"\x1bO"), vec![Action::SingleShift3]);
1630    }
1631
1632    // ── SGR Mouse (CSI < Pb ; Px ; Py M/m) ──────────────────────
1633
1634    #[test]
1635    fn sgr_mouse_left_press() {
1636        let mut p = Parser::new();
1637        // CSI < 0 ; 10 ; 5 M → left button press at col 9, row 4 (0-based)
1638        assert_eq!(
1639            p.feed(b"\x1b[<0;10;5M"),
1640            vec![Action::MouseEvent {
1641                button: 0,
1642                col: 9,
1643                row: 4,
1644                pressed: true,
1645            }]
1646        );
1647    }
1648
1649    #[test]
1650    fn sgr_mouse_left_release() {
1651        let mut p = Parser::new();
1652        // CSI < 0 ; 10 ; 5 m → left button release at col 9, row 4
1653        assert_eq!(
1654            p.feed(b"\x1b[<0;10;5m"),
1655            vec![Action::MouseEvent {
1656                button: 0,
1657                col: 9,
1658                row: 4,
1659                pressed: false,
1660            }]
1661        );
1662    }
1663
1664    #[test]
1665    fn sgr_mouse_right_press() {
1666        let mut p = Parser::new();
1667        // CSI < 2 ; 1 ; 1 M → right button press at col 0, row 0
1668        assert_eq!(
1669            p.feed(b"\x1b[<2;1;1M"),
1670            vec![Action::MouseEvent {
1671                button: 2,
1672                col: 0,
1673                row: 0,
1674                pressed: true,
1675            }]
1676        );
1677    }
1678
1679    #[test]
1680    fn sgr_mouse_middle_press() {
1681        let mut p = Parser::new();
1682        assert_eq!(
1683            p.feed(b"\x1b[<1;50;25M"),
1684            vec![Action::MouseEvent {
1685                button: 1,
1686                col: 49,
1687                row: 24,
1688                pressed: true,
1689            }]
1690        );
1691    }
1692
1693    #[test]
1694    fn sgr_mouse_scroll_up() {
1695        let mut p = Parser::new();
1696        // button 64 = scroll up
1697        assert_eq!(
1698            p.feed(b"\x1b[<64;5;3M"),
1699            vec![Action::MouseEvent {
1700                button: 64,
1701                col: 4,
1702                row: 2,
1703                pressed: true,
1704            }]
1705        );
1706    }
1707
1708    #[test]
1709    fn sgr_mouse_scroll_down() {
1710        let mut p = Parser::new();
1711        // button 65 = scroll down
1712        assert_eq!(
1713            p.feed(b"\x1b[<65;5;3M"),
1714            vec![Action::MouseEvent {
1715                button: 65,
1716                col: 4,
1717                row: 2,
1718                pressed: true,
1719            }]
1720        );
1721    }
1722
1723    #[test]
1724    fn sgr_mouse_ctrl_left_press() {
1725        let mut p = Parser::new();
1726        // button 16 = ctrl modifier + left button (0 + 16)
1727        assert_eq!(
1728            p.feed(b"\x1b[<16;1;1M"),
1729            vec![Action::MouseEvent {
1730                button: 16,
1731                col: 0,
1732                row: 0,
1733                pressed: true,
1734            }]
1735        );
1736    }
1737
1738    #[test]
1739    fn sgr_mouse_motion_while_pressed() {
1740        let mut p = Parser::new();
1741        // button 32 = motion with left button held (0 + 32)
1742        assert_eq!(
1743            p.feed(b"\x1b[<32;20;10M"),
1744            vec![Action::MouseEvent {
1745                button: 32,
1746                col: 19,
1747                row: 9,
1748                pressed: true,
1749            }]
1750        );
1751    }
1752
1753    #[test]
1754    fn sgr_mouse_incomplete_params_falls_through() {
1755        let mut p = Parser::new();
1756        // Only 2 params instead of 3
1757        let actions = p.feed(b"\x1b[<0;10M");
1758        assert!(
1759            !matches!(actions.first(), Some(Action::MouseEvent { .. })),
1760            "incomplete SGR mouse should not produce MouseEvent"
1761        );
1762    }
1763
1764    #[test]
1765    fn sgr_mouse_large_coords() {
1766        let mut p = Parser::new();
1767        // Large coordinates (wide terminal)
1768        assert_eq!(
1769            p.feed(b"\x1b[<0;300;100M"),
1770            vec![Action::MouseEvent {
1771                button: 0,
1772                col: 299,
1773                row: 99,
1774                pressed: true,
1775            }]
1776        );
1777    }
1778
1779    // ── feed_into equivalence ──────────────────────────────────────
1780
1781    #[test]
1782    fn feed_into_is_equivalent_to_feed() {
1783        let inputs: &[&[u8]] = &[
1784            b"Hello, World!",
1785            b"\x1b[31;1mBold Red\x1b[m",
1786            b"\x1b[?1049h\x1b[?2004h",
1787            b"\x1b[10;20H",
1788            b"\x1b]0;title\x07",
1789            b"\xc3\xa9\xc3\xa0",
1790        ];
1791        for &input in inputs {
1792            let mut p1 = Parser::new();
1793            let mut p2 = Parser::new();
1794            let expected = p1.feed(input);
1795            let mut actual = Vec::new();
1796            p2.feed_into(input, &mut actual);
1797            assert_eq!(expected, actual, "mismatch for input {:?}", input);
1798        }
1799    }
1800
1801    #[test]
1802    fn feed_into_reuses_capacity() {
1803        let mut p = Parser::new();
1804        let mut out = Vec::new();
1805        p.feed_into(b"\x1b[31m", &mut out);
1806        assert!(!out.is_empty());
1807        let cap = out.capacity();
1808        out.clear();
1809        p.feed_into(b"\x1b[32m", &mut out);
1810        // Capacity should be retained from the previous call.
1811        assert!(out.capacity() >= cap);
1812    }
1813
1814    #[test]
1815    fn take_buf_caps_retained_internal_capacity() {
1816        let mut p = Parser::new();
1817        p.buf = Vec::with_capacity(ESC_BUF_RETAIN_CAP_MAX * 4);
1818        p.buf.extend_from_slice(b"\x1b[31m");
1819
1820        let taken = p.take_buf();
1821        assert!(!taken.is_empty());
1822        assert!(
1823            p.buf.capacity() <= ESC_BUF_RETAIN_CAP_MAX,
1824            "retained capacity must be capped to avoid oversized churn"
1825        );
1826    }
1827}