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/// Colour slots reported to OSC 10/11/12 queries.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum OscColourSlot {
69    /// OSC 10 — default foreground.
70    Foreground,
71    /// OSC 11 — default background.
72    Background,
73    /// OSC 12 — cursor colour.
74    Cursor,
75}
76
77impl OscColourSlot {
78    pub(crate) const fn osc_number(self) -> u32 {
79        match self {
80            Self::Foreground => 10,
81            Self::Background => 11,
82            Self::Cursor => 12,
83        }
84    }
85
86    const fn index(self) -> usize {
87        match self {
88            Self::Foreground => 0,
89            Self::Background => 1,
90            Self::Cursor => 2,
91        }
92    }
93}
94
95/// Per-pane VT input parser, matching tmux `input_ctx`.
96pub struct InputParser {
97    /// Current parser state.
98    state: InputState,
99    /// Parser flags (INPUT_DISCARD, INPUT_LAST).
100    flags: u32,
101
102    /// Current character being processed.
103    ch: u8,
104
105    /// Intermediate character buffer.
106    interm_buf: [u8; INTERM_BUF_MAX],
107    interm_len: usize,
108
109    /// Raw parameter buffer.
110    param_buf: [u8; PARAM_BUF_MAX],
111    param_len: usize,
112
113    /// Dynamic input/string buffer.
114    input_buf: Vec<u8>,
115    input_buf_max: usize,
116    /// Which terminator ended the string.
117    input_end: InputEndType,
118
119    /// Parsed parameter list.
120    param_list: ParamList,
121
122    /// Cell state (current attributes, character set, etc.).
123    cell: CellState,
124    /// Saved cell state for DECSC/DECRC.
125    saved: SavedState,
126
127    /// UTF-8 accumulator.
128    utf8_buf: [u8; 4],
129    utf8_len: u8,
130    utf8_expected: u8,
131    utf8_started: bool,
132
133    /// Last printed character data for REP.
134    last_char: Option<char>,
135
136    /// Bytes accumulated since last ground state, for control-mode catch-up.
137    since_ground: Vec<u8>,
138
139    /// Whether ground timer would be active (modeled as flag; actual timer
140    /// is a server-side concern).
141    ground_timer_active: bool,
142
143    /// Reply buffer: replies to be sent back to the PTY.
144    reply_buf: Vec<u8>,
145    /// Dropped terminal passthrough events caused by parser string limits.
146    terminal_passthrough_dropped_count: u64,
147
148    /// Application-set OSC 10/11/12 colours (fg, bg, cursor). The daemon does
149    /// not invent colours for an unknown attached terminal palette.
150    osc_colours: [Option<String>; 3],
151}
152
153impl InputParser {
154    /// Creates a new parser in the ground state with default cell attributes.
155    #[must_use]
156    pub fn new() -> Self {
157        Self {
158            state: InputState::Ground,
159            flags: 0,
160            ch: 0,
161            interm_buf: [0; INTERM_BUF_MAX],
162            interm_len: 0,
163            param_buf: [0; PARAM_BUF_MAX],
164            param_len: 0,
165            input_buf: Vec::with_capacity(INPUT_BUF_START),
166            input_buf_max: INPUT_BUF_MAX,
167            input_end: InputEndType::St,
168            param_list: ParamList::new(),
169            cell: CellState::default(),
170            saved: SavedState::default(),
171            utf8_buf: [0; 4],
172            utf8_len: 0,
173            utf8_expected: 0,
174            utf8_started: false,
175            last_char: None,
176            since_ground: Vec::new(),
177            ground_timer_active: false,
178            reply_buf: Vec::new(),
179            terminal_passthrough_dropped_count: 0,
180            osc_colours: [None, None, None],
181        }
182    }
183
184    /// Returns an application-defined colour for an OSC 10/11/12 query.
185    pub(crate) fn osc_colour(&self, slot: OscColourSlot) -> Option<&str> {
186        self.osc_colours[slot.index()].as_deref()
187    }
188
189    /// Records an application-set OSC 10/11/12 colour so later queries
190    /// round-trip the value.
191    pub(crate) fn set_osc_colour(&mut self, slot: OscColourSlot, value: &str) {
192        // A stored colour is reflected verbatim to every later query, so bound
193        // it to a sane colour-spec length: otherwise a pane could set a huge
194        // (up to input-buffer-sized) value and then flood queries in one read
195        // batch, amplifying it into unbounded reply memory and OOM-ing the
196        // daemon. A real X11 / `rgb:` / `#rrggbb` spec is far shorter; longer
197        // values are not colours and are left unstored, so queries keep
198        // answering the prior value or remain silent when the palette is
199        // unknown.
200        const OSC_COLOUR_MAX_LEN: usize = 64;
201        if value.len() > OSC_COLOUR_MAX_LEN {
202            return;
203        }
204        self.osc_colours[slot.index()] = Some(value.to_owned());
205    }
206
207    /// Resets an OSC colour back to unknown (OSC 110/111/112).
208    pub(crate) fn reset_osc_colour(&mut self, slot: OscColourSlot) {
209        self.osc_colours[slot.index()] = None;
210    }
211
212    /// Updates the maximum regular string buffer size used for OSC/DCS input.
213    pub fn set_input_buffer_limit(&mut self, limit: usize) {
214        self.input_buf_max = limit.max(INPUT_BUF_START);
215    }
216
217    pub(crate) const fn configured_input_buffer_limit(&self) -> usize {
218        self.input_buf_max
219    }
220
221    /// Returns the current parser state.
222    #[must_use]
223    pub fn state(&self) -> InputState {
224        self.state
225    }
226
227    /// Returns and drains accumulated reply bytes.
228    pub fn take_replies(&mut self) -> Vec<u8> {
229        std::mem::take(&mut self.reply_buf)
230    }
231
232    /// Returns and drains terminal passthrough drops caused by parser limits.
233    pub(crate) fn take_terminal_passthrough_dropped_count(&mut self) -> u64 {
234        let dropped = self.terminal_passthrough_dropped_count;
235        self.terminal_passthrough_dropped_count = 0;
236        dropped
237    }
238
239    /// Returns and drains accumulated since-ground bytes.
240    pub fn take_since_ground(&mut self) -> Vec<u8> {
241        std::mem::take(&mut self.since_ground)
242    }
243
244    /// Returns any bytes still buffered in an incomplete parser state.
245    #[must_use]
246    pub fn pending_bytes(&self) -> Vec<u8> {
247        if self.state != InputState::Ground {
248            return self.since_ground.clone();
249        }
250        if self.utf8_started {
251            return self.utf8_buf[..usize::from(self.utf8_len)].to_vec();
252        }
253        Vec::new()
254    }
255
256    /// Returns true if the ground timer should be running.
257    #[must_use]
258    pub fn ground_timer_active(&self) -> bool {
259        self.ground_timer_active
260    }
261
262    /// Called by the server when the ground timer expires (5s timeout).
263    pub fn ground_timer_expired(&mut self) {
264        self.reset_to_ground();
265    }
266
267    /// Resets the parser to ground state.
268    pub fn reset_to_ground(&mut self) {
269        self.clear();
270        self.state = InputState::Ground;
271        self.flags = 0;
272        self.enter_ground();
273    }
274
275    /// Returns a reference to the current cell state.
276    #[must_use]
277    pub fn cell_state(&self) -> &CellState {
278        &self.cell
279    }
280
281    pub(crate) fn plain_output_forwarding_safe(&self) -> bool {
282        self.state == InputState::Ground && !self.utf8_started && self.cell == CellState::default()
283    }
284
285    /// Parse a buffer of bytes, dispatching actions to the screen writer.
286    pub fn parse<W: ScreenWriter + ?Sized>(&mut self, buf: &[u8], writer: &mut W) {
287        let mut index = 0;
288        while index < buf.len() {
289            if self.state == InputState::Ground && !self.utf8_started {
290                let printable_end = buf[index..]
291                    .iter()
292                    .position(|byte| !byte.is_ascii_graphic() && *byte != b' ')
293                    .map_or(buf.len(), |offset| index + offset);
294                if printable_end > index {
295                    self.handle_printable_ascii_run(&buf[index..printable_end], writer);
296                    index = printable_end;
297                    continue;
298                }
299                if self.handle_ground_c0_fast_path(buf[index], writer) {
300                    index += 1;
301                    continue;
302                }
303            }
304
305            self.ch = buf[index];
306            let transition = self.find_transition();
307            self.execute_transition(transition, writer);
308            index += 1;
309        }
310    }
311
312    fn handle_printable_ascii_run<W: ScreenWriter + ?Sized>(
313        &mut self,
314        bytes: &[u8],
315        writer: &mut W,
316    ) {
317        debug_assert_eq!(self.state, InputState::Ground);
318        let set = if self.cell.set == 0 {
319            self.cell.g0set
320        } else {
321            self.cell.g1set
322        };
323        let acs = set != 0;
324        writer.collect_add_ascii_run(bytes, &self.cell, acs);
325        if let Some(&last) = bytes.last() {
326            self.last_char = Some(char::from(last));
327        }
328        self.flags |= INPUT_LAST;
329    }
330
331    fn handle_ground_c0_fast_path<W: ScreenWriter + ?Sized>(
332        &mut self,
333        byte: u8,
334        writer: &mut W,
335    ) -> bool {
336        match byte {
337            0x0a..=0x0c => {
338                writer.collect_end();
339                writer.linefeed(false, self.cell.bg());
340                if writer.current_mode() & mode::MODE_CRLF != 0 {
341                    writer.carriage_return();
342                }
343            }
344            0x0d => {
345                writer.collect_end();
346                writer.carriage_return();
347            }
348            _ => return false,
349        }
350        self.flags &= !INPUT_LAST;
351        true
352    }
353
354    fn find_transition(&self) -> Transition {
355        self.state.transition_for_byte(self.ch)
356    }
357
358    fn execute_transition<W: ScreenWriter + ?Sized>(&mut self, trans: Transition, writer: &mut W) {
359        // Any state except print stops collect_end equivalent.
360        if !matches!(
361            trans.handler,
362            states::Handler::Print | states::Handler::TopBitSet
363        ) {
364            writer.collect_end();
365        }
366
367        // Execute handler; if it returns true, skip state transition.
368        let skip_state = match trans.handler {
369            states::Handler::None => false,
370            states::Handler::Print => self.handle_print(writer),
371            states::Handler::C0Dispatch => self.handle_c0_dispatch(writer),
372            states::Handler::EscDispatch => self.handle_esc_dispatch(writer),
373            states::Handler::CsiDispatch => self.handle_csi_dispatch(writer),
374            states::Handler::DcsDispatch => self.handle_dcs_dispatch(writer),
375            states::Handler::Intermediate => self.handle_intermediate(),
376            states::Handler::Parameter => self.handle_parameter(),
377            states::Handler::Input => self.handle_input(),
378            states::Handler::TopBitSet => self.handle_top_bit_set(writer),
379            states::Handler::EndBel => self.handle_end_bel(),
380        };
381
382        if skip_state {
383            return;
384        }
385
386        if self.should_recover_tmux_passthrough_osc_bel() {
387            self.input_end = InputEndType::Bel;
388            self.handle_dcs_dispatch(writer);
389            self.set_state(InputState::Ground, writer);
390            return;
391        }
392
393        if let Some(next) = trans.next_state {
394            self.set_state(next, writer);
395        }
396
397        // If not in ground state, save byte to since_ground.
398        if self.state != InputState::Ground && self.since_ground.len() < self.input_buf_max {
399            self.since_ground.push(self.ch);
400        }
401    }
402
403    fn set_state<W: ScreenWriter + ?Sized>(&mut self, next: InputState, writer: &mut W) {
404        // Call exit handler for current state.
405        self.exit_state(writer);
406        self.state = next;
407        // Call enter handler for new state.
408        self.enter_state(writer);
409    }
410
411    fn enter_state<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
412        match self.state {
413            InputState::Ground => self.enter_ground(),
414            InputState::EscEnter => self.clear(),
415            InputState::CsiEnter => self.clear(),
416            InputState::DcsEnter => self.enter_dcs(),
417            InputState::OscString => self.enter_osc(),
418            InputState::ApcString => self.enter_apc(),
419            InputState::RenameString => self.enter_rename(),
420            InputState::ConsumeSt => self.enter_rename(), // same as rename in tmux
421            _ => {}
422        }
423        let _ = writer; // writer not needed for enter handlers currently
424    }
425
426    fn exit_state<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
427        match self.state {
428            InputState::OscString => self.exit_osc(writer),
429            InputState::ApcString => self.exit_apc(writer),
430            InputState::RenameString => self.exit_rename(writer),
431            _ => {}
432        }
433    }
434
435    fn clear(&mut self) {
436        self.ground_timer_active = false;
437        self.interm_buf = [0; INTERM_BUF_MAX];
438        self.interm_len = 0;
439        self.param_buf = [0; PARAM_BUF_MAX];
440        self.param_len = 0;
441        self.input_buf.clear();
442        self.input_end = InputEndType::St;
443        self.flags &= !INPUT_DISCARD;
444    }
445
446    fn enter_ground(&mut self) {
447        self.ground_timer_active = false;
448        self.since_ground.clear();
449        // Shrink input buffer back to start size.
450        if self.input_buf.capacity() > INPUT_BUF_START {
451            self.input_buf = Vec::with_capacity(INPUT_BUF_START);
452        }
453    }
454
455    fn enter_dcs(&mut self) {
456        self.clear();
457        self.ground_timer_active = true;
458        self.flags &= !INPUT_LAST;
459    }
460
461    fn enter_osc(&mut self) {
462        self.clear();
463        self.ground_timer_active = true;
464        self.flags &= !INPUT_LAST;
465    }
466
467    fn enter_apc(&mut self) {
468        self.clear();
469        self.ground_timer_active = true;
470        self.flags &= !INPUT_LAST;
471    }
472
473    fn enter_rename(&mut self) {
474        self.clear();
475        self.ground_timer_active = true;
476        self.flags &= !INPUT_LAST;
477    }
478
479    fn exit_osc<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
480        if self.flags & INPUT_DISCARD != 0 {
481            return;
482        }
483        dispatch::dispatch_osc(self, writer);
484    }
485
486    fn exit_apc<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
487        if self.flags & INPUT_DISCARD != 0 {
488            return;
489        }
490        if passthrough::is_kitty_graphics_apc(&self.input_buf) {
491            writer.apc_passthrough(&self.input_buf);
492            return;
493        }
494        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
495        writer.set_title(&buf);
496    }
497
498    fn exit_rename<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
499        if self.flags & INPUT_DISCARD != 0 {
500            return;
501        }
502        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
503        writer.set_window_name(&buf);
504    }
505
506    /// Stop any in-progress UTF-8 sequence and emit U+FFFD.
507    fn stop_utf8<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
508        if self.utf8_started {
509            writer.collect_add('\u{FFFD}', &self.cell);
510            self.utf8_started = false;
511            self.utf8_len = 0;
512            self.utf8_expected = 0;
513        }
514    }
515
516    fn handle_print<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
517        self.stop_utf8(writer);
518
519        let ch = self.ch as char;
520        let set = if self.cell.set == 0 {
521            self.cell.g0set
522        } else {
523            self.cell.g1set
524        };
525
526        writer.collect_add_with_charset(ch, &self.cell, set != 0);
527
528        self.last_char = Some(ch);
529        self.flags |= INPUT_LAST;
530
531        false
532    }
533
534    fn handle_intermediate(&mut self) -> bool {
535        if self.interm_len >= INTERM_BUF_MAX - 1 {
536            self.flags |= INPUT_DISCARD;
537        } else {
538            self.interm_buf[self.interm_len] = self.ch;
539            self.interm_len += 1;
540        }
541        false
542    }
543
544    fn handle_parameter(&mut self) -> bool {
545        if self.param_len >= PARAM_BUF_MAX - 1 {
546            self.flags |= INPUT_DISCARD;
547        } else {
548            self.param_buf[self.param_len] = self.ch;
549            self.param_len += 1;
550        }
551        false
552    }
553
554    fn handle_input(&mut self) -> bool {
555        let escaped_dcs_byte = self.state == InputState::DcsEscape;
556        let bytes_to_push = if escaped_dcs_byte && self.ch != 0x1b {
557            2
558        } else {
559            1
560        };
561        let input_limit = self.input_buffer_limit();
562        if self.input_buf.len() + bytes_to_push >= input_limit {
563            if self.flags & INPUT_DISCARD == 0 && self.is_terminal_passthrough_string() {
564                self.terminal_passthrough_dropped_count =
565                    self.terminal_passthrough_dropped_count.saturating_add(1);
566            }
567            self.flags |= INPUT_DISCARD;
568        } else if escaped_dcs_byte && self.ch == 0x1b {
569            self.input_buf.push(0x1b);
570        } else if escaped_dcs_byte {
571            self.input_buf.push(0x1b);
572            self.input_buf.push(self.ch);
573        } else {
574            self.input_buf.push(self.ch);
575        }
576        false
577    }
578
579    fn input_buffer_limit(&self) -> usize {
580        if self.is_terminal_passthrough_string() {
581            return MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES;
582        }
583        self.input_buf_max
584    }
585
586    fn is_terminal_passthrough_string(&self) -> bool {
587        (self.state == InputState::ApcString && passthrough::is_kitty_graphics_apc(&self.input_buf))
588            || (matches!(self.state, InputState::DcsHandler | InputState::DcsEscape)
589                && self.interm_len == 0
590                && (self.input_buf.first() == Some(&b'q') || self.input_buf.starts_with(b"tmux;")))
591    }
592
593    fn should_recover_tmux_passthrough_osc_bel(&self) -> bool {
594        if self.state != InputState::DcsHandler || self.ch != 0x07 || self.interm_len != 0 {
595            return false;
596        }
597
598        let Some(payload) = self.input_buf.strip_prefix(b"tmux;") else {
599            return false;
600        };
601
602        payload.starts_with(b"\x1b]") || payload.first() == Some(&0x9d)
603    }
604
605    fn handle_end_bel(&mut self) -> bool {
606        self.input_end = InputEndType::Bel;
607        false
608    }
609
610    fn handle_c0_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
611        self.stop_utf8(writer);
612        dispatch::dispatch_c0(self, writer);
613        self.flags &= !INPUT_LAST;
614        false
615    }
616
617    fn handle_esc_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
618        if self.flags & INPUT_DISCARD != 0 {
619            return false;
620        }
621        dispatch::dispatch_esc(self, writer);
622        self.flags &= !INPUT_LAST;
623        false
624    }
625
626    fn handle_csi_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
627        if self.flags & INPUT_DISCARD != 0 {
628            return false;
629        }
630        dispatch::dispatch_csi(self, writer);
631        self.flags &= !INPUT_LAST;
632        false
633    }
634
635    fn handle_dcs_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
636        if self.flags & INPUT_DISCARD != 0 {
637            return false;
638        }
639        dispatch::dispatch_dcs(self, writer);
640        false
641    }
642
643    fn handle_top_bit_set<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
644        self.flags &= !INPUT_LAST;
645
646        if !self.utf8_started {
647            self.utf8_started = true;
648            self.utf8_len = 0;
649            // Determine expected byte count from first byte.
650            let expected = if self.ch & 0xE0 == 0xC0 {
651                2
652            } else if self.ch & 0xF0 == 0xE0 {
653                3
654            } else if self.ch & 0xF8 == 0xF0 {
655                4
656            } else {
657                // Invalid start byte.
658                self.stop_utf8(writer);
659                return false;
660            };
661            self.utf8_expected = expected;
662            self.utf8_buf[0] = self.ch;
663            self.utf8_len = 1;
664            return false;
665        }
666
667        // Continuation byte.
668        if self.ch & 0xC0 != 0x80 {
669            // Not a valid continuation: emit replacement and re-process.
670            self.stop_utf8(writer);
671            // Re-start UTF-8 with current byte if it's a start byte.
672            if self.ch >= 0x80 {
673                return self.handle_top_bit_set(writer);
674            }
675            return false;
676        }
677
678        self.utf8_buf[self.utf8_len as usize] = self.ch;
679        self.utf8_len += 1;
680
681        if self.utf8_len < self.utf8_expected {
682            return false; // More bytes expected.
683        }
684
685        // Complete: decode.
686        self.utf8_started = false;
687        let bytes = &self.utf8_buf[..self.utf8_len as usize];
688        let s = match std::str::from_utf8(bytes) {
689            Ok(s) => s,
690            Err(_) => {
691                writer.collect_add('\u{FFFD}', &self.cell);
692                return false;
693            }
694        };
695        let c = match s.chars().next() {
696            Some(c) => c,
697            None => {
698                writer.collect_add('\u{FFFD}', &self.cell);
699                return false;
700            }
701        };
702
703        writer.collect_add(c, &self.cell);
704
705        self.last_char = Some(c);
706        self.flags |= INPUT_LAST;
707
708        false
709    }
710
711    /// Append a reply string to the reply buffer.
712    fn reply(&mut self, s: &str) {
713        self.reply_buf.extend_from_slice(s.as_bytes());
714    }
715
716    /// Interm buf as a string slice for table lookups.
717    fn interm_str(&self) -> &[u8] {
718        &self.interm_buf[..self.interm_len]
719    }
720}
721
722impl Default for InputParser {
723    fn default() -> Self {
724        Self::new()
725    }
726}