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(&mut self, buf: &[u8], writer: &mut dyn ScreenWriter) {
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(&mut self, bytes: &[u8], writer: &mut dyn ScreenWriter) {
247        debug_assert_eq!(self.state, InputState::Ground);
248        let set = if self.cell.set == 0 {
249            self.cell.g0set
250        } else {
251            self.cell.g1set
252        };
253        let acs = set != 0;
254        writer.collect_add_ascii_run(bytes, &self.cell, acs);
255        if let Some(&last) = bytes.last() {
256            self.last_char = Some(char::from(last));
257        }
258        self.flags |= INPUT_LAST;
259    }
260
261    fn handle_ground_c0_fast_path(&mut self, byte: u8, writer: &mut dyn ScreenWriter) -> bool {
262        match byte {
263            0x0a..=0x0c => {
264                writer.collect_end();
265                writer.linefeed(false, self.cell.bg());
266                if writer.current_mode() & mode::MODE_CRLF != 0 {
267                    writer.carriage_return();
268                }
269            }
270            0x0d => {
271                writer.collect_end();
272                writer.carriage_return();
273            }
274            _ => return false,
275        }
276        self.flags &= !INPUT_LAST;
277        true
278    }
279
280    fn find_transition(&self) -> Transition {
281        let table = self.state.transition_table();
282        for entry in table {
283            if self.ch >= entry.first && self.ch <= entry.last {
284                return Transition {
285                    handler: entry.handler,
286                    next_state: entry.next_state,
287                };
288            }
289        }
290        // Should never happen with complete tables, but be safe.
291        Transition {
292            handler: states::Handler::None,
293            next_state: None,
294        }
295    }
296
297    fn execute_transition(&mut self, trans: Transition, writer: &mut dyn ScreenWriter) {
298        // Any state except print stops collect_end equivalent.
299        if !matches!(
300            trans.handler,
301            states::Handler::Print | states::Handler::TopBitSet
302        ) {
303            writer.collect_end();
304        }
305
306        // Execute handler; if it returns true, skip state transition.
307        let skip_state = match trans.handler {
308            states::Handler::None => false,
309            states::Handler::Print => self.handle_print(writer),
310            states::Handler::C0Dispatch => self.handle_c0_dispatch(writer),
311            states::Handler::EscDispatch => self.handle_esc_dispatch(writer),
312            states::Handler::CsiDispatch => self.handle_csi_dispatch(writer),
313            states::Handler::DcsDispatch => self.handle_dcs_dispatch(writer),
314            states::Handler::Intermediate => self.handle_intermediate(),
315            states::Handler::Parameter => self.handle_parameter(),
316            states::Handler::Input => self.handle_input(),
317            states::Handler::TopBitSet => self.handle_top_bit_set(writer),
318            states::Handler::EndBel => self.handle_end_bel(),
319        };
320
321        if skip_state {
322            return;
323        }
324
325        if let Some(next) = trans.next_state {
326            self.set_state(next, writer);
327        }
328
329        // If not in ground state, save byte to since_ground.
330        if self.state != InputState::Ground {
331            self.since_ground.push(self.ch);
332        }
333    }
334
335    fn set_state(&mut self, next: InputState, writer: &mut dyn ScreenWriter) {
336        // Call exit handler for current state.
337        self.exit_state(writer);
338        self.state = next;
339        // Call enter handler for new state.
340        self.enter_state(writer);
341    }
342
343    fn enter_state(&mut self, writer: &mut dyn ScreenWriter) {
344        match self.state {
345            InputState::Ground => self.enter_ground(),
346            InputState::EscEnter => self.clear(),
347            InputState::CsiEnter => self.clear(),
348            InputState::DcsEnter => self.enter_dcs(),
349            InputState::OscString => self.enter_osc(),
350            InputState::ApcString => self.enter_apc(),
351            InputState::RenameString => self.enter_rename(),
352            InputState::ConsumeSt => self.enter_rename(), // same as rename in tmux
353            _ => {}
354        }
355        let _ = writer; // writer not needed for enter handlers currently
356    }
357
358    fn exit_state(&mut self, writer: &mut dyn ScreenWriter) {
359        match self.state {
360            InputState::OscString => self.exit_osc(writer),
361            InputState::ApcString => self.exit_apc(writer),
362            InputState::RenameString => self.exit_rename(writer),
363            _ => {}
364        }
365    }
366
367    fn clear(&mut self) {
368        self.ground_timer_active = false;
369        self.interm_buf = [0; INTERM_BUF_MAX];
370        self.interm_len = 0;
371        self.param_buf = [0; PARAM_BUF_MAX];
372        self.param_len = 0;
373        self.input_buf.clear();
374        self.input_end = InputEndType::St;
375        self.flags &= !INPUT_DISCARD;
376    }
377
378    fn enter_ground(&mut self) {
379        self.ground_timer_active = false;
380        self.since_ground.clear();
381        // Shrink input buffer back to start size.
382        if self.input_buf.capacity() > INPUT_BUF_START {
383            self.input_buf = Vec::with_capacity(INPUT_BUF_START);
384        }
385    }
386
387    fn enter_dcs(&mut self) {
388        self.clear();
389        self.ground_timer_active = true;
390        self.flags &= !INPUT_LAST;
391    }
392
393    fn enter_osc(&mut self) {
394        self.clear();
395        self.ground_timer_active = true;
396        self.flags &= !INPUT_LAST;
397    }
398
399    fn enter_apc(&mut self) {
400        self.clear();
401        self.ground_timer_active = true;
402        self.flags &= !INPUT_LAST;
403    }
404
405    fn enter_rename(&mut self) {
406        self.clear();
407        self.ground_timer_active = true;
408        self.flags &= !INPUT_LAST;
409    }
410
411    fn exit_osc(&mut self, writer: &mut dyn ScreenWriter) {
412        if self.flags & INPUT_DISCARD != 0 {
413            return;
414        }
415        dispatch::dispatch_osc(self, writer);
416    }
417
418    fn exit_apc(&mut self, writer: &mut dyn ScreenWriter) {
419        if self.flags & INPUT_DISCARD != 0 {
420            return;
421        }
422        if passthrough::is_kitty_graphics_apc(&self.input_buf) {
423            writer.apc_passthrough(&self.input_buf);
424            return;
425        }
426        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
427        writer.set_title(&buf);
428    }
429
430    fn exit_rename(&mut self, writer: &mut dyn ScreenWriter) {
431        if self.flags & INPUT_DISCARD != 0 {
432            return;
433        }
434        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
435        writer.set_window_name(&buf);
436    }
437
438    /// Stop any in-progress UTF-8 sequence and emit U+FFFD.
439    fn stop_utf8(&mut self, writer: &mut dyn ScreenWriter) {
440        if self.utf8_started {
441            writer.collect_add('\u{FFFD}', &self.cell);
442            self.utf8_started = false;
443            self.utf8_len = 0;
444            self.utf8_expected = 0;
445        }
446    }
447
448    fn handle_print(&mut self, writer: &mut dyn ScreenWriter) -> bool {
449        self.stop_utf8(writer);
450
451        let ch = self.ch as char;
452        let set = if self.cell.set == 0 {
453            self.cell.g0set
454        } else {
455            self.cell.g1set
456        };
457
458        writer.collect_add_with_charset(ch, &self.cell, set != 0);
459
460        self.last_char = Some(ch);
461        self.flags |= INPUT_LAST;
462
463        false
464    }
465
466    fn handle_intermediate(&mut self) -> bool {
467        if self.interm_len >= INTERM_BUF_MAX - 1 {
468            self.flags |= INPUT_DISCARD;
469        } else {
470            self.interm_buf[self.interm_len] = self.ch;
471            self.interm_len += 1;
472        }
473        false
474    }
475
476    fn handle_parameter(&mut self) -> bool {
477        if self.param_len >= PARAM_BUF_MAX - 1 {
478            self.flags |= INPUT_DISCARD;
479        } else {
480            self.param_buf[self.param_len] = self.ch;
481            self.param_len += 1;
482        }
483        false
484    }
485
486    fn handle_input(&mut self) -> bool {
487        let escaped_dcs_byte = self.state == InputState::DcsEscape;
488        let bytes_to_push = if escaped_dcs_byte && self.ch != 0x1b {
489            2
490        } else {
491            1
492        };
493        let input_limit = self.input_buffer_limit();
494        if self.input_buf.len() + bytes_to_push >= input_limit {
495            if self.flags & INPUT_DISCARD == 0 && self.is_terminal_passthrough_string() {
496                self.terminal_passthrough_dropped_count =
497                    self.terminal_passthrough_dropped_count.saturating_add(1);
498            }
499            self.flags |= INPUT_DISCARD;
500        } else if escaped_dcs_byte && self.ch == 0x1b {
501            self.input_buf.push(0x1b);
502        } else if escaped_dcs_byte {
503            self.input_buf.push(0x1b);
504            self.input_buf.push(self.ch);
505        } else {
506            self.input_buf.push(self.ch);
507        }
508        false
509    }
510
511    fn input_buffer_limit(&self) -> usize {
512        if self.is_terminal_passthrough_string() {
513            return MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES;
514        }
515        self.input_buf_max
516    }
517
518    fn is_terminal_passthrough_string(&self) -> bool {
519        (self.state == InputState::ApcString && passthrough::is_kitty_graphics_apc(&self.input_buf))
520            || (matches!(self.state, InputState::DcsHandler | InputState::DcsEscape)
521                && self.interm_len == 0
522                && (self.input_buf.first() == Some(&b'q') || self.input_buf.starts_with(b"tmux;")))
523    }
524
525    fn handle_end_bel(&mut self) -> bool {
526        self.input_end = InputEndType::Bel;
527        false
528    }
529
530    fn handle_c0_dispatch(&mut self, writer: &mut dyn ScreenWriter) -> bool {
531        self.stop_utf8(writer);
532        dispatch::dispatch_c0(self, writer);
533        self.flags &= !INPUT_LAST;
534        false
535    }
536
537    fn handle_esc_dispatch(&mut self, writer: &mut dyn ScreenWriter) -> bool {
538        if self.flags & INPUT_DISCARD != 0 {
539            return false;
540        }
541        dispatch::dispatch_esc(self, writer);
542        self.flags &= !INPUT_LAST;
543        false
544    }
545
546    fn handle_csi_dispatch(&mut self, writer: &mut dyn ScreenWriter) -> bool {
547        if self.flags & INPUT_DISCARD != 0 {
548            return false;
549        }
550        dispatch::dispatch_csi(self, writer);
551        self.flags &= !INPUT_LAST;
552        false
553    }
554
555    fn handle_dcs_dispatch(&mut self, writer: &mut dyn ScreenWriter) -> bool {
556        if self.flags & INPUT_DISCARD != 0 {
557            return false;
558        }
559        dispatch::dispatch_dcs(self, writer);
560        false
561    }
562
563    fn handle_top_bit_set(&mut self, writer: &mut dyn ScreenWriter) -> bool {
564        self.flags &= !INPUT_LAST;
565
566        if !self.utf8_started {
567            self.utf8_started = true;
568            self.utf8_len = 0;
569            // Determine expected byte count from first byte.
570            let expected = if self.ch & 0xE0 == 0xC0 {
571                2
572            } else if self.ch & 0xF0 == 0xE0 {
573                3
574            } else if self.ch & 0xF8 == 0xF0 {
575                4
576            } else {
577                // Invalid start byte.
578                self.stop_utf8(writer);
579                return false;
580            };
581            self.utf8_expected = expected;
582            self.utf8_buf[0] = self.ch;
583            self.utf8_len = 1;
584            return false;
585        }
586
587        // Continuation byte.
588        if self.ch & 0xC0 != 0x80 {
589            // Not a valid continuation: emit replacement and re-process.
590            self.stop_utf8(writer);
591            // Re-start UTF-8 with current byte if it's a start byte.
592            if self.ch >= 0x80 {
593                return self.handle_top_bit_set(writer);
594            }
595            return false;
596        }
597
598        self.utf8_buf[self.utf8_len as usize] = self.ch;
599        self.utf8_len += 1;
600
601        if self.utf8_len < self.utf8_expected {
602            return false; // More bytes expected.
603        }
604
605        // Complete: decode.
606        self.utf8_started = false;
607        let bytes = &self.utf8_buf[..self.utf8_len as usize];
608        let s = match std::str::from_utf8(bytes) {
609            Ok(s) => s,
610            Err(_) => {
611                writer.collect_add('\u{FFFD}', &self.cell);
612                return false;
613            }
614        };
615        let c = match s.chars().next() {
616            Some(c) => c,
617            None => {
618                writer.collect_add('\u{FFFD}', &self.cell);
619                return false;
620            }
621        };
622
623        writer.collect_add(c, &self.cell);
624
625        self.last_char = Some(c);
626        self.flags |= INPUT_LAST;
627
628        false
629    }
630
631    /// Append a reply string to the reply buffer.
632    fn reply(&mut self, s: &str) {
633        self.reply_buf.extend_from_slice(s.as_bytes());
634    }
635
636    /// Interm buf as a string slice for table lookups.
637    fn interm_str(&self) -> &[u8] {
638        &self.interm_buf[..self.interm_len]
639    }
640}
641
642impl Default for InputParser {
643    fn default() -> Self {
644        Self::new()
645    }
646}