1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
//! Small terminal-key contract, not a complete terminal emulator.
//!
//! UTF-8 characters, Enter, Escape and unambiguous legacy control/Alt characters are
//! decoded. Other escape sequences and bracketed paste produce `Other`, never
//! their embedded characters. Unbracketed paste is indistinguishable from typed
//! text. Releases are absent from byte protocols; Windows capture ignores them
//! and expands bounded repeats. Shift cannot be recovered from legacy bytes.
//! Ambiguous Alt encodings (including Alt+Space) are conservatively parsed as
//! escape sequences, not text keys; incomplete sequences fail closed.
use std::io;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Key {
Character(char),
Enter,
Escape,
Other,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct KeyModifiers {
pub control: bool,
pub alt: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyEvent {
pub key: Key,
pub modifiers: KeyModifiers,
}
#[derive(Default)]
enum State {
#[default]
Ground,
Escape,
EscapeIntermediate,
Utf8 {
length: usize,
alt: bool,
},
Csi,
Ss3,
Paste {
matched: usize,
},
String {
bell: bool,
escape: bool,
},
Failed,
}
/// Incremental decoder with at most 128 buffered bytes and 64 KiB per control
/// string/paste. Errors poison the decoder: trailing input cannot become keys.
/// Callers own timing; [`TerminalKeys`] also bounds incomplete-sequence time.
#[derive(Default)]
pub struct KeyDecoder {
state: State,
pending: Vec<u8>,
consumed: usize,
}
impl KeyDecoder {
pub fn new() -> Self {
Self::default()
}
fn failure(&mut self) -> io::Error {
self.state = State::Failed;
self.pending.clear();
io::Error::new(
io::ErrorKind::InvalidData,
"malformed or oversized terminal key sequence",
)
}
fn event(&mut self, key: Key, control: bool, alt: bool) -> Option<KeyEvent> {
self.state = State::Ground;
self.pending.clear();
self.consumed = 0;
Some(KeyEvent {
key,
modifiers: KeyModifiers { control, alt },
})
}
fn character(&mut self, byte: u8, alt: bool) -> io::Result<Option<KeyEvent>> {
let key = match byte {
b'\r' | b'\n' => Key::Enter,
1..=26 => {
return Ok(self.event(Key::Character(char::from(b'a' + byte - 1)), true, alt))
}
32..=126 => Key::Character(char::from(byte)),
0 | 28..=31 | 127 => Key::Other,
0xc2..=0xf4 => {
let length = if byte < 0xe0 {
2
} else if byte < 0xf0 {
3
} else {
4
};
self.pending.push(byte);
self.state = State::Utf8 { length, alt };
return Ok(None);
}
_ => return Err(self.failure()),
};
Ok(self.event(key, false, alt))
}
/// Feed one byte. `None` means a sequence is incomplete, not a key release.
pub fn push(&mut self, byte: u8) -> io::Result<Option<KeyEvent>> {
self.consumed += 1;
if self.consumed > 65_536 {
return Err(self.failure());
}
match self.state {
State::Failed => Err(self.failure()),
State::Ground if byte == 27 => {
self.state = State::Escape;
Ok(None)
}
State::Ground => self.character(byte, false),
State::Escape => match byte {
b'[' => {
self.state = State::Csi;
Ok(None)
}
b'O' => {
self.state = State::Ss3;
Ok(None)
}
27 => {
let event = self.event(Key::Escape, false, false);
self.state = State::Escape;
self.consumed = 1;
Ok(event)
}
b']' | b'P' | b'_' | b'^' | b'X' => {
self.state = State::String {
bell: byte == b']',
escape: false,
};
Ok(None)
}
0x20..=0x2f => {
self.state = State::EscapeIntermediate;
Ok(None)
}
_ => self.character(byte, true),
},
State::EscapeIntermediate => match byte {
0x20..=0x2f if self.consumed <= 128 => Ok(None),
0x30..=0x7e => Ok(self.event(Key::Other, false, false)),
_ => Err(self.failure()),
},
State::Utf8 { length, alt } => {
self.pending.push(byte);
if self.pending.len() < length {
return Ok(None);
}
let character = std::str::from_utf8(&self.pending)
.ok()
.and_then(|text| text.chars().next());
match character {
Some(character) => Ok(self.event(Key::Character(character), false, alt)),
None => Err(self.failure()),
}
}
State::Csi | State::Ss3 => {
let csi = matches!(self.state, State::Csi);
if self.pending.len() >= 128 {
return Err(self.failure());
}
self.pending.push(byte);
match byte {
0x20..=0x3f => Ok(None),
0x40..=0x7e if csi && self.pending == b"200~" => {
self.pending.clear();
self.state = State::Paste { matched: 0 };
Ok(None)
}
// X10 mouse packets have a raw payload after this final.
// They are unsupported, not standalone keys followed by text.
b'M' if csi && self.pending == b"M" => Err(self.failure()),
0x40..=0x7e => Ok(self.event(Key::Other, false, false)),
_ => Err(self.failure()),
}
}
State::Paste { matched } => {
let marker = b"\x1b[201~";
let matched = if byte == marker[matched] {
matched + 1
} else {
usize::from(byte == 27)
};
if matched == marker.len() {
return Ok(self.event(Key::Other, false, false));
}
self.state = State::Paste { matched };
Ok(None)
}
State::String { bell, escape } => {
if (bell && byte == 7) || (escape && byte == b'\\') {
return Ok(self.event(Key::Other, false, false));
}
self.state = State::String {
bell,
escape: byte == 27,
};
Ok(None)
}
}
}
/// Resolve a lone Escape after the caller's inter-byte wait. Other partial
/// sequences are errors, so their trailing spaces can never escape parsing.
pub fn finish_pending(&mut self) -> io::Result<Option<KeyEvent>> {
match self.state {
State::Ground => Ok(None),
State::Escape => Ok(self.event(Key::Escape, false, false)),
_ => Err(self.failure()),
}
}
}
/// Exclusive noncanonical, no-echo input owner. Existing native signal-key and
/// output processing are preserved; normally Ctrl+C remains a signal, not an
/// input event. Drop restores modes best-effort via the existing native session.
/// No additional background listener or channel is added.
pub struct TerminalKeys {
input: crate::TerminalInputSession,
decoder: KeyDecoder,
bytes: Vec<u8>,
cursor: usize,
partial_since: Option<Instant>,
}
impl TerminalKeys {
pub fn new() -> io::Result<Option<Self>> {
crate::TerminalInputSession::new_for_keys().map(|input| {
input.map(|input| Self {
input,
decoder: KeyDecoder::new(),
bytes: Vec::new(),
cursor: 0,
partial_since: None,
})
})
}
/// Return one key with a maximum requested wait of 100 ms. A poll examines
/// at most 64 KiB; queued bytes survive subsequent polls. Incomplete input
/// expires after 250 ms, checked on each poll (a lone Escape becomes a key).
pub fn poll(&mut self, wait: Duration) -> io::Result<Option<KeyEvent>> {
if wait > Duration::from_millis(100) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"terminal poll exceeds 100 ms",
));
}
if self.cursor == self.bytes.len()
&& self
.partial_since
.is_some_and(|start| start.elapsed() >= Duration::from_millis(250))
{
self.partial_since = None;
return self.decoder.finish_pending();
}
let deadline = Instant::now() + wait;
for _ in 0..65_536 {
if self.cursor == self.bytes.len() {
self.bytes.clear();
self.cursor = 0;
let Some(chunk) = self
.input
.read_chunk(deadline.saturating_duration_since(Instant::now()))?
else {
return Ok(None);
};
self.bytes = chunk.data;
if self.bytes.is_empty() {
return Ok(None);
}
}
let byte = self.bytes[self.cursor];
self.cursor += 1;
match self.decoder.push(byte)? {
Some(event) => {
self.partial_since =
(!matches!(self.decoder.state, State::Ground)).then(Instant::now);
return Ok(Some(event));
}
None => {
self.partial_since.get_or_insert_with(Instant::now);
}
}
}
Ok(None)
}
}