1/// The mappings defined in this file where created from reading the alacritty source
2use alacritty_terminal::term::TermMode;
3use gpui::keymap::Keystroke;
4
5#[derive(Debug, PartialEq, Eq)]
6pub enum Modifiers {
7 None,
8 Alt,
9 Ctrl,
10 Shift,
11 CtrlShift,
12 Other,
13}
14
15impl Modifiers {
16 fn new(ks: &Keystroke) -> Self {
17 match (ks.alt, ks.ctrl, ks.shift, ks.cmd) {
18 (false, false, false, false) => Modifiers::None,
19 (true, false, false, false) => Modifiers::Alt,
20 (false, true, false, false) => Modifiers::Ctrl,
21 (false, false, true, false) => Modifiers::Shift,
22 (false, true, true, false) => Modifiers::CtrlShift,
23 _ => Modifiers::Other,
24 }
25 }
26
27 fn any(&self) -> bool {
28 match &self {
29 Modifiers::None => false,
30 Modifiers::Alt => true,
31 Modifiers::Ctrl => true,
32 Modifiers::Shift => true,
33 Modifiers::CtrlShift => true,
34 Modifiers::Other => true,
35 }
36 }
37}
38
39///This function checks if to_esc_str would work, assuming all terminal settings are off.
40///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds.
41///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal
42///lock to determine whether we could actually send the keystroke with the current settings. Therefore,
43///This conservative guess is used instead. Note that in practice the case where this method
44///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes
45///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode.
46///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough
47pub fn might_convert(keystroke: &Keystroke) -> bool {
48 to_esc_str(keystroke, &TermMode::NONE, false).is_some()
49}
50
51pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
52 let modifiers = Modifiers::new(keystroke);
53
54 // Manual Bindings including modifiers
55 let manual_esc_str = match (keystroke.key.as_ref(), &modifiers) {
56 //Basic special keys
57 ("tab", Modifiers::None) => Some("\x09".to_string()),
58 ("escape", Modifiers::None) => Some("\x1b".to_string()),
59 ("enter", Modifiers::None) => Some("\x0d".to_string()),
60 ("enter", Modifiers::Shift) => Some("\x0d".to_string()),
61 ("backspace", Modifiers::None) => Some("\x7f".to_string()),
62 //Interesting escape codes
63 ("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()),
64 ("backspace", Modifiers::Alt) => Some("\x1b\x7f".to_string()),
65 ("backspace", Modifiers::Shift) => Some("\x7f".to_string()),
66 ("home", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
67 Some("\x1b[1;2H".to_string())
68 }
69 ("end", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
70 Some("\x1b[1;2F".to_string())
71 }
72 ("pageup", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
73 Some("\x1b[5;2~".to_string())
74 }
75 ("pagedown", Modifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
76 Some("\x1b[6;2~".to_string())
77 }
78 ("home", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
79 Some("\x1bOH".to_string())
80 }
81 ("home", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
82 Some("\x1b[H".to_string())
83 }
84 ("end", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
85 Some("\x1bOF".to_string())
86 }
87 ("end", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
88 Some("\x1b[F".to_string())
89 }
90 ("up", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
91 Some("\x1bOA".to_string())
92 }
93 ("up", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
94 Some("\x1b[A".to_string())
95 }
96 ("down", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
97 Some("\x1bOB".to_string())
98 }
99 ("down", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
100 Some("\x1b[B".to_string())
101 }
102 ("right", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
103 Some("\x1bOC".to_string())
104 }
105 ("right", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
106 Some("\x1b[C".to_string())
107 }
108 ("left", Modifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
109 Some("\x1bOD".to_string())
110 }
111 ("left", Modifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
112 Some("\x1b[D".to_string())
113 }
114 ("back", Modifiers::None) => Some("\x7f".to_string()),
115 ("insert", Modifiers::None) => Some("\x1b[2~".to_string()),
116 ("delete", Modifiers::None) => Some("\x1b[3~".to_string()),
117 ("pageup", Modifiers::None) => Some("\x1b[5~".to_string()),
118 ("pagedown", Modifiers::None) => Some("\x1b[6~".to_string()),
119 ("f1", Modifiers::None) => Some("\x1bOP".to_string()),
120 ("f2", Modifiers::None) => Some("\x1bOQ".to_string()),
121 ("f3", Modifiers::None) => Some("\x1bOR".to_string()),
122 ("f4", Modifiers::None) => Some("\x1bOS".to_string()),
123 ("f5", Modifiers::None) => Some("\x1b[15~".to_string()),
124 ("f6", Modifiers::None) => Some("\x1b[17~".to_string()),
125 ("f7", Modifiers::None) => Some("\x1b[18~".to_string()),
126 ("f8", Modifiers::None) => Some("\x1b[19~".to_string()),
127 ("f9", Modifiers::None) => Some("\x1b[20~".to_string()),
128 ("f10", Modifiers::None) => Some("\x1b[21~".to_string()),
129 ("f11", Modifiers::None) => Some("\x1b[23~".to_string()),
130 ("f12", Modifiers::None) => Some("\x1b[24~".to_string()),
131 ("f13", Modifiers::None) => Some("\x1b[25~".to_string()),
132 ("f14", Modifiers::None) => Some("\x1b[26~".to_string()),
133 ("f15", Modifiers::None) => Some("\x1b[28~".to_string()),
134 ("f16", Modifiers::None) => Some("\x1b[29~".to_string()),
135 ("f17", Modifiers::None) => Some("\x1b[31~".to_string()),
136 ("f18", Modifiers::None) => Some("\x1b[32~".to_string()),
137 ("f19", Modifiers::None) => Some("\x1b[33~".to_string()),
138 ("f20", Modifiers::None) => Some("\x1b[34~".to_string()),
139 // NumpadEnter, Action::Esc("\n".into());
140 //Mappings for caret notation keys
141 ("a", Modifiers::Ctrl) => Some("\x01".to_string()), //1
142 ("A", Modifiers::CtrlShift) => Some("\x01".to_string()), //1
143 ("b", Modifiers::Ctrl) => Some("\x02".to_string()), //2
144 ("B", Modifiers::CtrlShift) => Some("\x02".to_string()), //2
145 ("c", Modifiers::Ctrl) => Some("\x03".to_string()), //3
146 ("C", Modifiers::CtrlShift) => Some("\x03".to_string()), //3
147 ("d", Modifiers::Ctrl) => Some("\x04".to_string()), //4
148 ("D", Modifiers::CtrlShift) => Some("\x04".to_string()), //4
149 ("e", Modifiers::Ctrl) => Some("\x05".to_string()), //5
150 ("E", Modifiers::CtrlShift) => Some("\x05".to_string()), //5
151 ("f", Modifiers::Ctrl) => Some("\x06".to_string()), //6
152 ("F", Modifiers::CtrlShift) => Some("\x06".to_string()), //6
153 ("g", Modifiers::Ctrl) => Some("\x07".to_string()), //7
154 ("G", Modifiers::CtrlShift) => Some("\x07".to_string()), //7
155 ("h", Modifiers::Ctrl) => Some("\x08".to_string()), //8
156 ("H", Modifiers::CtrlShift) => Some("\x08".to_string()), //8
157 ("i", Modifiers::Ctrl) => Some("\x09".to_string()), //9
158 ("I", Modifiers::CtrlShift) => Some("\x09".to_string()), //9
159 ("j", Modifiers::Ctrl) => Some("\x0a".to_string()), //10
160 ("J", Modifiers::CtrlShift) => Some("\x0a".to_string()), //10
161 ("k", Modifiers::Ctrl) => Some("\x0b".to_string()), //11
162 ("K", Modifiers::CtrlShift) => Some("\x0b".to_string()), //11
163 ("l", Modifiers::Ctrl) => Some("\x0c".to_string()), //12
164 ("L", Modifiers::CtrlShift) => Some("\x0c".to_string()), //12
165 ("m", Modifiers::Ctrl) => Some("\x0d".to_string()), //13
166 ("M", Modifiers::CtrlShift) => Some("\x0d".to_string()), //13
167 ("n", Modifiers::Ctrl) => Some("\x0e".to_string()), //14
168 ("N", Modifiers::CtrlShift) => Some("\x0e".to_string()), //14
169 ("o", Modifiers::Ctrl) => Some("\x0f".to_string()), //15
170 ("O", Modifiers::CtrlShift) => Some("\x0f".to_string()), //15
171 ("p", Modifiers::Ctrl) => Some("\x10".to_string()), //16
172 ("P", Modifiers::CtrlShift) => Some("\x10".to_string()), //16
173 ("q", Modifiers::Ctrl) => Some("\x11".to_string()), //17
174 ("Q", Modifiers::CtrlShift) => Some("\x11".to_string()), //17
175 ("r", Modifiers::Ctrl) => Some("\x12".to_string()), //18
176 ("R", Modifiers::CtrlShift) => Some("\x12".to_string()), //18
177 ("s", Modifiers::Ctrl) => Some("\x13".to_string()), //19
178 ("S", Modifiers::CtrlShift) => Some("\x13".to_string()), //19
179 ("t", Modifiers::Ctrl) => Some("\x14".to_string()), //20
180 ("T", Modifiers::CtrlShift) => Some("\x14".to_string()), //20
181 ("u", Modifiers::Ctrl) => Some("\x15".to_string()), //21
182 ("U", Modifiers::CtrlShift) => Some("\x15".to_string()), //21
183 ("v", Modifiers::Ctrl) => Some("\x16".to_string()), //22
184 ("V", Modifiers::CtrlShift) => Some("\x16".to_string()), //22
185 ("w", Modifiers::Ctrl) => Some("\x17".to_string()), //23
186 ("W", Modifiers::CtrlShift) => Some("\x17".to_string()), //23
187 ("x", Modifiers::Ctrl) => Some("\x18".to_string()), //24
188 ("X", Modifiers::CtrlShift) => Some("\x18".to_string()), //24
189 ("y", Modifiers::Ctrl) => Some("\x19".to_string()), //25
190 ("Y", Modifiers::CtrlShift) => Some("\x19".to_string()), //25
191 ("z", Modifiers::Ctrl) => Some("\x1a".to_string()), //26
192 ("Z", Modifiers::CtrlShift) => Some("\x1a".to_string()), //26
193 ("@", Modifiers::Ctrl) => Some("\x00".to_string()), //0
194 ("[", Modifiers::Ctrl) => Some("\x1b".to_string()), //27
195 ("\\", Modifiers::Ctrl) => Some("\x1c".to_string()), //28
196 ("]", Modifiers::Ctrl) => Some("\x1d".to_string()), //29
197 ("^", Modifiers::Ctrl) => Some("\x1e".to_string()), //30
198 ("_", Modifiers::Ctrl) => Some("\x1f".to_string()), //31
199 ("?", Modifiers::Ctrl) => Some("\x7f".to_string()), //127
200 _ => None,
201 };
202 if manual_esc_str.is_some() {
203 return manual_esc_str;
204 }
205
206 // Automated bindings applying modifiers
207 if modifiers.any() {
208 let modifier_code = modifier_code(keystroke);
209 let modified_esc_str = match keystroke.key.as_ref() {
210 "up" => Some(format!("\x1b[1;{}A", modifier_code)),
211 "down" => Some(format!("\x1b[1;{}B", modifier_code)),
212 "right" => Some(format!("\x1b[1;{}C", modifier_code)),
213 "left" => Some(format!("\x1b[1;{}D", modifier_code)),
214 "f1" => Some(format!("\x1b[1;{}P", modifier_code)),
215 "f2" => Some(format!("\x1b[1;{}Q", modifier_code)),
216 "f3" => Some(format!("\x1b[1;{}R", modifier_code)),
217 "f4" => Some(format!("\x1b[1;{}S", modifier_code)),
218 "F5" => Some(format!("\x1b[15;{}~", modifier_code)),
219 "f6" => Some(format!("\x1b[17;{}~", modifier_code)),
220 "f7" => Some(format!("\x1b[18;{}~", modifier_code)),
221 "f8" => Some(format!("\x1b[19;{}~", modifier_code)),
222 "f9" => Some(format!("\x1b[20;{}~", modifier_code)),
223 "f10" => Some(format!("\x1b[21;{}~", modifier_code)),
224 "f11" => Some(format!("\x1b[23;{}~", modifier_code)),
225 "f12" => Some(format!("\x1b[24;{}~", modifier_code)),
226 "f13" => Some(format!("\x1b[25;{}~", modifier_code)),
227 "f14" => Some(format!("\x1b[26;{}~", modifier_code)),
228 "f15" => Some(format!("\x1b[28;{}~", modifier_code)),
229 "f16" => Some(format!("\x1b[29;{}~", modifier_code)),
230 "f17" => Some(format!("\x1b[31;{}~", modifier_code)),
231 "f18" => Some(format!("\x1b[32;{}~", modifier_code)),
232 "f19" => Some(format!("\x1b[33;{}~", modifier_code)),
233 "f20" => Some(format!("\x1b[34;{}~", modifier_code)),
234 _ if modifier_code == 2 => None,
235 "insert" => Some(format!("\x1b[2;{}~", modifier_code)),
236 "pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
237 "pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
238 "end" => Some(format!("\x1b[1;{}F", modifier_code)),
239 "home" => Some(format!("\x1b[1;{}H", modifier_code)),
240 _ => None,
241 };
242 if modified_esc_str.is_some() {
243 return modified_esc_str;
244 }
245 }
246
247 let alt_meta_binding = if alt_is_meta && modifiers == Modifiers::Alt && keystroke.key.is_ascii()
248 {
249 Some(format!("\x1b{}", keystroke.key))
250 } else {
251 None
252 };
253
254 if alt_meta_binding.is_some() {
255 return alt_meta_binding;
256 }
257
258 None
259}
260
261/// Code Modifiers
262/// ---------+---------------------------
263/// 2 | Shift
264/// 3 | Alt
265/// 4 | Shift + Alt
266/// 5 | Control
267/// 6 | Shift + Control
268/// 7 | Alt + Control
269/// 8 | Shift + Alt + Control
270/// ---------+---------------------------
271/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
272fn modifier_code(keystroke: &Keystroke) -> u32 {
273 let mut modifier_code = 0;
274 if keystroke.shift {
275 modifier_code |= 1;
276 }
277 if keystroke.alt {
278 modifier_code |= 1 << 1;
279 }
280 if keystroke.ctrl {
281 modifier_code |= 1 << 2;
282 }
283 modifier_code + 1
284}
285
286#[cfg(test)]
287mod test {
288 use super::*;
289
290 #[test]
291 fn test_scroll_keys() {
292 //These keys should be handled by the scrolling element directly
293 //Need to signify this by returning 'None'
294 let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
295 let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
296 let shift_home = Keystroke::parse("shift-home").unwrap();
297 let shift_end = Keystroke::parse("shift-end").unwrap();
298
299 let none = TermMode::NONE;
300 assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
301 assert_eq!(to_esc_str(&shift_pagedown, &none, false), None);
302 assert_eq!(to_esc_str(&shift_home, &none, false), None);
303 assert_eq!(to_esc_str(&shift_end, &none, false), None);
304
305 let alt_screen = TermMode::ALT_SCREEN;
306 assert_eq!(
307 to_esc_str(&shift_pageup, &alt_screen, false),
308 Some("\x1b[5;2~".to_string())
309 );
310 assert_eq!(
311 to_esc_str(&shift_pagedown, &alt_screen, false),
312 Some("\x1b[6;2~".to_string())
313 );
314 assert_eq!(
315 to_esc_str(&shift_home, &alt_screen, false),
316 Some("\x1b[1;2H".to_string())
317 );
318 assert_eq!(
319 to_esc_str(&shift_end, &alt_screen, false),
320 Some("\x1b[1;2F".to_string())
321 );
322
323 let pageup = Keystroke::parse("pageup").unwrap();
324 let pagedown = Keystroke::parse("pagedown").unwrap();
325 let any = TermMode::ANY;
326
327 assert_eq!(
328 to_esc_str(&pageup, &any, false),
329 Some("\x1b[5~".to_string())
330 );
331 assert_eq!(
332 to_esc_str(&pagedown, &any, false),
333 Some("\x1b[6~".to_string())
334 );
335 }
336
337 #[test]
338 fn test_plain_inputs() {
339 let ks = Keystroke {
340 ctrl: false,
341 alt: false,
342 shift: false,
343 cmd: false,
344 function: false,
345 key: "🖖🏻".to_string(), //2 char string
346 };
347 assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
348 }
349
350 #[test]
351 fn test_application_mode() {
352 let app_cursor = TermMode::APP_CURSOR;
353 let none = TermMode::NONE;
354
355 let up = Keystroke::parse("up").unwrap();
356 let down = Keystroke::parse("down").unwrap();
357 let left = Keystroke::parse("left").unwrap();
358 let right = Keystroke::parse("right").unwrap();
359
360 assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".to_string()));
361 assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".to_string()));
362 assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".to_string()));
363 assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".to_string()));
364
365 assert_eq!(
366 to_esc_str(&up, &app_cursor, false),
367 Some("\x1bOA".to_string())
368 );
369 assert_eq!(
370 to_esc_str(&down, &app_cursor, false),
371 Some("\x1bOB".to_string())
372 );
373 assert_eq!(
374 to_esc_str(&right, &app_cursor, false),
375 Some("\x1bOC".to_string())
376 );
377 assert_eq!(
378 to_esc_str(&left, &app_cursor, false),
379 Some("\x1bOD".to_string())
380 );
381 }
382
383 #[test]
384 fn test_ctrl_codes() {
385 let letters_lower = 'a'..='z';
386 let letters_upper = 'A'..='Z';
387 let mode = TermMode::ANY;
388
389 for (lower, upper) in letters_lower.zip(letters_upper) {
390 assert_eq!(
391 to_esc_str(
392 &Keystroke::parse(&format!("ctrl-{}", lower)).unwrap(),
393 &mode,
394 false
395 ),
396 to_esc_str(
397 &Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
398 &mode,
399 false
400 ),
401 "On letter: {}/{}",
402 lower,
403 upper
404 )
405 }
406 }
407
408 #[test]
409 fn alt_is_meta() {
410 let ascii_printable = ' '..='~';
411 for character in ascii_printable {
412 assert_eq!(
413 to_esc_str(
414 &Keystroke::parse(&format!("alt-{}", character)).unwrap(),
415 &TermMode::NONE,
416 true
417 )
418 .unwrap(),
419 format!("\x1b{}", character)
420 );
421 }
422
423 let gpui_keys = [
424 "up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9",
425 "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert",
426 "pageup", "pagedown", "end", "home",
427 ];
428
429 for key in gpui_keys {
430 assert_ne!(
431 to_esc_str(
432 &Keystroke::parse(&format!("alt-{}", key)).unwrap(),
433 &TermMode::NONE,
434 true
435 )
436 .unwrap(),
437 format!("\x1b{}", key)
438 );
439 }
440 }
441
442 #[test]
443 fn test_modifier_code_calc() {
444 // Code Modifiers
445 // ---------+---------------------------
446 // 2 | Shift
447 // 3 | Alt
448 // 4 | Shift + Alt
449 // 5 | Control
450 // 6 | Shift + Control
451 // 7 | Alt + Control
452 // 8 | Shift + Alt + Control
453 // ---------+---------------------------
454 // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
455 assert_eq!(2, modifier_code(&Keystroke::parse("shift-A").unwrap()));
456 assert_eq!(3, modifier_code(&Keystroke::parse("alt-A").unwrap()));
457 assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-A").unwrap()));
458 assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-A").unwrap()));
459 assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-A").unwrap()));
460 assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-A").unwrap()));
461 assert_eq!(
462 8,
463 modifier_code(&Keystroke::parse("shift-ctrl-alt-A").unwrap())
464 );
465 }
466}