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