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