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