Skip to main content

tern_sdk/
keys.rs

1//! Layer 2b: the key decoder. It turns key bytes (what the input parser
2//! passes through) into keys named as in Tern's plugin API: legacy xterm
3//! input, kitty `CSI <code>[;<mods>] u` keys and bracketed paste.
4
5use std::fmt;
6
7use serde::Serialize;
8
9/// One key press, or a paste (`name` `"paste"` with its text).
10#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
11pub struct Key {
12	/// The key: a lowercase character (`"a"`, `"é"`), `"space"`, a named key
13	/// (`"enter"`, `"up"`, `"f5"`, …) or `"paste"`.
14	pub name:  String,
15	/// What the key types, when it types something.
16	#[serde(skip_serializing_if = "Option::is_none")]
17	pub text:  Option<String>,
18	/// Control held.
19	#[serde(skip_serializing_if = "std::ops::Not::not")]
20	pub ctrl:  bool,
21	/// Alt (Option) held.
22	#[serde(skip_serializing_if = "std::ops::Not::not")]
23	pub alt:   bool,
24	/// Shift held.
25	#[serde(skip_serializing_if = "std::ops::Not::not")]
26	pub shift: bool,
27	/// Cmd on macOS, Super elsewhere.
28	#[serde(skip_serializing_if = "std::ops::Not::not")]
29	pub meta:  bool,
30}
31
32impl Key {
33	/// A key without modifiers or text.
34	pub fn named(name: impl Into<String>) -> Self {
35		Self { name: name.into(), ..Self::default() }
36	}
37
38	/// Whether this is the chord `chord`: modifiers and a name joined by `+`
39	/// (`"ctrl+c"`, `"shift+tab"`, `"escape"`, `"ctrl++"`), every modifier
40	/// matching exactly.
41	///
42	/// # Example
43	/// ```
44	/// use tern_sdk::keys::Key;
45	///
46	/// let key = Key { ctrl: true, ..Key::named("c") };
47	/// assert!(key.is("ctrl+c"));
48	/// assert!(!key.is("c"));
49	/// ```
50	pub fn is(&self, chord: &str) -> bool {
51		let (mut ctrl, mut alt, mut shift, mut meta) = (false, false, false, false);
52		let mut name = chord;
53		while let Some((modifier, rest)) = name.split_once('+') {
54			match modifier {
55				"ctrl" => ctrl = true,
56				"alt" => alt = true,
57				"shift" => shift = true,
58				"meta" | "cmd" | "super" => meta = true,
59				_ => break,
60			}
61			name = rest;
62		}
63		self.name == name
64			&& self.ctrl == ctrl
65			&& self.alt == alt
66			&& self.shift == shift
67			&& self.meta == meta
68	}
69
70	/// Whether the key types text without Control, Alt or Meta held.
71	pub fn typed(&self) -> Option<&str> {
72		if self.ctrl || self.alt || self.meta || self.name == "paste" {
73			return None;
74		}
75		self.text.as_deref()
76	}
77
78	/// Applies xterm modifier parameter `param` (1 + bits).
79	const fn with_mods(mut self, param: u32) -> Self {
80		let bits = param.saturating_sub(1);
81		self.shift |= bits & 1 != 0;
82		self.alt |= bits & 2 != 0;
83		self.ctrl |= bits & 4 != 0;
84		self.meta |= bits & (8 | 32) != 0;
85		self
86	}
87}
88
89impl fmt::Display for Key {
90	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91		for (on, name) in
92			[(self.ctrl, "ctrl+"), (self.alt, "alt+"), (self.shift, "shift+"), (self.meta, "meta+")]
93		{
94			if on {
95				f.write_str(name)?;
96			}
97		}
98		f.write_str(&self.name)
99	}
100}
101
102/// What decoding the start of the buffer gave.
103enum Step {
104	/// Not decidable yet.
105	Wait,
106	/// This many bytes were consumed, giving maybe a key.
107	Took(usize, Option<Key>),
108}
109
110/// The streaming key decoder.
111///
112/// # Example
113/// ```
114/// use tern_sdk::keys::Decoder;
115///
116/// let mut keys = Decoder::new();
117/// let got = keys.feed(b"\x1b[1;5C");
118/// assert!(got[0].is("ctrl+right"));
119/// ```
120#[derive(Clone, Debug, Default)]
121pub struct Decoder {
122	/// Bytes of an undecided sequence.
123	buf:   Vec<u8>,
124	/// The text of a paste in progress.
125	paste: Option<Vec<u8>>,
126}
127
128impl Decoder {
129	/// A decoder with nothing held.
130	pub fn new() -> Self {
131		Self::default()
132	}
133
134	/// Whether bytes are held that [`flush`](Self::flush) would decide.
135	pub const fn pending(&self) -> bool {
136		!self.buf.is_empty() && self.paste.is_none()
137	}
138
139	/// Feeds key bytes and returns the keys they complete.
140	pub fn feed(&mut self, bytes: &[u8]) -> Vec<Key> {
141		self.buf.extend_from_slice(bytes);
142		let mut out = Vec::new();
143		self.run(&mut out, false);
144		out
145	}
146
147	/// Decides what is held: a lone `ESC` is Escape, an incomplete sequence
148	/// is dropped. A paste in progress keeps waiting for its end.
149	pub fn flush(&mut self) -> Vec<Key> {
150		let mut out = Vec::new();
151		self.run(&mut out, true);
152		out
153	}
154
155	/// Decodes as much of the buffer as it can.
156	fn run(&mut self, out: &mut Vec<Key>, flush: bool) {
157		let mut at = 0;
158		while at < self.buf.len() {
159			if let Some(paste) = &mut self.paste {
160				let rest = &self.buf[at..];
161				if let Some(end) = rest.windows(6).position(|w| w == b"\x1b[201~") {
162					paste.extend_from_slice(&rest[..end]);
163					at += end + 6;
164					let text =
165						String::from_utf8_lossy(&self.paste.take().unwrap_or_default()).into_owned();
166					out.push(Key { text: Some(text), ..Key::named("paste") });
167					continue;
168				}
169				let hold = (1..6)
170					.rev()
171					.find(|&n| rest.ends_with(&b"\x1b[201~"[..n]))
172					.unwrap_or(0);
173				paste.extend_from_slice(&rest[..rest.len() - hold]);
174				at = self.buf.len() - hold;
175				break;
176			}
177			match decode(&self.buf[at..], flush) {
178				Step::Wait => break,
179				Step::Took(n, key) => {
180					if &self.buf[at..at + n] == b"\x1b[200~" {
181						self.paste = Some(Vec::new());
182					}
183					at += n;
184					out.extend(key);
185				},
186			}
187		}
188		self.buf.drain(..at);
189	}
190}
191
192/// Decodes one key at the start of `b` (non-empty).
193fn decode(b: &[u8], flush: bool) -> Step {
194	match b[0] {
195		0x1b => escape(b, flush),
196		c if c < 0x20 || c == 0x7f => Step::Took(1, Some(control(c))),
197		_ => match utf8(b, flush) {
198			Some((n, Some(ch))) => Step::Took(n, Some(printable(ch))),
199			Some((n, None)) => Step::Took(n, None),
200			None => Step::Wait,
201		},
202	}
203}
204
205/// A control character's key.
206fn control(c: u8) -> Key {
207	match c {
208		b'\r' | b'\n' => Key::named("enter"),
209		b'\t' => Key::named("tab"),
210		0x7f => Key::named("backspace"),
211		0x08 => Key { ctrl: true, ..Key::named("backspace") },
212		0x1b => Key::named("escape"),
213		0x00 => Key { ctrl: true, ..Key::named("space") },
214		0x01..=0x1a => Key { ctrl: true, ..Key::named(((b'a' + c - 1) as char).to_string()) },
215		0x1c => Key { ctrl: true, ..Key::named("\\") },
216		0x1d => Key { ctrl: true, ..Key::named("]") },
217		0x1e => Key { ctrl: true, ..Key::named("^") },
218		_ => Key { ctrl: true, ..Key::named("_") },
219	}
220}
221
222/// A typed character's key.
223fn printable(ch: char) -> Key {
224	if ch == ' ' {
225		return Key { text: Some(" ".into()), ..Key::named("space") };
226	}
227	let lower: String = ch.to_lowercase().collect();
228	let shift = lower.chars().ne(std::iter::once(ch));
229	Key { text: Some(ch.to_string()), shift, ..Key::named(lower) }
230}
231
232/// One UTF-8 character at the start of `b`: its length and the character
233/// (`None` for an invalid byte), or `None` when it is incomplete.
234fn utf8(b: &[u8], flush: bool) -> Option<(usize, Option<char>)> {
235	let len = match b[0] {
236		0x00..=0x7f => 1,
237		0xc0..=0xdf => 2,
238		0xe0..=0xef => 3,
239		0xf0..=0xf7 => 4,
240		_ => return Some((1, None)),
241	};
242	if b.len() < len {
243		return if flush { Some((b.len(), None)) } else { None };
244	}
245	match std::str::from_utf8(&b[..len]) {
246		Ok(s) => Some((len, s.chars().next())),
247		Err(_) => Some((1, None)),
248	}
249}
250
251/// A sequence starting with `ESC`.
252fn escape(b: &[u8], flush: bool) -> Step {
253	let Some(&next) = b.get(1) else {
254		return if flush {
255			Step::Took(1, Some(Key::named("escape")))
256		} else {
257			Step::Wait
258		};
259	};
260	match next {
261		b'[' => csi(b, flush),
262		b'O' => match b.get(2) {
263			Some(&c) => Step::Took(3, ss3(c)),
264			None if flush => Step::Took(2, None),
265			None => Step::Wait,
266		},
267		0x1b => match escape(&b[1..], flush) {
268			Step::Wait => Step::Wait,
269			Step::Took(n, key) => Step::Took(n + 1, key.map(|k| Key { alt: true, text: None, ..k })),
270		},
271		_ => match decode(&b[1..], flush) {
272			Step::Wait => Step::Wait,
273			Step::Took(n, key) => Step::Took(n + 1, key.map(|k| Key { alt: true, text: None, ..k })),
274		},
275	}
276}
277
278/// An SS3 key (`ESC O <c>`).
279fn ss3(c: u8) -> Option<Key> {
280	let name = match c {
281		b'A' => "up",
282		b'B' => "down",
283		b'C' => "right",
284		b'D' => "left",
285		b'H' => "home",
286		b'F' => "end",
287		b'E' => "begin",
288		b'P' => "f1",
289		b'Q' => "f2",
290		b'R' => "f3",
291		b'S' => "f4",
292		b'M' => "enter",
293		_ => return None,
294	};
295	Some(Key::named(name))
296}
297
298/// A CSI sequence (`ESC [ … final`).
299fn csi(b: &[u8], flush: bool) -> Step {
300	let mut i = 2;
301	while let Some(&c) = b.get(i) {
302		match c {
303			0x20..=0x3f => i += 1,
304			0x40..=0x7e => return Step::Took(i + 1, csi_key(&b[2..i], c)),
305			_ => return Step::Took(i, None),
306		}
307	}
308	if flush {
309		Step::Took(b.len(), None)
310	} else {
311		Step::Wait
312	}
313}
314
315/// Parses `a;b:c;d` into numeric fields (each a list of `:` parts).
316fn fields(params: &[u8]) -> Option<Vec<Vec<u32>>> {
317	let text = std::str::from_utf8(params).ok()?;
318	text
319		.split(';')
320		.map(|f| {
321			f.split(':')
322				.map(|n| {
323					if n.is_empty() {
324						Some(0)
325					} else {
326						n.parse().ok()
327					}
328				})
329				.collect::<Option<Vec<u32>>>()
330		})
331		.collect()
332}
333
334/// The key of a CSI sequence with parameters `params` and final byte `fin`.
335fn csi_key(params: &[u8], fin: u8) -> Option<Key> {
336	if params
337		.first()
338		.is_some_and(|&p| matches!(p, b'?' | b'<' | b'>' | b'='))
339	{
340		return None;
341	}
342	let f = fields(params)?;
343	let num = |i: usize| f.get(i).and_then(|v| v.first()).copied().unwrap_or(0);
344	let mods = num(1).max(1);
345	match fin {
346		b'A' | b'B' | b'C' | b'D' | b'H' | b'F' | b'E' | b'P' | b'Q' | b'R' | b'S' => {
347			let name = match fin {
348				b'A' => "up",
349				b'B' => "down",
350				b'C' => "right",
351				b'D' => "left",
352				b'H' => "home",
353				b'F' => "end",
354				b'E' => "begin",
355				b'P' => "f1",
356				b'Q' => "f2",
357				b'R' => "f3",
358				_ => "f4",
359			};
360			Some(Key::named(name).with_mods(mods))
361		},
362		b'Z' => Some(Key { shift: true, ..Key::named("tab") }.with_mods(mods)),
363		b'~' => tilde(num(0)).map(|name| Key::named(name).with_mods(mods)),
364		b'u' => kitty(&f),
365		_ => None,
366	}
367}
368
369/// The name of a `CSI <n> ~` key.
370const fn tilde(n: u32) -> Option<&'static str> {
371	Some(match n {
372		1 | 7 => "home",
373		2 => "insert",
374		3 => "delete",
375		4 | 8 => "end",
376		5 => "page_up",
377		6 => "page_down",
378		11 => "f1",
379		12 => "f2",
380		13 => "f3",
381		14 => "f4",
382		15 => "f5",
383		17 => "f6",
384		18 => "f7",
385		19 => "f8",
386		20 => "f9",
387		21 => "f10",
388		23 => "f11",
389		24 => "f12",
390		25 => "f13",
391		26 => "f14",
392		28 => "f15",
393		29 => "menu",
394		31 => "f17",
395		32 => "f18",
396		33 => "f19",
397		34 => "f20",
398		_ => return None,
399	})
400}
401
402/// A kitty `CSI code[:alt…][;mods[:event]][;text] u` key.
403fn kitty(f: &[Vec<u32>]) -> Option<Key> {
404	let code = f.first().and_then(|v| v.first()).copied()?;
405	let mods_field = f.get(1);
406	let mods = mods_field
407		.and_then(|v| v.first())
408		.copied()
409		.unwrap_or(1)
410		.max(1);
411	// Event type 3 is a release, which is never delivered.
412	if mods_field.and_then(|v| v.get(1)).copied() == Some(3) {
413		return None;
414	}
415	let named = |name: &str| Some(Key::named(name).with_mods(mods));
416	let key = match code {
417		27 => named("escape"),
418		13 => named("enter"),
419		9 => named("tab"),
420		127 | 8 => named("backspace"),
421		32 => named("space"),
422		57358 => named("caps_lock"),
423		57359 => named("scroll_lock"),
424		57360 => named("num_lock"),
425		57361 => named("print_screen"),
426		57362 => named("pause"),
427		57363 => named("menu"),
428		57364..=57398 => {
429			let name = format!("f{}", code - 57364 + 1);
430			Some(Key::named(name).with_mods(mods))
431		},
432		57399..=57427 => keypad(code).map(|name| Key::named(name).with_mods(mods)),
433		// Unmapped functional keys, media keys and lone modifiers are not delivered.
434		57344..=63743 => None,
435		_ => {
436			let ch = char::from_u32(code)?;
437			let lower: String = ch.to_lowercase().collect();
438			Some(Key::named(lower).with_mods(mods))
439		},
440	}?;
441	Some(with_text(key, f.get(2), code))
442}
443
444/// Adds the text a kitty key types: its text field, or for a printable key
445/// without Control, Alt or Meta, the (shifted) character.
446fn with_text(mut key: Key, text: Option<&Vec<u32>>, code: u32) -> Key {
447	if key.ctrl || key.alt || key.meta {
448		return key;
449	}
450	if let Some(cps) = text {
451		let s: String = cps.iter().filter_map(|&c| char::from_u32(c)).collect();
452		if !s.is_empty() {
453			key.text = Some(s);
454		}
455		return key;
456	}
457	if key.name == "space" {
458		key.text = Some(" ".into());
459	} else if key.name.chars().count() == 1 && code >= 0x20 {
460		key.text = Some(if key.shift {
461			key.name.to_uppercase()
462		} else {
463			key.name.clone()
464		});
465	}
466	key
467}
468
469/// The character or name a keypad key stands for.
470const fn keypad(code: u32) -> Option<&'static str> {
471	Some(match code {
472		57399 => "0",
473		57400 => "1",
474		57401 => "2",
475		57402 => "3",
476		57403 => "4",
477		57404 => "5",
478		57405 => "6",
479		57406 => "7",
480		57407 => "8",
481		57408 => "9",
482		57409 => ".",
483		57410 => "/",
484		57411 => "*",
485		57412 => "-",
486		57413 => "+",
487		57414 => "enter",
488		57415 => "=",
489		57416 => ",",
490		57417 => "left",
491		57418 => "right",
492		57419 => "up",
493		57420 => "down",
494		57421 => "page_up",
495		57422 => "page_down",
496		57423 => "home",
497		57424 => "end",
498		57425 => "insert",
499		57426 => "delete",
500		57427 => "begin",
501		_ => return None,
502	})
503}