Skip to main content

rmux_core/input/
mod.rs

1//! tmux-compatible VT parser state machine.
2//!
3//! It implements DEC-style terminal parsing for tmux-compatible streams.
4//! This module provides the parser, state tables, command enums, parameter
5//! splitting, and SGR logic as pure safe Rust. Screen-write effects are
6//! delegated through the [`crate::input::ScreenWriter`] trait.
7
8mod cell;
9mod colour;
10mod commands;
11mod csi_helpers;
12mod dispatch;
13pub mod mode;
14mod params;
15mod passthrough;
16mod sgr;
17mod states;
18mod tables;
19#[cfg(test)]
20mod tests;
21mod writer;
22
23pub use cell::{CellState, GridAttr, SavedState};
24pub use colour::{
25    colour_join_rgb, Colour, COLOUR_DEFAULT, COLOUR_FLAG_256, COLOUR_FLAG_RGB, COLOUR_NONE,
26    COLOUR_TERMINAL,
27};
28pub use dispatch::{CsiCommand, DcsPayload, EscCommand, InputAction, OscCommand, ScreenWriter};
29pub use params::{InputParam, ParamType};
30pub use states::InputState;
31
32use params::ParamList;
33use states::Transition;
34
35use crate::terminal_passthrough::MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES;
36
37/// Maximum number of parameters in a CSI/DCS sequence.
38const PARAM_LIST_MAX: usize = 24;
39
40/// Intermediate buffer capacity.
41const INTERM_BUF_MAX: usize = 4;
42
43/// Initial input buffer size.
44const INPUT_BUF_START: usize = 32;
45
46/// Maximum input buffer size (1 MiB, matching `INPUT_BUF_DEFAULT_SIZE`).
47const INPUT_BUF_MAX: usize = 1_048_576;
48
49/// Parameter buffer capacity for raw parameter bytes.
50const PARAM_BUF_MAX: usize = 64;
51
52/// Parser flags.
53const INPUT_DISCARD: u32 = 0x1;
54/// Last printable character was emitted (for REP).
55const INPUT_LAST: u32 = 0x2;
56
57/// Type of string terminator seen for OSC/DCS.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum InputEndType {
60    /// ESC \\ (ST)
61    St,
62    /// BEL (0x07)
63    Bel,
64}
65
66/// Per-pane VT input parser, matching tmux `input_ctx`.
67pub struct InputParser {
68    /// Current parser state.
69    state: InputState,
70    /// Parser flags (INPUT_DISCARD, INPUT_LAST).
71    flags: u32,
72
73    /// Current character being processed.
74    ch: u8,
75
76    /// Intermediate character buffer.
77    interm_buf: [u8; INTERM_BUF_MAX],
78    interm_len: usize,
79
80    /// Raw parameter buffer.
81    param_buf: [u8; PARAM_BUF_MAX],
82    param_len: usize,
83
84    /// Dynamic input/string buffer.
85    input_buf: Vec<u8>,
86    input_buf_max: usize,
87    /// Which terminator ended the string.
88    input_end: InputEndType,
89
90    /// Parsed parameter list.
91    param_list: ParamList,
92
93    /// Cell state (current attributes, character set, etc.).
94    cell: CellState,
95    /// Saved cell state for DECSC/DECRC.
96    saved: SavedState,
97
98    /// UTF-8 accumulator.
99    utf8_buf: [u8; 4],
100    utf8_len: u8,
101    utf8_expected: u8,
102    utf8_started: bool,
103
104    /// Last printed character data for REP.
105    last_char: Option<char>,
106
107    /// Bytes accumulated since last ground state, for control-mode catch-up.
108    since_ground: Vec<u8>,
109
110    /// Whether ground timer would be active (modeled as flag; actual timer
111    /// is a server-side concern).
112    ground_timer_active: bool,
113
114    /// Reply buffer: replies to be sent back to the PTY.
115    reply_buf: Vec<u8>,
116    /// Dropped terminal passthrough events caused by parser string limits.
117    terminal_passthrough_dropped_count: u64,
118}
119
120impl InputParser {
121    /// Creates a new parser in the ground state with default cell attributes.
122    #[must_use]
123    pub fn new() -> Self {
124        Self {
125            state: InputState::Ground,
126            flags: 0,
127            ch: 0,
128            interm_buf: [0; INTERM_BUF_MAX],
129            interm_len: 0,
130            param_buf: [0; PARAM_BUF_MAX],
131            param_len: 0,
132            input_buf: Vec::with_capacity(INPUT_BUF_START),
133            input_buf_max: INPUT_BUF_MAX,
134            input_end: InputEndType::St,
135            param_list: ParamList::new(),
136            cell: CellState::default(),
137            saved: SavedState::default(),
138            utf8_buf: [0; 4],
139            utf8_len: 0,
140            utf8_expected: 0,
141            utf8_started: false,
142            last_char: None,
143            since_ground: Vec::new(),
144            ground_timer_active: false,
145            reply_buf: Vec::new(),
146            terminal_passthrough_dropped_count: 0,
147        }
148    }
149
150    /// Updates the maximum regular string buffer size used for OSC/DCS input.
151    pub fn set_input_buffer_limit(&mut self, limit: usize) {
152        self.input_buf_max = limit.max(INPUT_BUF_START);
153    }
154
155    pub(crate) const fn configured_input_buffer_limit(&self) -> usize {
156        self.input_buf_max
157    }
158
159    /// Returns the current parser state.
160    #[must_use]
161    pub fn state(&self) -> InputState {
162        self.state
163    }
164
165    /// Returns and drains accumulated reply bytes.
166    pub fn take_replies(&mut self) -> Vec<u8> {
167        std::mem::take(&mut self.reply_buf)
168    }
169
170    /// Returns and drains terminal passthrough drops caused by parser limits.
171    pub(crate) fn take_terminal_passthrough_dropped_count(&mut self) -> u64 {
172        let dropped = self.terminal_passthrough_dropped_count;
173        self.terminal_passthrough_dropped_count = 0;
174        dropped
175    }
176
177    /// Returns and drains accumulated since-ground bytes.
178    pub fn take_since_ground(&mut self) -> Vec<u8> {
179        std::mem::take(&mut self.since_ground)
180    }
181
182    /// Returns any bytes still buffered in an incomplete parser state.
183    #[must_use]
184    pub fn pending_bytes(&self) -> Vec<u8> {
185        if self.state != InputState::Ground {
186            return self.since_ground.clone();
187        }
188        if self.utf8_started {
189            return self.utf8_buf[..usize::from(self.utf8_len)].to_vec();
190        }
191        Vec::new()
192    }
193
194    /// Returns true if the ground timer should be running.
195    #[must_use]
196    pub fn ground_timer_active(&self) -> bool {
197        self.ground_timer_active
198    }
199
200    /// Called by the server when the ground timer expires (5s timeout).
201    pub fn ground_timer_expired(&mut self) {
202        self.reset_to_ground();
203    }
204
205    /// Resets the parser to ground state.
206    pub fn reset_to_ground(&mut self) {
207        self.clear();
208        self.state = InputState::Ground;
209        self.flags = 0;
210        self.enter_ground();
211    }
212
213    /// Returns a reference to the current cell state.
214    #[must_use]
215    pub fn cell_state(&self) -> &CellState {
216        &self.cell
217    }
218
219    /// Parse a buffer of bytes, dispatching actions to the screen writer.
220    pub fn parse<W: ScreenWriter + ?Sized>(&mut self, buf: &[u8], writer: &mut W) {
221        let mut index = 0;
222        while index < buf.len() {
223            if self.state == InputState::Ground && !self.utf8_started {
224                let printable_end = buf[index..]
225                    .iter()
226                    .position(|byte| !byte.is_ascii_graphic() && *byte != b' ')
227                    .map_or(buf.len(), |offset| index + offset);
228                if printable_end > index {
229                    self.handle_printable_ascii_run(&buf[index..printable_end], writer);
230                    index = printable_end;
231                    continue;
232                }
233                if self.handle_ground_c0_fast_path(buf[index], writer) {
234                    index += 1;
235                    continue;
236                }
237            }
238
239            self.ch = buf[index];
240            let transition = self.find_transition();
241            self.execute_transition(transition, writer);
242            index += 1;
243        }
244    }
245
246    fn handle_printable_ascii_run<W: ScreenWriter + ?Sized>(
247        &mut self,
248        bytes: &[u8],
249        writer: &mut W,
250    ) {
251        debug_assert_eq!(self.state, InputState::Ground);
252        let set = if self.cell.set == 0 {
253            self.cell.g0set
254        } else {
255            self.cell.g1set
256        };
257        let acs = set != 0;
258        writer.collect_add_ascii_run(bytes, &self.cell, acs);
259        if let Some(&last) = bytes.last() {
260            self.last_char = Some(char::from(last));
261        }
262        self.flags |= INPUT_LAST;
263    }
264
265    fn handle_ground_c0_fast_path<W: ScreenWriter + ?Sized>(
266        &mut self,
267        byte: u8,
268        writer: &mut W,
269    ) -> bool {
270        match byte {
271            0x0a..=0x0c => {
272                writer.collect_end();
273                writer.linefeed(false, self.cell.bg());
274                if writer.current_mode() & mode::MODE_CRLF != 0 {
275                    writer.carriage_return();
276                }
277            }
278            0x0d => {
279                writer.collect_end();
280                writer.carriage_return();
281            }
282            _ => return false,
283        }
284        self.flags &= !INPUT_LAST;
285        true
286    }
287
288    fn find_transition(&self) -> Transition {
289        self.state.transition_for_byte(self.ch)
290    }
291
292    fn execute_transition<W: ScreenWriter + ?Sized>(&mut self, trans: Transition, writer: &mut W) {
293        // Any state except print stops collect_end equivalent.
294        if !matches!(
295            trans.handler,
296            states::Handler::Print | states::Handler::TopBitSet
297        ) {
298            writer.collect_end();
299        }
300
301        // Execute handler; if it returns true, skip state transition.
302        let skip_state = match trans.handler {
303            states::Handler::None => false,
304            states::Handler::Print => self.handle_print(writer),
305            states::Handler::C0Dispatch => self.handle_c0_dispatch(writer),
306            states::Handler::EscDispatch => self.handle_esc_dispatch(writer),
307            states::Handler::CsiDispatch => self.handle_csi_dispatch(writer),
308            states::Handler::DcsDispatch => self.handle_dcs_dispatch(writer),
309            states::Handler::Intermediate => self.handle_intermediate(),
310            states::Handler::Parameter => self.handle_parameter(),
311            states::Handler::Input => self.handle_input(),
312            states::Handler::TopBitSet => self.handle_top_bit_set(writer),
313            states::Handler::EndBel => self.handle_end_bel(),
314        };
315
316        if skip_state {
317            return;
318        }
319
320        if let Some(next) = trans.next_state {
321            self.set_state(next, writer);
322        }
323
324        // If not in ground state, save byte to since_ground.
325        if self.state != InputState::Ground && self.since_ground.len() < self.input_buf_max {
326            self.since_ground.push(self.ch);
327        }
328    }
329
330    fn set_state<W: ScreenWriter + ?Sized>(&mut self, next: InputState, writer: &mut W) {
331        // Call exit handler for current state.
332        self.exit_state(writer);
333        self.state = next;
334        // Call enter handler for new state.
335        self.enter_state(writer);
336    }
337
338    fn enter_state<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
339        match self.state {
340            InputState::Ground => self.enter_ground(),
341            InputState::EscEnter => self.clear(),
342            InputState::CsiEnter => self.clear(),
343            InputState::DcsEnter => self.enter_dcs(),
344            InputState::OscString => self.enter_osc(),
345            InputState::ApcString => self.enter_apc(),
346            InputState::RenameString => self.enter_rename(),
347            InputState::ConsumeSt => self.enter_rename(), // same as rename in tmux
348            _ => {}
349        }
350        let _ = writer; // writer not needed for enter handlers currently
351    }
352
353    fn exit_state<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
354        match self.state {
355            InputState::OscString => self.exit_osc(writer),
356            InputState::ApcString => self.exit_apc(writer),
357            InputState::RenameString => self.exit_rename(writer),
358            _ => {}
359        }
360    }
361
362    fn clear(&mut self) {
363        self.ground_timer_active = false;
364        self.interm_buf = [0; INTERM_BUF_MAX];
365        self.interm_len = 0;
366        self.param_buf = [0; PARAM_BUF_MAX];
367        self.param_len = 0;
368        self.input_buf.clear();
369        self.input_end = InputEndType::St;
370        self.flags &= !INPUT_DISCARD;
371    }
372
373    fn enter_ground(&mut self) {
374        self.ground_timer_active = false;
375        self.since_ground.clear();
376        // Shrink input buffer back to start size.
377        if self.input_buf.capacity() > INPUT_BUF_START {
378            self.input_buf = Vec::with_capacity(INPUT_BUF_START);
379        }
380    }
381
382    fn enter_dcs(&mut self) {
383        self.clear();
384        self.ground_timer_active = true;
385        self.flags &= !INPUT_LAST;
386    }
387
388    fn enter_osc(&mut self) {
389        self.clear();
390        self.ground_timer_active = true;
391        self.flags &= !INPUT_LAST;
392    }
393
394    fn enter_apc(&mut self) {
395        self.clear();
396        self.ground_timer_active = true;
397        self.flags &= !INPUT_LAST;
398    }
399
400    fn enter_rename(&mut self) {
401        self.clear();
402        self.ground_timer_active = true;
403        self.flags &= !INPUT_LAST;
404    }
405
406    fn exit_osc<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
407        if self.flags & INPUT_DISCARD != 0 {
408            return;
409        }
410        dispatch::dispatch_osc(self, writer);
411    }
412
413    fn exit_apc<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
414        if self.flags & INPUT_DISCARD != 0 {
415            return;
416        }
417        if passthrough::is_kitty_graphics_apc(&self.input_buf) {
418            writer.apc_passthrough(&self.input_buf);
419            return;
420        }
421        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
422        writer.set_title(&buf);
423    }
424
425    fn exit_rename<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
426        if self.flags & INPUT_DISCARD != 0 {
427            return;
428        }
429        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
430        writer.set_window_name(&buf);
431    }
432
433    /// Stop any in-progress UTF-8 sequence and emit U+FFFD.
434    fn stop_utf8<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
435        if self.utf8_started {
436            writer.collect_add('\u{FFFD}', &self.cell);
437            self.utf8_started = false;
438            self.utf8_len = 0;
439            self.utf8_expected = 0;
440        }
441    }
442
443    fn handle_print<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
444        self.stop_utf8(writer);
445
446        let ch = self.ch as char;
447        let set = if self.cell.set == 0 {
448            self.cell.g0set
449        } else {
450            self.cell.g1set
451        };
452
453        writer.collect_add_with_charset(ch, &self.cell, set != 0);
454
455        self.last_char = Some(ch);
456        self.flags |= INPUT_LAST;
457
458        false
459    }
460
461    fn handle_intermediate(&mut self) -> bool {
462        if self.interm_len >= INTERM_BUF_MAX - 1 {
463            self.flags |= INPUT_DISCARD;
464        } else {
465            self.interm_buf[self.interm_len] = self.ch;
466            self.interm_len += 1;
467        }
468        false
469    }
470
471    fn handle_parameter(&mut self) -> bool {
472        if self.param_len >= PARAM_BUF_MAX - 1 {
473            self.flags |= INPUT_DISCARD;
474        } else {
475            self.param_buf[self.param_len] = self.ch;
476            self.param_len += 1;
477        }
478        false
479    }
480
481    fn handle_input(&mut self) -> bool {
482        let escaped_dcs_byte = self.state == InputState::DcsEscape;
483        let bytes_to_push = if escaped_dcs_byte && self.ch != 0x1b {
484            2
485        } else {
486            1
487        };
488        let input_limit = self.input_buffer_limit();
489        if self.input_buf.len() + bytes_to_push >= input_limit {
490            if self.flags & INPUT_DISCARD == 0 && self.is_terminal_passthrough_string() {
491                self.terminal_passthrough_dropped_count =
492                    self.terminal_passthrough_dropped_count.saturating_add(1);
493            }
494            self.flags |= INPUT_DISCARD;
495        } else if escaped_dcs_byte && self.ch == 0x1b {
496            self.input_buf.push(0x1b);
497        } else if escaped_dcs_byte {
498            self.input_buf.push(0x1b);
499            self.input_buf.push(self.ch);
500        } else {
501            self.input_buf.push(self.ch);
502        }
503        false
504    }
505
506    fn input_buffer_limit(&self) -> usize {
507        if self.is_terminal_passthrough_string() {
508            return MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES;
509        }
510        self.input_buf_max
511    }
512
513    fn is_terminal_passthrough_string(&self) -> bool {
514        (self.state == InputState::ApcString && passthrough::is_kitty_graphics_apc(&self.input_buf))
515            || (matches!(self.state, InputState::DcsHandler | InputState::DcsEscape)
516                && self.interm_len == 0
517                && (self.input_buf.first() == Some(&b'q') || self.input_buf.starts_with(b"tmux;")))
518    }
519
520    fn handle_end_bel(&mut self) -> bool {
521        self.input_end = InputEndType::Bel;
522        false
523    }
524
525    fn handle_c0_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
526        self.stop_utf8(writer);
527        dispatch::dispatch_c0(self, writer);
528        self.flags &= !INPUT_LAST;
529        false
530    }
531
532    fn handle_esc_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
533        if self.flags & INPUT_DISCARD != 0 {
534            return false;
535        }
536        dispatch::dispatch_esc(self, writer);
537        self.flags &= !INPUT_LAST;
538        false
539    }
540
541    fn handle_csi_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
542        if self.flags & INPUT_DISCARD != 0 {
543            return false;
544        }
545        dispatch::dispatch_csi(self, writer);
546        self.flags &= !INPUT_LAST;
547        false
548    }
549
550    fn handle_dcs_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
551        if self.flags & INPUT_DISCARD != 0 {
552            return false;
553        }
554        dispatch::dispatch_dcs(self, writer);
555        false
556    }
557
558    fn handle_top_bit_set<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
559        self.flags &= !INPUT_LAST;
560
561        if !self.utf8_started {
562            self.utf8_started = true;
563            self.utf8_len = 0;
564            // Determine expected byte count from first byte.
565            let expected = if self.ch & 0xE0 == 0xC0 {
566                2
567            } else if self.ch & 0xF0 == 0xE0 {
568                3
569            } else if self.ch & 0xF8 == 0xF0 {
570                4
571            } else {
572                // Invalid start byte.
573                self.stop_utf8(writer);
574                return false;
575            };
576            self.utf8_expected = expected;
577            self.utf8_buf[0] = self.ch;
578            self.utf8_len = 1;
579            return false;
580        }
581
582        // Continuation byte.
583        if self.ch & 0xC0 != 0x80 {
584            // Not a valid continuation: emit replacement and re-process.
585            self.stop_utf8(writer);
586            // Re-start UTF-8 with current byte if it's a start byte.
587            if self.ch >= 0x80 {
588                return self.handle_top_bit_set(writer);
589            }
590            return false;
591        }
592
593        self.utf8_buf[self.utf8_len as usize] = self.ch;
594        self.utf8_len += 1;
595
596        if self.utf8_len < self.utf8_expected {
597            return false; // More bytes expected.
598        }
599
600        // Complete: decode.
601        self.utf8_started = false;
602        let bytes = &self.utf8_buf[..self.utf8_len as usize];
603        let s = match std::str::from_utf8(bytes) {
604            Ok(s) => s,
605            Err(_) => {
606                writer.collect_add('\u{FFFD}', &self.cell);
607                return false;
608            }
609        };
610        let c = match s.chars().next() {
611            Some(c) => c,
612            None => {
613                writer.collect_add('\u{FFFD}', &self.cell);
614                return false;
615            }
616        };
617
618        writer.collect_add(c, &self.cell);
619
620        self.last_char = Some(c);
621        self.flags |= INPUT_LAST;
622
623        false
624    }
625
626    /// Append a reply string to the reply buffer.
627    fn reply(&mut self, s: &str) {
628        self.reply_buf.extend_from_slice(s.as_bytes());
629    }
630
631    /// Interm buf as a string slice for table lookups.
632    fn interm_str(&self) -> &[u8] {
633        &self.interm_buf[..self.interm_len]
634    }
635}
636
637impl Default for InputParser {
638    fn default() -> Self {
639        Self::new()
640    }
641}