From 08029c4aa7e54e7a874915dce82b4a2d31228ef7 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 2 Oct 2026 16:36:22 +0200 Subject: [PATCH] Upstream zed-industries/zed state of the touched files before 41f88af76a --- crates/terminal/src/mappings/keys.rs | 442 +++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 crates/terminal/src/mappings/keys.rs diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs new file mode 100644 index 0000000..03152a4 --- /dev/null +++ b/crates/terminal/src/mappings/keys.rs @@ -0,0 +1,442 @@ +use std::borrow::Cow; + +/// The mappings defined in this file where created from reading the alacritty source +use gpui::Keystroke; + +use crate::Modes; + +#[derive(Debug, PartialEq, Eq)] +enum TerminalModifiers { + None, + Alt, + Ctrl, + Shift, + CtrlShift, + Other, +} + +impl TerminalModifiers { + fn new(ks: &Keystroke) -> Self { + match ( + ks.modifiers.alt, + ks.modifiers.control, + ks.modifiers.shift, + ks.modifiers.platform, + ) { + (false, false, false, false) => TerminalModifiers::None, + (true, false, false, false) => TerminalModifiers::Alt, + (false, true, false, false) => TerminalModifiers::Ctrl, + (false, false, true, false) => TerminalModifiers::Shift, + (false, true, true, false) => TerminalModifiers::CtrlShift, + _ => TerminalModifiers::Other, + } + } + + fn any(&self) -> bool { + match &self { + TerminalModifiers::None => false, + TerminalModifiers::Alt => true, + TerminalModifiers::Ctrl => true, + TerminalModifiers::Shift => true, + TerminalModifiers::CtrlShift => true, + TerminalModifiers::Other => true, + } + } +} + +pub(crate) fn to_esc_str( + keystroke: &Keystroke, + mode: Modes, + option_as_meta: bool, +) -> Option> { + let modifiers = TerminalModifiers::new(keystroke); + + // Manual Bindings including modifiers + let manual_esc_str: Option<&'static str> = match (keystroke.key.as_ref(), &modifiers) { + //Basic special keys + ("tab", TerminalModifiers::None) => Some("\x09"), + ("escape", TerminalModifiers::None) => Some("\x1b"), + ("enter", TerminalModifiers::None) => Some("\x0d"), + ("enter", TerminalModifiers::Shift) => Some("\x0a"), + ("enter", TerminalModifiers::Alt) => Some("\x1b\x0d"), + ("backspace", TerminalModifiers::None) => Some("\x7f"), + //Interesting escape codes + ("tab", TerminalModifiers::Shift) => Some("\x1b[Z"), + ("backspace", TerminalModifiers::Ctrl) => Some("\x08"), + ("backspace", TerminalModifiers::Alt) => Some("\x1b\x7f"), + ("backspace", TerminalModifiers::Shift) => Some("\x7f"), + ("space", TerminalModifiers::Ctrl) => Some("\x00"), + ("home", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOH"), + ("home", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[H"), + ("end", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOF"), + ("end", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[F"), + ("up", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOA"), + ("up", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[A"), + ("down", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOB"), + ("down", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[B"), + ("right", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOC"), + ("right", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[C"), + ("left", TerminalModifiers::None) if mode.contains(Modes::APP_CURSOR) => Some("\x1bOD"), + ("left", TerminalModifiers::None) if !mode.contains(Modes::APP_CURSOR) => Some("\x1b[D"), + ("back", TerminalModifiers::None) => Some("\x7f"), + ("insert", TerminalModifiers::None) => Some("\x1b[2~"), + ("delete", TerminalModifiers::None) => Some("\x1b[3~"), + ("pageup", TerminalModifiers::None) => Some("\x1b[5~"), + ("pagedown", TerminalModifiers::None) => Some("\x1b[6~"), + ("f1", TerminalModifiers::None) => Some("\x1bOP"), + ("f2", TerminalModifiers::None) => Some("\x1bOQ"), + ("f3", TerminalModifiers::None) => Some("\x1bOR"), + ("f4", TerminalModifiers::None) => Some("\x1bOS"), + ("f5", TerminalModifiers::None) => Some("\x1b[15~"), + ("f6", TerminalModifiers::None) => Some("\x1b[17~"), + ("f7", TerminalModifiers::None) => Some("\x1b[18~"), + ("f8", TerminalModifiers::None) => Some("\x1b[19~"), + ("f9", TerminalModifiers::None) => Some("\x1b[20~"), + ("f10", TerminalModifiers::None) => Some("\x1b[21~"), + ("f11", TerminalModifiers::None) => Some("\x1b[23~"), + ("f12", TerminalModifiers::None) => Some("\x1b[24~"), + ("f13", TerminalModifiers::None) => Some("\x1b[25~"), + ("f14", TerminalModifiers::None) => Some("\x1b[26~"), + ("f15", TerminalModifiers::None) => Some("\x1b[28~"), + ("f16", TerminalModifiers::None) => Some("\x1b[29~"), + ("f17", TerminalModifiers::None) => Some("\x1b[31~"), + ("f18", TerminalModifiers::None) => Some("\x1b[32~"), + ("f19", TerminalModifiers::None) => Some("\x1b[33~"), + ("f20", TerminalModifiers::None) => Some("\x1b[34~"), + // NumpadEnter, Action::Esc("\n".into()); + //Mappings for caret notation keys + ("a", TerminalModifiers::Ctrl) => Some("\x01"), //1 + ("A", TerminalModifiers::CtrlShift) => Some("\x01"), //1 + ("b", TerminalModifiers::Ctrl) => Some("\x02"), //2 + ("B", TerminalModifiers::CtrlShift) => Some("\x02"), //2 + ("c", TerminalModifiers::Ctrl) => Some("\x03"), //3 + ("C", TerminalModifiers::CtrlShift) => Some("\x03"), //3 + ("d", TerminalModifiers::Ctrl) => Some("\x04"), //4 + ("D", TerminalModifiers::CtrlShift) => Some("\x04"), //4 + ("e", TerminalModifiers::Ctrl) => Some("\x05"), //5 + ("E", TerminalModifiers::CtrlShift) => Some("\x05"), //5 + ("f", TerminalModifiers::Ctrl) => Some("\x06"), //6 + ("F", TerminalModifiers::CtrlShift) => Some("\x06"), //6 + ("g", TerminalModifiers::Ctrl) => Some("\x07"), //7 + ("G", TerminalModifiers::CtrlShift) => Some("\x07"), //7 + ("h", TerminalModifiers::Ctrl) => Some("\x08"), //8 + ("H", TerminalModifiers::CtrlShift) => Some("\x08"), //8 + ("i", TerminalModifiers::Ctrl) => Some("\x09"), //9 + ("I", TerminalModifiers::CtrlShift) => Some("\x09"), //9 + ("j", TerminalModifiers::Ctrl) => Some("\x0a"), //10 + ("J", TerminalModifiers::CtrlShift) => Some("\x0a"), //10 + ("k", TerminalModifiers::Ctrl) => Some("\x0b"), //11 + ("K", TerminalModifiers::CtrlShift) => Some("\x0b"), //11 + ("l", TerminalModifiers::Ctrl) => Some("\x0c"), //12 + ("L", TerminalModifiers::CtrlShift) => Some("\x0c"), //12 + ("m", TerminalModifiers::Ctrl) => Some("\x0d"), //13 + ("M", TerminalModifiers::CtrlShift) => Some("\x0d"), //13 + ("n", TerminalModifiers::Ctrl) => Some("\x0e"), //14 + ("N", TerminalModifiers::CtrlShift) => Some("\x0e"), //14 + ("o", TerminalModifiers::Ctrl) => Some("\x0f"), //15 + ("O", TerminalModifiers::CtrlShift) => Some("\x0f"), //15 + ("p", TerminalModifiers::Ctrl) => Some("\x10"), //16 + ("P", TerminalModifiers::CtrlShift) => Some("\x10"), //16 + ("q", TerminalModifiers::Ctrl) => Some("\x11"), //17 + ("Q", TerminalModifiers::CtrlShift) => Some("\x11"), //17 + ("r", TerminalModifiers::Ctrl) => Some("\x12"), //18 + ("R", TerminalModifiers::CtrlShift) => Some("\x12"), //18 + ("s", TerminalModifiers::Ctrl) => Some("\x13"), //19 + ("S", TerminalModifiers::CtrlShift) => Some("\x13"), //19 + ("t", TerminalModifiers::Ctrl) => Some("\x14"), //20 + ("T", TerminalModifiers::CtrlShift) => Some("\x14"), //20 + ("u", TerminalModifiers::Ctrl) => Some("\x15"), //21 + ("U", TerminalModifiers::CtrlShift) => Some("\x15"), //21 + ("v", TerminalModifiers::Ctrl) => Some("\x16"), //22 + ("V", TerminalModifiers::CtrlShift) => Some("\x16"), //22 + ("w", TerminalModifiers::Ctrl) => Some("\x17"), //23 + ("W", TerminalModifiers::CtrlShift) => Some("\x17"), //23 + ("x", TerminalModifiers::Ctrl) => Some("\x18"), //24 + ("X", TerminalModifiers::CtrlShift) => Some("\x18"), //24 + ("y", TerminalModifiers::Ctrl) => Some("\x19"), //25 + ("Y", TerminalModifiers::CtrlShift) => Some("\x19"), //25 + ("z", TerminalModifiers::Ctrl) => Some("\x1a"), //26 + ("Z", TerminalModifiers::CtrlShift) => Some("\x1a"), //26 + ("@", TerminalModifiers::Ctrl) => Some("\x00"), //0 + ("[", TerminalModifiers::Ctrl) => Some("\x1b"), //27 + ("\\", TerminalModifiers::Ctrl) => Some("\x1c"), //28 + ("]", TerminalModifiers::Ctrl) => Some("\x1d"), //29 + ("^", TerminalModifiers::Ctrl) => Some("\x1e"), //30 + ("_", TerminalModifiers::Ctrl) => Some("\x1f"), //31 + ("?", TerminalModifiers::Ctrl) => Some("\x7f"), //127 + _ => None, + }; + if let Some(esc_str) = manual_esc_str { + return Some(Cow::Borrowed(esc_str)); + } + + // Automated bindings applying modifiers + if modifiers.any() { + let modifier_code = modifier_code(keystroke); + let modified_esc_str = match keystroke.key.as_ref() { + "up" => Some(format!("\x1b[1;{}A", modifier_code)), + "down" => Some(format!("\x1b[1;{}B", modifier_code)), + "right" => Some(format!("\x1b[1;{}C", modifier_code)), + "left" => Some(format!("\x1b[1;{}D", modifier_code)), + "f1" => Some(format!("\x1b[1;{}P", modifier_code)), + "f2" => Some(format!("\x1b[1;{}Q", modifier_code)), + "f3" => Some(format!("\x1b[1;{}R", modifier_code)), + "f4" => Some(format!("\x1b[1;{}S", modifier_code)), + "f5" => Some(format!("\x1b[15;{}~", modifier_code)), + "f6" => Some(format!("\x1b[17;{}~", modifier_code)), + "f7" => Some(format!("\x1b[18;{}~", modifier_code)), + "f8" => Some(format!("\x1b[19;{}~", modifier_code)), + "f9" => Some(format!("\x1b[20;{}~", modifier_code)), + "f10" => Some(format!("\x1b[21;{}~", modifier_code)), + "f11" => Some(format!("\x1b[23;{}~", modifier_code)), + "f12" => Some(format!("\x1b[24;{}~", modifier_code)), + "f13" => Some(format!("\x1b[25;{}~", modifier_code)), + "f14" => Some(format!("\x1b[26;{}~", modifier_code)), + "f15" => Some(format!("\x1b[28;{}~", modifier_code)), + "f16" => Some(format!("\x1b[29;{}~", modifier_code)), + "f17" => Some(format!("\x1b[31;{}~", modifier_code)), + "f18" => Some(format!("\x1b[32;{}~", modifier_code)), + "f19" => Some(format!("\x1b[33;{}~", modifier_code)), + "f20" => Some(format!("\x1b[34;{}~", modifier_code)), + "insert" => Some(format!("\x1b[2;{}~", modifier_code)), + "pageup" => Some(format!("\x1b[5;{}~", modifier_code)), + "pagedown" => Some(format!("\x1b[6;{}~", modifier_code)), + "end" => Some(format!("\x1b[1;{}F", modifier_code)), + "home" => Some(format!("\x1b[1;{}H", modifier_code)), + _ => None, + }; + if let Some(esc_str) = modified_esc_str { + return Some(Cow::Owned(esc_str)); + } + } + + // Handling alt keys. These work for single ASCII characters. + if (!cfg!(target_os = "macos") || option_as_meta) + && keystroke.modifiers.alt + && keystroke.key.is_ascii() + && keystroke.key.len() == 1 + { + let key = keystroke.key.chars().next().unwrap(); + + if modifiers == TerminalModifiers::Alt { + return Some(Cow::Owned(format!("\x1b{key}"))); + } else if keystroke.modifiers.shift { + return Some(Cow::Owned(format!("\x1b{}", key.to_ascii_uppercase()))); + } else if keystroke.modifiers.control && key.is_ascii_lowercase() { + return Some(Cow::Owned(format!( + "\x1b{}", + (key as u8 - b'a' + 1) as char + ))); + } + } + + None +} + +/// Code Modifiers +/// ---------+--------------------------- +/// 2 | Shift +/// 3 | Alt +/// 4 | Shift + Alt +/// 5 | Control +/// 6 | Shift + Control +/// 7 | Alt + Control +/// 8 | Shift + Alt + Control +/// ---------+--------------------------- +/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys +fn modifier_code(keystroke: &Keystroke) -> u32 { + let mut modifier_code = 0; + if keystroke.modifiers.shift { + modifier_code |= 1; + } + if keystroke.modifiers.alt { + modifier_code |= 1 << 1; + } + if keystroke.modifiers.control { + modifier_code |= 1 << 2; + } + modifier_code + 1 +} + +#[cfg(test)] +mod test { + use gpui::Modifiers; + + use super::*; + + #[test] + fn test_plain_inputs() { + let ks = Keystroke { + modifiers: Modifiers { + control: false, + alt: false, + shift: false, + platform: false, + function: false, + }, + key: "🖖🏻".to_string(), //2 char string + key_char: None, + }; + assert_eq!(to_esc_str(&ks, Modes::NONE, false), None); + } + + #[test] + fn test_application_mode() { + let app_cursor = Modes::APP_CURSOR; + let none = Modes::NONE; + + let up = Keystroke::parse("up").unwrap(); + let down = Keystroke::parse("down").unwrap(); + let left = Keystroke::parse("left").unwrap(); + let right = Keystroke::parse("right").unwrap(); + + assert_eq!(to_esc_str(&up, none, false), Some("\x1b[A".into())); + assert_eq!(to_esc_str(&down, none, false), Some("\x1b[B".into())); + assert_eq!(to_esc_str(&right, none, false), Some("\x1b[C".into())); + assert_eq!(to_esc_str(&left, none, false), Some("\x1b[D".into())); + + assert_eq!(to_esc_str(&up, app_cursor, false), Some("\x1bOA".into())); + assert_eq!(to_esc_str(&down, app_cursor, false), Some("\x1bOB".into())); + assert_eq!(to_esc_str(&right, app_cursor, false), Some("\x1bOC".into())); + assert_eq!(to_esc_str(&left, app_cursor, false), Some("\x1bOD".into())); + + let home = Keystroke::parse("home").unwrap(); + let end = Keystroke::parse("end").unwrap(); + assert_eq!(to_esc_str(&home, none, false), Some("\x1b[H".into())); + assert_eq!(to_esc_str(&end, none, false), Some("\x1b[F".into())); + assert_eq!(to_esc_str(&home, app_cursor, false), Some("\x1bOH".into())); + assert_eq!(to_esc_str(&end, app_cursor, false), Some("\x1bOF".into())); + + let shift_up = Keystroke::parse("shift-up").unwrap(); + let shift_down = Keystroke::parse("shift-down").unwrap(); + let shift_home = Keystroke::parse("shift-home").unwrap(); + let shift_end = Keystroke::parse("shift-end").unwrap(); + assert_eq!(to_esc_str(&shift_up, none, false), Some("\x1b[1;2A".into())); + assert_eq!( + to_esc_str(&shift_down, none, false), + Some("\x1b[1;2B".into()) + ); + assert_eq!( + to_esc_str(&shift_home, none, false), + Some("\x1b[1;2H".into()) + ); + assert_eq!( + to_esc_str(&shift_end, none, false), + Some("\x1b[1;2F".into()) + ); + } + + #[test] + fn test_ctrl_codes() { + let letters_lower = 'a'..='z'; + let letters_upper = 'A'..='Z'; + let mode = Modes::APP_CURSOR; + + for (lower, upper) in letters_lower.zip(letters_upper) { + assert_eq!( + to_esc_str( + &Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(), + mode, + false + ), + to_esc_str( + &Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(), + mode, + false + ), + "On letter: {}/{}", + lower, + upper + ) + } + } + + #[test] + fn alt_is_meta() { + let ascii_printable = ' '..='~'; + for character in ascii_printable { + assert_eq!( + to_esc_str( + &Keystroke::parse(&format!("alt-{}", character)).unwrap(), + Modes::NONE, + true + ) + .unwrap(), + format!("\x1b{}", character) + ); + } + + let gpui_keys = [ + "up", "down", "right", "left", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", + "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert", + "pageup", "pagedown", "end", "home", + ]; + + for key in gpui_keys { + assert_ne!( + to_esc_str( + &Keystroke::parse(&format!("alt-{}", key)).unwrap(), + Modes::NONE, + true + ) + .unwrap(), + format!("\x1b{}", key) + ); + } + } + + #[test] + fn test_ctrl_alt_codes() { + let letters_lower = 'a'..='z'; + let esc_codes = '\x01'..='\x1a'; + for (character, esc_code) in letters_lower.zip(esc_codes) { + assert_eq!( + to_esc_str( + &Keystroke::parse(&format!("ctrl-alt-{}", character)).unwrap(), + Modes::NONE, + true + ) + .unwrap(), + format!("\x1b{}", esc_code) + ); + } + } + + #[test] + fn test_shift_enter_newline() { + let shift_enter = Keystroke::parse("shift-enter").unwrap(); + let regular_enter = Keystroke::parse("enter").unwrap(); + let mode = Modes::NONE; + + // Shift-enter should send line feed (newline) + assert_eq!(to_esc_str(&shift_enter, mode, false), Some("\x0a".into())); + + // Regular enter should still send carriage return + assert_eq!(to_esc_str(®ular_enter, mode, false), Some("\x0d".into())); + } + + #[test] + fn test_modifier_code_calc() { + // Code Modifiers + // ---------+--------------------------- + // 2 | Shift + // 3 | Alt + // 4 | Shift + Alt + // 5 | Control + // 6 | Shift + Control + // 7 | Alt + Control + // 8 | Shift + Alt + Control + // ---------+--------------------------- + // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys + assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap())); + assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap())); + assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap())); + assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap())); + assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap())); + assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap())); + assert_eq!( + 8, + modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap()) + ); + } +}