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