Skip to main content

max / shop

18.1 KB · 529 lines History Blame Raw
1 //! Key events to the bytes a terminal program expects.
2 //!
3 //! The compositor hands over a keysym and, for anything printable, the UTF-8
4 //! xkbcommon already produced for it. Turning that into what the program on
5 //! the other end of the PTY is waiting for is the job here, and it is entirely
6 //! a matter of convention: there is no standard, only what xterm does and what
7 //! every terminfo entry has recorded about it since.
8 //!
9 //! shop advertises `xterm-256color`, so xterm is the contract. Where xterm is
10 //! self-inconsistent this crate follows what the terminfo entry claims,
11 //! because that is what programs actually read.
12 //!
13 //! # Why this is a crate
14 //!
15 //! No Wayland here, and deliberately: the encoding is a pure function of
16 //! keysym, modifiers, and the two DEC modes, so it is testable without a
17 //! compositor, a seat, or a window. [`xkeysym`] is the only dependency, and
18 //! SCTK re-exports the same [`Keysym`] type, so the binary passes its keysyms
19 //! straight through.
20 //!
21 //! Keymap loading, modifier tracking and key repeat are not here. SCTK already
22 //! runs libxkbcommon for all three, and a second implementation would be a
23 //! second set of bugs about dead keys and layout switching.
24
25 use xkeysym::Keysym;
26
27 /// Modifier state at the moment of the press.
28 ///
29 /// Named for what they do rather than for the keycaps: `logo` is what
30 /// Wayland calls the Windows/Command key, and terminals call meta.
31 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
32 pub struct Mods {
33 pub shift: bool,
34 pub alt: bool,
35 pub ctrl: bool,
36 pub logo: bool,
37 }
38
39 impl Mods {
40 /// The xterm modifier number: `1 + shift + 2*alt + 4*ctrl + 8*meta`.
41 ///
42 /// 1 means "no modifiers", which is why the modified and unmodified forms
43 /// of a key are different sequences rather than the same one with a
44 /// parameter of zero.
45 fn xterm_number(self) -> u8 {
46 1 + u8::from(self.shift)
47 + 2 * u8::from(self.alt)
48 + 4 * u8::from(self.ctrl)
49 + 8 * u8::from(self.logo)
50 }
51
52 fn none(self) -> bool {
53 self == Self::default()
54 }
55 }
56
57 /// The DEC modes that change what a key sends.
58 ///
59 /// Both are set by the program, not the user, and both are the reason a key
60 /// cannot be encoded from the event alone.
61 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
62 pub struct Modes {
63 /// DECCKM (`CSI ? 1 h`). Cursor keys send `SS3 A` instead of `CSI A`.
64 /// vim and readline both turn this on, and a terminal that ignores it
65 /// gives you `[A` in the buffer instead of moving the cursor.
66 pub cursor_keys_application: bool,
67 /// DECKPAM (`ESC =`). The keypad sends its own function sequences rather
68 /// than digits.
69 pub keypad_application: bool,
70 }
71
72 /// Encode one key press.
73 ///
74 /// `utf8` is what xkbcommon resolved the key to, if anything — already
75 /// layout-aware and already carrying the control character for Ctrl+letter.
76 /// Empty for keys with no printable form.
77 ///
78 /// Returns an empty vector for keys that send nothing: modifiers themselves,
79 /// and anything the layout gave no text and this table no sequence.
80 #[must_use]
81 pub fn encode(keysym: Keysym, utf8: &str, mods: Mods, modes: Modes) -> Vec<u8> {
82 if let Some(bytes) = encode_named(keysym, mods, modes) {
83 return bytes;
84 }
85 if modes.keypad_application
86 && let Some(bytes) = encode_keypad_application(keysym, mods)
87 {
88 return bytes;
89 }
90 encode_text(keysym, utf8, mods)
91 }
92
93 /// Keys whose sequence does not depend on the layout.
94 fn encode_named(keysym: Keysym, mods: Mods, modes: Modes) -> Option<Vec<u8>> {
95 let m = mods.xterm_number();
96 // Cursor and editing keys. The application-mode form only applies
97 // unmodified: xterm sends the CSI form the moment a modifier is involved,
98 // because the parameter has nowhere to go in the SS3 form.
99 let cursor = |final_byte: u8| -> Vec<u8> {
100 if m == 1 {
101 let introducer = if modes.cursor_keys_application {
102 b'O'
103 } else {
104 b'['
105 };
106 vec![0x1b, introducer, final_byte]
107 } else {
108 format!("\x1b[1;{m}{}", final_byte as char).into_bytes()
109 }
110 };
111 let tilde = |code: u8| -> Vec<u8> {
112 if m == 1 {
113 format!("\x1b[{code}~").into_bytes()
114 } else {
115 format!("\x1b[{code};{m}~").into_bytes()
116 }
117 };
118
119 let bytes = match keysym {
120 Keysym::Up => cursor(b'A'),
121 Keysym::Down => cursor(b'B'),
122 Keysym::Right => cursor(b'C'),
123 Keysym::Left => cursor(b'D'),
124 Keysym::Home => cursor(b'H'),
125 Keysym::End => cursor(b'F'),
126
127 Keysym::Insert => tilde(2),
128 Keysym::Delete => tilde(3),
129 Keysym::Page_Up => tilde(5),
130 Keysym::Page_Down => tilde(6),
131
132 // F1-F4 are SS3 keys unmodified and CSI keys otherwise, which is the
133 // one place xterm's function-key table is not a straight run.
134 Keysym::F1 => function_ss3(b'P', m),
135 Keysym::F2 => function_ss3(b'Q', m),
136 Keysym::F3 => function_ss3(b'R', m),
137 Keysym::F4 => function_ss3(b'S', m),
138 Keysym::F5 => tilde(15),
139 // 16 is skipped by the convention, not by an oversight here.
140 Keysym::F6 => tilde(17),
141 Keysym::F7 => tilde(18),
142 Keysym::F8 => tilde(19),
143 Keysym::F9 => tilde(20),
144 Keysym::F10 => tilde(21),
145 // 22 skipped as well.
146 Keysym::F11 => tilde(23),
147 Keysym::F12 => tilde(24),
148
149 // Shift+Tab. xkb reports it as its own keysym rather than as Tab with
150 // a shift modifier, so it never reaches the text path.
151 Keysym::ISO_Left_Tab => b"\x1b[Z".to_vec(),
152 Keysym::Tab if mods.shift => b"\x1b[Z".to_vec(),
153 Keysym::Tab if mods.alt => b"\x1b\t".to_vec(),
154 Keysym::Tab => b"\t".to_vec(),
155
156 Keysym::KP_Enter if modes.keypad_application => b"\x1bOM".to_vec(),
157 Keysym::Return | Keysym::KP_Enter if mods.alt => b"\x1b\r".to_vec(),
158 Keysym::Return | Keysym::KP_Enter => b"\r".to_vec(),
159
160 // Backspace sends DEL, and Ctrl+Backspace sends BS. That looks
161 // backwards written down and is what every terminfo entry says, so
162 // readline's ^H binding is the one that has to be reached with Ctrl.
163 Keysym::BackSpace if mods.ctrl => vec![0x08],
164 Keysym::BackSpace if mods.alt => vec![0x1b, 0x7f],
165 Keysym::BackSpace => vec![0x7f],
166
167 Keysym::Escape if mods.alt => vec![0x1b, 0x1b],
168 Keysym::Escape => vec![0x1b],
169
170 // Ctrl+Space is NUL. xkbcommon hands back a plain space for it, so
171 // without this the null byte a few programs still want never arrives.
172 Keysym::space if mods.ctrl && !mods.alt => vec![0x00],
173
174 _ => return None,
175 };
176 Some(bytes)
177 }
178
179 fn function_ss3(final_byte: u8, m: u8) -> Vec<u8> {
180 if m == 1 {
181 vec![0x1b, b'O', final_byte]
182 } else {
183 format!("\x1b[1;{m}{}", final_byte as char).into_bytes()
184 }
185 }
186
187 /// The keypad, when the program has asked for its application form.
188 ///
189 /// Only reached with DECKPAM set; in numeric mode these keys fall through to
190 /// the layout and type their digits.
191 fn encode_keypad_application(keysym: Keysym, mods: Mods) -> Option<Vec<u8>> {
192 // Modified keypad presses have no application form in xterm; they fall
193 // back to the ordinary text path.
194 if !mods.none() {
195 return None;
196 }
197 let final_byte = match keysym {
198 Keysym::KP_Space => b' ',
199 Keysym::KP_Tab => b'I',
200 Keysym::KP_Multiply => b'j',
201 Keysym::KP_Add => b'k',
202 Keysym::KP_Separator => b'l',
203 Keysym::KP_Subtract => b'm',
204 Keysym::KP_Decimal => b'n',
205 Keysym::KP_Divide => b'o',
206 Keysym::KP_0 => b'p',
207 Keysym::KP_1 => b'q',
208 Keysym::KP_2 => b'r',
209 Keysym::KP_3 => b's',
210 Keysym::KP_4 => b't',
211 Keysym::KP_5 | Keysym::KP_Begin => b'u',
212 Keysym::KP_6 => b'v',
213 Keysym::KP_7 => b'w',
214 Keysym::KP_8 => b'x',
215 Keysym::KP_9 => b'y',
216 Keysym::KP_Equal => b'X',
217 _ => return None,
218 };
219 Some(vec![0x1b, b'O', final_byte])
220 }
221
222 /// Everything the layout resolved to text.
223 fn encode_text(keysym: Keysym, utf8: &str, mods: Mods) -> Vec<u8> {
224 if utf8.is_empty() {
225 return Vec::new();
226 }
227 // A bare modifier press reports itself as a keysym with no text on most
228 // layouts, but not all; drop it explicitly so a stray keycap does not type.
229 if is_modifier(keysym) {
230 return Vec::new();
231 }
232 if mods.alt {
233 // xterm's "Alt sends escape". The alternative convention, setting the
234 // high bit, cannot represent anything outside Latin-1 and loses to
235 // UTF-8 the moment the layout is not English.
236 let mut out = Vec::with_capacity(utf8.len() + 1);
237 out.push(0x1b);
238 out.extend_from_slice(utf8.as_bytes());
239 return out;
240 }
241 utf8.as_bytes().to_vec()
242 }
243
244 fn is_modifier(keysym: Keysym) -> bool {
245 matches!(
246 keysym,
247 Keysym::Shift_L
248 | Keysym::Shift_R
249 | Keysym::Control_L
250 | Keysym::Control_R
251 | Keysym::Alt_L
252 | Keysym::Alt_R
253 | Keysym::Super_L
254 | Keysym::Super_R
255 | Keysym::Meta_L
256 | Keysym::Meta_R
257 | Keysym::Hyper_L
258 | Keysym::Hyper_R
259 | Keysym::Caps_Lock
260 | Keysym::Shift_Lock
261 | Keysym::Num_Lock
262 | Keysym::ISO_Level3_Shift
263 | Keysym::ISO_Level5_Shift
264 )
265 }
266
267 #[cfg(test)]
268 mod tests {
269 use super::*;
270
271 const NONE: Mods = Mods {
272 shift: false,
273 alt: false,
274 ctrl: false,
275 logo: false,
276 };
277 const CTRL: Mods = Mods { ctrl: true, ..NONE };
278 const SHIFT: Mods = Mods {
279 shift: true,
280 ..NONE
281 };
282 const ALT: Mods = Mods { alt: true, ..NONE };
283
284 const NORMAL: Modes = Modes {
285 cursor_keys_application: false,
286 keypad_application: false,
287 };
288 const APP_CURSOR: Modes = Modes {
289 cursor_keys_application: true,
290 keypad_application: false,
291 };
292 const APP_KEYPAD: Modes = Modes {
293 cursor_keys_application: false,
294 keypad_application: true,
295 };
296
297 fn seq(keysym: Keysym, utf8: &str, mods: Mods, modes: Modes) -> String {
298 String::from_utf8(encode(keysym, utf8, mods, modes)).unwrap()
299 }
300
301 // ---- modifier numbering --------------------------------------------
302
303 #[test]
304 fn the_unmodified_number_is_one_not_zero() {
305 assert_eq!(NONE.xterm_number(), 1);
306 }
307
308 #[test]
309 fn modifier_numbers_match_the_xterm_table() {
310 assert_eq!(SHIFT.xterm_number(), 2);
311 assert_eq!(ALT.xterm_number(), 3);
312 assert_eq!(CTRL.xterm_number(), 5);
313 assert_eq!(
314 Mods {
315 shift: true,
316 ctrl: true,
317 ..NONE
318 }
319 .xterm_number(),
320 6
321 );
322 assert_eq!(
323 Mods {
324 shift: true,
325 alt: true,
326 ctrl: true,
327 logo: true
328 }
329 .xterm_number(),
330 16
331 );
332 }
333
334 // ---- cursor keys ----------------------------------------------------
335
336 #[test]
337 fn cursor_keys_are_csi_by_default() {
338 assert_eq!(seq(Keysym::Up, "", NONE, NORMAL), "\x1b[A");
339 assert_eq!(seq(Keysym::Down, "", NONE, NORMAL), "\x1b[B");
340 assert_eq!(seq(Keysym::Right, "", NONE, NORMAL), "\x1b[C");
341 assert_eq!(seq(Keysym::Left, "", NONE, NORMAL), "\x1b[D");
342 }
343
344 #[test]
345 fn application_mode_makes_cursor_keys_ss3() {
346 assert_eq!(seq(Keysym::Up, "", NONE, APP_CURSOR), "\x1bOA");
347 assert_eq!(seq(Keysym::Left, "", NONE, APP_CURSOR), "\x1bOD");
348 }
349
350 #[test]
351 fn a_modified_cursor_key_is_csi_even_in_application_mode() {
352 // The SS3 form has no room for the parameter, so xterm drops back to
353 // CSI. A terminal that keeps SS3 here sends vim something it reads as
354 // a bare Escape followed by garbage.
355 assert_eq!(seq(Keysym::Up, "", CTRL, APP_CURSOR), "\x1b[1;5A");
356 assert_eq!(seq(Keysym::Right, "", SHIFT, APP_CURSOR), "\x1b[1;2C");
357 }
358
359 #[test]
360 fn home_and_end_ride_the_cursor_path() {
361 assert_eq!(seq(Keysym::Home, "", NONE, NORMAL), "\x1b[H");
362 assert_eq!(seq(Keysym::End, "", NONE, NORMAL), "\x1b[F");
363 assert_eq!(seq(Keysym::Home, "", NONE, APP_CURSOR), "\x1bOH");
364 assert_eq!(seq(Keysym::End, "", CTRL, NORMAL), "\x1b[1;5F");
365 }
366
367 // ---- editing keys ---------------------------------------------------
368
369 #[test]
370 fn editing_keys_use_the_tilde_form() {
371 assert_eq!(seq(Keysym::Insert, "", NONE, NORMAL), "\x1b[2~");
372 assert_eq!(seq(Keysym::Delete, "", NONE, NORMAL), "\x1b[3~");
373 assert_eq!(seq(Keysym::Page_Up, "", NONE, NORMAL), "\x1b[5~");
374 assert_eq!(seq(Keysym::Page_Down, "", NONE, NORMAL), "\x1b[6~");
375 }
376
377 #[test]
378 fn a_modified_editing_key_carries_its_number() {
379 assert_eq!(seq(Keysym::Delete, "", CTRL, NORMAL), "\x1b[3;5~");
380 assert_eq!(seq(Keysym::Page_Up, "", SHIFT, NORMAL), "\x1b[5;2~");
381 }
382
383 // ---- function keys --------------------------------------------------
384
385 #[test]
386 fn the_first_four_function_keys_are_ss3() {
387 assert_eq!(seq(Keysym::F1, "", NONE, NORMAL), "\x1bOP");
388 assert_eq!(seq(Keysym::F2, "", NONE, NORMAL), "\x1bOQ");
389 assert_eq!(seq(Keysym::F3, "", NONE, NORMAL), "\x1bOR");
390 assert_eq!(seq(Keysym::F4, "", NONE, NORMAL), "\x1bOS");
391 }
392
393 #[test]
394 fn modified_f1_to_f4_become_csi() {
395 assert_eq!(seq(Keysym::F1, "", SHIFT, NORMAL), "\x1b[1;2P");
396 assert_eq!(seq(Keysym::F4, "", CTRL, NORMAL), "\x1b[1;5S");
397 }
398
399 #[test]
400 fn the_rest_are_tilde_keys_with_the_gaps_the_convention_has() {
401 assert_eq!(seq(Keysym::F5, "", NONE, NORMAL), "\x1b[15~");
402 assert_eq!(seq(Keysym::F6, "", NONE, NORMAL), "\x1b[17~");
403 assert_eq!(seq(Keysym::F10, "", NONE, NORMAL), "\x1b[21~");
404 assert_eq!(seq(Keysym::F11, "", NONE, NORMAL), "\x1b[23~");
405 assert_eq!(seq(Keysym::F12, "", NONE, NORMAL), "\x1b[24~");
406 }
407
408 // ---- tab, return, backspace, escape ---------------------------------
409
410 #[test]
411 fn shift_tab_is_backtab_by_either_route() {
412 // xkb usually reports its own keysym, but not every layout does.
413 assert_eq!(seq(Keysym::ISO_Left_Tab, "", SHIFT, NORMAL), "\x1b[Z");
414 assert_eq!(seq(Keysym::Tab, "\t", SHIFT, NORMAL), "\x1b[Z");
415 }
416
417 #[test]
418 fn plain_tab_is_a_tab() {
419 assert_eq!(seq(Keysym::Tab, "\t", NONE, NORMAL), "\t");
420 }
421
422 #[test]
423 fn return_is_carriage_return_not_line_feed() {
424 assert_eq!(seq(Keysym::Return, "\r", NONE, NORMAL), "\r");
425 assert_eq!(seq(Keysym::KP_Enter, "", NONE, NORMAL), "\r");
426 }
427
428 #[test]
429 fn keypad_enter_has_an_application_form() {
430 assert_eq!(seq(Keysym::KP_Enter, "", NONE, APP_KEYPAD), "\x1bOM");
431 }
432
433 #[test]
434 fn backspace_sends_del_and_ctrl_backspace_sends_bs() {
435 assert_eq!(encode(Keysym::BackSpace, "", NONE, NORMAL), vec![0x7f]);
436 assert_eq!(encode(Keysym::BackSpace, "", CTRL, NORMAL), vec![0x08]);
437 assert_eq!(encode(Keysym::BackSpace, "", ALT, NORMAL), vec![0x1b, 0x7f]);
438 }
439
440 #[test]
441 fn ctrl_space_is_a_null_byte() {
442 // xkbcommon hands back a plain space here, so the table has to.
443 assert_eq!(encode(Keysym::space, " ", CTRL, NORMAL), vec![0x00]);
444 assert_eq!(encode(Keysym::space, " ", NONE, NORMAL), b" ".to_vec());
445 }
446
447 #[test]
448 fn alt_escape_is_two_escapes() {
449 assert_eq!(encode(Keysym::Escape, "", NONE, NORMAL), vec![0x1b]);
450 assert_eq!(encode(Keysym::Escape, "", ALT, NORMAL), vec![0x1b, 0x1b]);
451 }
452
453 // ---- the keypad ------------------------------------------------------
454
455 #[test]
456 fn the_keypad_types_digits_in_numeric_mode() {
457 assert_eq!(seq(Keysym::KP_1, "1", NONE, NORMAL), "1");
458 assert_eq!(seq(Keysym::KP_Add, "+", NONE, NORMAL), "+");
459 }
460
461 #[test]
462 fn the_keypad_has_its_own_sequences_in_application_mode() {
463 assert_eq!(seq(Keysym::KP_0, "0", NONE, APP_KEYPAD), "\x1bOp");
464 assert_eq!(seq(Keysym::KP_9, "9", NONE, APP_KEYPAD), "\x1bOy");
465 assert_eq!(seq(Keysym::KP_Add, "+", NONE, APP_KEYPAD), "\x1bOk");
466 assert_eq!(seq(Keysym::KP_Divide, "/", NONE, APP_KEYPAD), "\x1bOo");
467 }
468
469 #[test]
470 fn the_centre_key_is_the_same_sequence_under_either_keysym() {
471 // Num Lock decides whether the 5 key reports KP_5 or KP_Begin.
472 assert_eq!(seq(Keysym::KP_5, "5", NONE, APP_KEYPAD), "\x1bOu");
473 assert_eq!(seq(Keysym::KP_Begin, "", NONE, APP_KEYPAD), "\x1bOu");
474 }
475
476 #[test]
477 fn a_modified_keypad_press_falls_back_to_its_text() {
478 assert_eq!(seq(Keysym::KP_1, "1", CTRL, APP_KEYPAD), "1");
479 }
480
481 // ---- text ------------------------------------------------------------
482
483 #[test]
484 fn printable_text_passes_through() {
485 assert_eq!(seq(Keysym::a, "a", NONE, NORMAL), "a");
486 assert_eq!(seq(Keysym::A, "A", SHIFT, NORMAL), "A");
487 }
488
489 #[test]
490 fn xkb_owns_the_control_characters() {
491 // Ctrl+C arrives already folded to 0x03; recomputing it here would
492 // mean disagreeing with the layout about what the key is.
493 assert_eq!(encode(Keysym::c, "\x03", CTRL, NORMAL), vec![0x03]);
494 }
495
496 #[test]
497 fn alt_prefixes_an_escape() {
498 assert_eq!(seq(Keysym::b, "b", ALT, NORMAL), "\x1bb");
499 }
500
501 #[test]
502 fn a_non_ascii_layout_survives_alt() {
503 assert_eq!(seq(Keysym::adiaeresis, "ä", ALT, NORMAL), "\x1bä");
504 }
505
506 #[test]
507 fn keys_with_no_text_and_no_sequence_send_nothing() {
508 assert!(encode(Keysym::Menu, "", NONE, NORMAL).is_empty());
509 assert!(encode(Keysym::Print, "", NONE, NORMAL).is_empty());
510 }
511
512 #[test]
513 fn modifier_keys_never_type() {
514 for keysym in [
515 Keysym::Shift_L,
516 Keysym::Control_R,
517 Keysym::Alt_L,
518 Keysym::Super_L,
519 Keysym::Caps_Lock,
520 Keysym::ISO_Level3_Shift,
521 ] {
522 assert!(
523 encode(keysym, " ", NONE, NORMAL).is_empty(),
524 "{keysym:?} typed something"
525 );
526 }
527 }
528 }
529