|
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)]
|
|
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 |
+ |
|