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}