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.array_windows().position(|w| w == b"\x1b[201~") {
162					paste.extend_from_slice(&rest[..end]);
163					at += end + 6;
164					let text = String::from_utf8_lossy_owned(self.paste.take().unwrap_or_default());
165					out.push(Key { text: Some(text), ..Key::named("paste") });
166					continue;
167				}
168				let hold = (1..6)
169					.rev()
170					.find(|&n| rest.ends_with(&b"\x1b[201~"[..n]))
171					.unwrap_or(0);
172				paste.extend_from_slice(&rest[..rest.len() - hold]);
173				at = self.buf.len() - hold;
174				break;
175			}
176			match decode(&self.buf[at..], flush) {
177				Step::Wait => break,
178				Step::Took(n, key) => {
179					if &self.buf[at..at + n] == b"\x1b[200~" {
180						self.paste = Some(Vec::new());
181					}
182					at += n;
183					out.extend(key);
184				},
185			}
186		}
187		self.buf.drain(..at);
188	}
189}
190
191/// Decodes one key at the start of `b` (non-empty).
192fn decode(b: &[u8], flush: bool) -> Step {
193	match b[0] {
194		0x1b => escape(b, flush),
195		c if c < 0x20 || c == 0x7f => Step::Took(1, Some(control(c))),
196		_ => match utf8(b, flush) {
197			Some((n, Some(ch))) => Step::Took(n, Some(printable(ch))),
198			Some((n, None)) => Step::Took(n, None),
199			None => Step::Wait,
200		},
201	}
202}
203
204/// A control character's key.
205fn control(c: u8) -> Key {
206	match c {
207		b'\r' | b'\n' => Key::named("enter"),
208		b'\t' => Key::named("tab"),
209		0x7f => Key::named("backspace"),
210		0x08 => Key { ctrl: true, ..Key::named("backspace") },
211		0x1b => Key::named("escape"),
212		0x00 => Key { ctrl: true, ..Key::named("space") },
213		0x01..=0x1a => Key { ctrl: true, ..Key::named(((b'a' + c - 1) as char).to_string()) },
214		0x1c => Key { ctrl: true, ..Key::named("\\") },
215		0x1d => Key { ctrl: true, ..Key::named("]") },
216		0x1e => Key { ctrl: true, ..Key::named("^") },
217		_ => Key { ctrl: true, ..Key::named("_") },
218	}
219}
220
221/// A typed character's key.
222fn printable(ch: char) -> Key {
223	if ch == ' ' {
224		return Key { text: Some(" ".into()), ..Key::named("space") };
225	}
226	let lower: String = ch.to_lowercase().collect();
227	let shift = lower.chars().ne(std::iter::once(ch));
228	Key { text: Some(ch.to_string()), shift, ..Key::named(lower) }
229}
230
231/// One UTF-8 character at the start of `b`: its length and the character
232/// (`None` for an invalid byte), or `None` when it is incomplete.
233fn utf8(b: &[u8], flush: bool) -> Option<(usize, Option<char>)> {
234	let len = match b[0] {
235		0x00..=0x7f => 1,
236		0xc0..=0xdf => 2,
237		0xe0..=0xef => 3,
238		0xf0..=0xf7 => 4,
239		_ => return Some((1, None)),
240	};
241	if b.len() < len {
242		return if flush { Some((b.len(), None)) } else { None };
243	}
244	match std::str::from_utf8(&b[..len]) {
245		Ok(s) => Some((len, s.chars().next())),
246		Err(_) => Some((1, None)),
247	}
248}
249
250/// A sequence starting with `ESC`.
251fn escape(b: &[u8], flush: bool) -> Step {
252	let Some(&next) = b.get(1) else {
253		return if flush {
254			Step::Took(1, Some(Key::named("escape")))
255		} else {
256			Step::Wait
257		};
258	};
259	match next {
260		b'[' => csi(b, flush),
261		b'O' => match b.get(2) {
262			Some(&c) => Step::Took(3, ss3(c)),
263			None if flush => Step::Took(2, None),
264			None => Step::Wait,
265		},
266		0x1b => match escape(&b[1..], flush) {
267			Step::Wait => Step::Wait,
268			Step::Took(n, key) => Step::Took(n + 1, key.map(|k| Key { alt: true, text: None, ..k })),
269		},
270		_ => match decode(&b[1..], flush) {
271			Step::Wait => Step::Wait,
272			Step::Took(n, key) => Step::Took(n + 1, key.map(|k| Key { alt: true, text: None, ..k })),
273		},
274	}
275}
276
277/// An SS3 key (`ESC O <c>`).
278fn ss3(c: u8) -> Option<Key> {
279	let name = match c {
280		b'A' => "up",
281		b'B' => "down",
282		b'C' => "right",
283		b'D' => "left",
284		b'H' => "home",
285		b'F' => "end",
286		b'E' => "begin",
287		b'P' => "f1",
288		b'Q' => "f2",
289		b'R' => "f3",
290		b'S' => "f4",
291		b'M' => "enter",
292		_ => return None,
293	};
294	Some(Key::named(name))
295}
296
297/// A CSI sequence (`ESC [ … final`).
298fn csi(b: &[u8], flush: bool) -> Step {
299	let mut i = 2;
300	while let Some(&c) = b.get(i) {
301		match c {
302			0x20..=0x3f => i += 1,
303			0x40..=0x7e => return Step::Took(i + 1, csi_key(&b[2..i], c)),
304			_ => return Step::Took(i, None),
305		}
306	}
307	if flush {
308		Step::Took(b.len(), None)
309	} else {
310		Step::Wait
311	}
312}
313
314/// Parses `a;b:c;d` into numeric fields (each a list of `:` parts).
315fn fields(params: &[u8]) -> Option<Vec<Vec<u32>>> {
316	let text = std::str::from_utf8(params).ok()?;
317	text
318		.split(';')
319		.map(|f| {
320			f.split(':')
321				.map(|n| {
322					if n.is_empty() {
323						Some(0)
324					} else {
325						n.parse().ok()
326					}
327				})
328				.collect::<Option<Vec<u32>>>()
329		})
330		.collect()
331}
332
333/// The key of a CSI sequence with parameters `params` and final byte `fin`.
334fn csi_key(params: &[u8], fin: u8) -> Option<Key> {
335	if params
336		.first()
337		.is_some_and(|&p| matches!(p, b'?' | b'<' | b'>' | b'='))
338	{
339		return None;
340	}
341	let f = fields(params)?;
342	let num = |i: usize| f.get(i).and_then(|v| v.first()).copied().unwrap_or(0);
343	let mods = num(1).max(1);
344	match fin {
345		b'A' | b'B' | b'C' | b'D' | b'H' | b'F' | b'E' | b'P' | b'Q' | b'R' | b'S' => {
346			let name = match fin {
347				b'A' => "up",
348				b'B' => "down",
349				b'C' => "right",
350				b'D' => "left",
351				b'H' => "home",
352				b'F' => "end",
353				b'E' => "begin",
354				b'P' => "f1",
355				b'Q' => "f2",
356				b'R' => "f3",
357				_ => "f4",
358			};
359			Some(Key::named(name).with_mods(mods))
360		},
361		b'Z' => Some(Key { shift: true, ..Key::named("tab") }.with_mods(mods)),
362		b'~' => tilde(num(0)).map(|name| Key::named(name).with_mods(mods)),
363		b'u' => kitty(&f),
364		_ => None,
365	}
366}
367
368/// The name of a `CSI <n> ~` key.
369const fn tilde(n: u32) -> Option<&'static str> {
370	Some(match n {
371		1 | 7 => "home",
372		2 => "insert",
373		3 => "delete",
374		4 | 8 => "end",
375		5 => "page_up",
376		6 => "page_down",
377		11 => "f1",
378		12 => "f2",
379		13 => "f3",
380		14 => "f4",
381		15 => "f5",
382		17 => "f6",
383		18 => "f7",
384		19 => "f8",
385		20 => "f9",
386		21 => "f10",
387		23 => "f11",
388		24 => "f12",
389		25 => "f13",
390		26 => "f14",
391		28 => "f15",
392		29 => "menu",
393		31 => "f17",
394		32 => "f18",
395		33 => "f19",
396		34 => "f20",
397		_ => return None,
398	})
399}
400
401/// A kitty `CSI code[:alt…][;mods[:event]][;text] u` key.
402fn kitty(f: &[Vec<u32>]) -> Option<Key> {
403	let code = f.first().and_then(|v| v.first()).copied()?;
404	let mods_field = f.get(1);
405	let mods = mods_field
406		.and_then(|v| v.first())
407		.copied()
408		.unwrap_or(1)
409		.max(1);
410	// Event type 3 is a release, which is never delivered.
411	if mods_field.and_then(|v| v.get(1)).copied() == Some(3) {
412		return None;
413	}
414	let named = |name: &str| Some(Key::named(name).with_mods(mods));
415	let key = match code {
416		27 => named("escape"),
417		13 => named("enter"),
418		9 => named("tab"),
419		127 | 8 => named("backspace"),
420		32 => named("space"),
421		57358 => named("caps_lock"),
422		57359 => named("scroll_lock"),
423		57360 => named("num_lock"),
424		57361 => named("print_screen"),
425		57362 => named("pause"),
426		57363 => named("menu"),
427		57364..=57398 => {
428			let name = format!("f{}", code - 57364 + 1);
429			Some(Key::named(name).with_mods(mods))
430		},
431		57399..=57427 => keypad(code).map(|name| Key::named(name).with_mods(mods)),
432		// Unmapped functional keys, media keys and lone modifiers are not
433		// 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}