Skip to main content

devela/sys/os/term/event/input/
parser.rs

1// devela/src/sys/os/term/event/input/parser.rs
2//
3//! Defines [`TermInputParser`].
4//
5
6use crate::{_impl_init, EventKind, Key, TermInputState, TermParsed};
7
8#[doc = crate::_tags!(term event parser)]
9/// Parses terminal input bytes into normalized events.
10#[doc = crate::_doc_meta!{
11    location("sys/os/term"),
12    test_size_of(TermInputParser = 19|152),
13}]
14///
15/// `TermInputParser` is a byte-fed state machine. It accepts ordinary bytes,
16/// UTF-8 text, and terminal escape sequences, returning an [`EventKind`] when
17/// a complete input event has been recognized.
18///
19/// It is intentionally independent from any concrete terminal backend. Linux,
20/// Windows, web-terminal, pseudo-terminal, and test backends can all feed bytes
21/// into the same parser.
22///
23/// # Public output
24/// The public [`feed`][Self::feed] method returns only normalized user-facing events.
25///
26/// Terminal replies used for probing capabilities are parsed internally
27/// and are exposed only inside the crate.
28///
29/// # Escape handling
30/// A lone `ESC` byte is ambiguous: it may be the Escape key, or it may begin a
31/// longer escape sequence. Backends should call [`flush_escape`][Self::flush_escape]
32/// after their escape timeout expires.
33///
34/// # Supported seed grammar
35/// The first parser layer recognizes:
36/// - printable ASCII
37/// - UTF-8 scalar values
38/// - Enter, Tab, Backspace, Escape
39/// - basic Control-letter combinations
40/// - common CSI navigation and editing keys
41/// - cursor-position and device-attribute replies, internally
42///
43/// # Terminal limits
44///
45/// Terminal input is normalized from the byte stream reported by the active
46/// terminal emulator. Some information may be unavailable because terminals
47/// often reserve key or mouse combinations for selection, menus, shortcuts, or
48/// scrollback behavior before the application receives them.
49///
50/// In SGR mouse mode, terminal reports can encode Shift, Alt/Meta, and Control
51/// modifier bits. `TermInputParser` preserves those bits when present, but
52/// applications should not assume every terminal will report every modifier
53/// combination. In particular, Shift-click and Control-click are commonly
54/// intercepted or repurposed by terminal emulators.
55#[derive(Clone, Debug, Default)]
56pub struct TermInputParser {
57    pub(super) state: TermInputState,
58    pub(super) paste: bool,
59}
60_impl_init! { Self::new() => TermInputParser }
61
62impl TermInputParser {
63    /// Returns a new parser in the ground state.
64    #[must_use]
65    pub const fn new() -> Self {
66        Self { state: TermInputState::Ground, paste: false }
67    }
68
69    /// Feeds one byte into the parser.
70    ///
71    /// Returns `Some(EventKind)` when the byte completes a user-facing event.
72    ///
73    /// Returns `None` while a multi-byte sequence is still pending, or when the
74    /// completed sequence is an internal terminal reply.
75    ///
76    /// Use [`flush_escape`][Self::flush_escape] to resolve a pending lone `ESC`
77    /// after the backend's escape timeout expires.
78    //
79    // NOTE: not const because it discards `TermParsed`,
80    // whose event path may carry non-const-drop event payloads.
81    pub fn feed(&mut self, byte: u8) -> Option<EventKind> {
82        match self.feed_parsed(byte) {
83            TermParsed::Event(ev) => Some(ev),
84            _ => None,
85        }
86    }
87
88    #[cfg(not(feature = "alloc"))]
89    #[cfg_attr(nightly_doc, doc(cfg(not(feature = "alloc"))))]
90    /// Feeds one byte into the parser in const contexts.
91    ///
92    /// This is only available without `alloc`,
93    /// because alloc-enabled events may contain owned values with destructors.
94    pub const fn feed_const(&mut self, byte: u8) -> Option<EventKind> {
95        match self.feed_parsed(byte) {
96            TermParsed::Event(ev) => Some(ev),
97            _ => None,
98        }
99    }
100
101    /// Flushes a pending lone `ESC` as an Escape key press.
102    ///
103    /// Returns `None` unless the parser is currently waiting after an `ESC` byte.
104    ///
105    /// This is needed because terminal escape sequences
106    /// and the Escape key share the same leading byte.
107    pub const fn flush_escape(&mut self) -> Option<EventKind> {
108        if matches!(self.state, TermInputState::Esc) {
109            self.state = TermInputState::Ground;
110            Some(Self::key(Key::Escape))
111        } else {
112            None
113        }
114    }
115}