keys.rs

  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}