1use gpui::KeyDownEvent;
2use twrite_core::{KeyCode, KeyEvent, Modifiers};
3
4pub fn translate_key_down(event: &KeyDownEvent) -> Option<KeyEvent> {
25 let keystroke = &event.keystroke;
26 Some(KeyEvent {
27 code: code_for(keystroke),
28 modifiers: Modifiers {
29 ctrl: keystroke.modifiers.control,
30 alt: keystroke.modifiers.alt,
31 shift: keystroke.modifiers.shift,
32 meta: keystroke.modifiers.platform,
33 },
34 })
35}
36
37fn code_for(keystroke: &gpui::Keystroke) -> KeyCode {
42 let mods = &keystroke.modifiers;
43 match keystroke.key.as_str() {
44 "space" => return KeyCode::Char(' '),
45 "left" | "arrowleft" => return KeyCode::Left,
46 "right" | "arrowright" => return KeyCode::Right,
47 "up" | "arrowup" => return KeyCode::Up,
48 "down" | "arrowdown" => return KeyCode::Down,
49 _ => {}
50 }
51 if let Some(text) = &keystroke.key_char
52 && text.chars().count() == 1
53 && !mods.control
54 && !mods.platform
55 && !mods.alt
56 {
57 return KeyCode::Char(text.chars().next().unwrap());
58 }
59 if keystroke.key.chars().count() == 1 {
60 return KeyCode::Char(keystroke.key.chars().next().unwrap().to_ascii_lowercase());
61 }
62 match keystroke.key.as_str() {
63 "enter" => KeyCode::Enter,
64 "tab" => KeyCode::Tab,
65 "escape" => KeyCode::Escape,
66 "backspace" => KeyCode::Backspace,
67 "delete" => KeyCode::Delete,
68 "home" => KeyCode::Home,
69 "end" => KeyCode::End,
70 "pageup" => KeyCode::PageUp,
71 "pagedown" => KeyCode::PageDown,
72 "insert" => KeyCode::Insert,
73 name => parse_function_key(name).unwrap_or(KeyCode::Unidentified),
74 }
75}
76
77fn parse_function_key(name: &str) -> Option<KeyCode> {
79 let lower = name.to_ascii_lowercase();
80 let digits = lower.strip_prefix('f')?;
81 if !(1..=2).contains(&digits.len()) || !digits.chars().all(|c| c.is_ascii_digit()) {
82 return None;
83 }
84 let n: u8 = digits.parse().ok()?;
85 (1..=24).contains(&n).then_some(KeyCode::F(n))
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 fn keystroke(key: &str, key_char: Option<&str>, modifiers: gpui::Modifiers) -> KeyDownEvent {
93 KeyDownEvent {
94 keystroke: gpui::Keystroke {
95 key: key.to_string(),
96 key_char: key_char.map(|s| s.to_string()),
97 modifiers,
98 },
99 is_held: false,
100 }
101 }
102
103 fn plain(key: &str, key_char: Option<&str>) -> KeyDownEvent {
104 keystroke(key, key_char, gpui::Modifiers::default())
105 }
106
107 #[test]
108 fn shift_letter_yields_capital_with_shift_held() {
109 let event = keystroke(
110 "a",
111 Some("A"),
112 gpui::Modifiers {
113 shift: true,
114 ..Default::default()
115 },
116 );
117 let translated = translate_key_down(&event).unwrap();
118 assert_eq!(translated.code, KeyCode::Char('A'));
119 assert!(translated.modifiers.shift);
120 }
121
122 #[test]
123 fn plain_letter_passes_through() {
124 let translated = translate_key_down(&plain("a", Some("a"))).unwrap();
125 assert_eq!(translated.code, KeyCode::Char('a'));
126 assert!(!translated.modifiers.shift);
127 }
128
129 #[test]
130 fn shifted_symbol_uses_typed_character() {
131 let event = keystroke(
132 "/",
133 Some("?"),
134 gpui::Modifiers {
135 shift: true,
136 ..Default::default()
137 },
138 );
139 assert_eq!(translate_key_down(&event).unwrap().code, KeyCode::Char('?'));
140 }
141
142 #[test]
143 fn command_combos_keep_physical_key_names() {
144 let event = keystroke(
145 "b",
146 Some("b"),
147 gpui::Modifiers {
148 control: true,
149 ..Default::default()
150 },
151 );
152 let translated = translate_key_down(&event).unwrap();
153 assert_eq!(translated.code, KeyCode::Char('b'));
154 assert!(translated.modifiers.ctrl);
155 }
156
157 #[test]
158 fn alt_combos_keep_key_names() {
159 let alt = gpui::Modifiers {
162 alt: true,
163 ..Default::default()
164 };
165 let translated = translate_key_down(&keystroke("c", Some("รง"), alt)).unwrap();
166 assert_eq!(translated.code, KeyCode::Char('c'));
167 assert!(translated.modifiers.alt);
168 }
169
170 #[test]
171 fn multichar_or_missing_key_char_falls_back_to_key() {
172 let translated = translate_key_down(&plain("a", Some("aeiou"))).unwrap();
174 assert_eq!(translated.code, KeyCode::Char('a'));
175 let translated = translate_key_down(&plain("s", None)).unwrap();
177 assert_eq!(translated.code, KeyCode::Char('s'));
178 }
179
180 #[test]
181 fn named_keys_and_aliases_map_to_variants() {
182 assert_eq!(
183 translate_key_down(&plain("enter", None)).unwrap().code,
184 KeyCode::Enter
185 );
186 assert_eq!(
187 translate_key_down(&plain("left", None)).unwrap().code,
188 KeyCode::Left
189 );
190 assert_eq!(
191 translate_key_down(&plain("space", None)).unwrap().code,
192 KeyCode::Char(' ')
193 );
194 assert_eq!(
195 translate_key_down(&plain("f3", None)).unwrap().code,
196 KeyCode::F(3)
197 );
198 assert_eq!(
199 translate_key_down(&plain("back", None)).unwrap().code,
200 KeyCode::Unidentified
201 );
202 }
203}