1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{
4 error::Error,
5 fmt::{Display, Write},
6};
7
8/// A keystroke and associated metadata generated by the platform
9#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
10pub struct Keystroke {
11 /// the state of the modifier keys at the time the keystroke was generated
12 pub modifiers: Modifiers,
13
14 /// key is the character printed on the key that was pressed
15 /// e.g. for option-s, key is "s"
16 pub key: String,
17
18 /// key_char is the character that could have been typed when
19 /// this binding was pressed.
20 /// e.g. for s this is "s", for option-s "ร", and cmd-s None
21 pub key_char: Option<String>,
22}
23
24/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
25/// markdown to display it.
26#[derive(Debug)]
27pub struct InvalidKeystrokeError {
28 /// The invalid keystroke.
29 pub keystroke: String,
30}
31
32impl Error for InvalidKeystrokeError {}
33
34impl Display for InvalidKeystrokeError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(
37 f,
38 "Invalid keystroke \"{}\". {}",
39 self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
40 )
41 }
42}
43
44/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
45pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
46 (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
47 followed by a key, separated by `-`.";
48
49impl Keystroke {
50 /// When matching a key we cannot know whether the user intended to type
51 /// the key_char or the key itself. On some non-US keyboards keys we use in our
52 /// bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
53 /// and on some keyboards the IME handler converts a sequence of keys into a
54 /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
55 ///
56 /// This method assumes that `self` was typed and `target' is in the keymap, and checks
57 /// both possibilities for self against the target.
58 pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
59 if let Some(key_char) = self
60 .key_char
61 .as_ref()
62 .filter(|key_char| key_char != &&self.key)
63 {
64 let ime_modifiers = Modifiers {
65 control: self.modifiers.control,
66 platform: self.modifiers.platform,
67 ..Default::default()
68 };
69
70 if &target.key == key_char && target.modifiers == ime_modifiers {
71 return true;
72 }
73 }
74
75 target.modifiers == self.modifiers && target.key == self.key
76 }
77
78 /// key syntax is:
79 /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
80 /// key_char syntax is only used for generating test events,
81 /// secondary means "cmd" on macOS and "ctrl" on other platforms
82 /// when matching a key with an key_char set will be matched without it.
83 pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
84 let mut control = false;
85 let mut alt = false;
86 let mut shift = false;
87 let mut platform = false;
88 let mut function = false;
89 let mut key = None;
90 let mut key_char = None;
91
92 let mut components = source.split('-').peekable();
93 while let Some(component) = components.next() {
94 if component.eq_ignore_ascii_case("ctrl") {
95 control = true;
96 continue;
97 }
98 if component.eq_ignore_ascii_case("alt") {
99 alt = true;
100 continue;
101 }
102 if component.eq_ignore_ascii_case("shift") {
103 shift = true;
104 continue;
105 }
106 if component.eq_ignore_ascii_case("fn") {
107 function = true;
108 continue;
109 }
110 if component.eq_ignore_ascii_case("secondary") {
111 if cfg!(target_os = "macos") {
112 platform = true;
113 } else {
114 control = true;
115 };
116 continue;
117 }
118
119 let is_platform = component.eq_ignore_ascii_case("cmd")
120 || component.eq_ignore_ascii_case("super")
121 || component.eq_ignore_ascii_case("win");
122
123 if is_platform {
124 platform = true;
125 continue;
126 }
127
128 let mut key_str = component.to_string();
129
130 if let Some(next) = components.peek() {
131 if next.is_empty() && source.ends_with('-') {
132 key = Some(String::from("-"));
133 break;
134 } else if next.len() > 1 && next.starts_with('>') {
135 key = Some(key_str);
136 key_char = Some(String::from(&next[1..]));
137 components.next();
138 } else {
139 return Err(InvalidKeystrokeError {
140 keystroke: source.to_owned(),
141 });
142 }
143 continue;
144 }
145
146 if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
147 // Convert to shift + lowercase char
148 shift = true;
149 key_str.make_ascii_lowercase();
150 } else {
151 // convert ascii chars to lowercase so that named keys like "tab" and "enter"
152 // are accepted case insensitively and stored how we expect so they are matched properly
153 key_str.make_ascii_lowercase()
154 }
155 key = Some(key_str);
156 }
157
158 // Allow for the user to specify a keystroke modifier as the key itself
159 // This sets the `key` to the modifier, and disables the modifier
160 if key.is_none() {
161 if shift {
162 key = Some("shift".to_string());
163 shift = false;
164 } else if control {
165 key = Some("control".to_string());
166 control = false;
167 } else if alt {
168 key = Some("alt".to_string());
169 alt = false;
170 } else if platform {
171 key = Some("platform".to_string());
172 platform = false;
173 } else if function {
174 key = Some("function".to_string());
175 function = false;
176 }
177 }
178
179 let key = key.ok_or_else(|| InvalidKeystrokeError {
180 keystroke: source.to_owned(),
181 })?;
182
183 Ok(Keystroke {
184 modifiers: Modifiers {
185 control,
186 alt,
187 shift,
188 platform,
189 function,
190 },
191 key,
192 key_char,
193 })
194 }
195
196 /// Produces a representation of this key that Parse can understand.
197 pub fn unparse(&self) -> String {
198 let mut str = String::new();
199 if self.modifiers.function {
200 str.push_str("fn-");
201 }
202 if self.modifiers.control {
203 str.push_str("ctrl-");
204 }
205 if self.modifiers.alt {
206 str.push_str("alt-");
207 }
208 if self.modifiers.platform {
209 #[cfg(target_os = "macos")]
210 str.push_str("cmd-");
211
212 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
213 str.push_str("super-");
214
215 #[cfg(target_os = "windows")]
216 str.push_str("win-");
217 }
218 if self.modifiers.shift {
219 str.push_str("shift-");
220 }
221 str.push_str(&self.key);
222 str
223 }
224
225 /// Returns true if this keystroke left
226 /// the ime system in an incomplete state.
227 pub fn is_ime_in_progress(&self) -> bool {
228 self.key_char.is_none()
229 && (is_printable_key(&self.key) || self.key.is_empty())
230 && !(self.modifiers.platform
231 || self.modifiers.control
232 || self.modifiers.function
233 || self.modifiers.alt)
234 }
235
236 /// Returns a new keystroke with the key_char filled.
237 /// This is used for dispatch_keystroke where we want users to
238 /// be able to simulate typing "space", etc.
239 pub fn with_simulated_ime(mut self) -> Self {
240 if self.key_char.is_none()
241 && !self.modifiers.platform
242 && !self.modifiers.control
243 && !self.modifiers.function
244 && !self.modifiers.alt
245 {
246 self.key_char = match self.key.as_str() {
247 "space" => Some(" ".into()),
248 "tab" => Some("\t".into()),
249 "enter" => Some("\n".into()),
250 key if !is_printable_key(key) || key.is_empty() => None,
251 key => {
252 if self.modifiers.shift {
253 Some(key.to_uppercase())
254 } else {
255 Some(key.into())
256 }
257 }
258 }
259 }
260 self
261 }
262}
263
264fn is_printable_key(key: &str) -> bool {
265 !matches!(
266 key,
267 "f1" | "f2"
268 | "f3"
269 | "f4"
270 | "f5"
271 | "f6"
272 | "f7"
273 | "f8"
274 | "f9"
275 | "f10"
276 | "f11"
277 | "f12"
278 | "f13"
279 | "f14"
280 | "f15"
281 | "f16"
282 | "f17"
283 | "f18"
284 | "f19"
285 | "f20"
286 | "f21"
287 | "f22"
288 | "f23"
289 | "f24"
290 | "f25"
291 | "f26"
292 | "f27"
293 | "f28"
294 | "f29"
295 | "f30"
296 | "f31"
297 | "f32"
298 | "f33"
299 | "f34"
300 | "f35"
301 | "backspace"
302 | "delete"
303 | "left"
304 | "right"
305 | "up"
306 | "down"
307 | "pageup"
308 | "pagedown"
309 | "insert"
310 | "home"
311 | "end"
312 | "back"
313 | "forward"
314 | "escape"
315 )
316}
317
318impl std::fmt::Display for Keystroke {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 if self.modifiers.control {
321 if cfg!(target_os = "macos") {
322 f.write_char('^')?;
323 } else {
324 write!(f, "ctrl-")?;
325 }
326 }
327 if self.modifiers.alt {
328 if cfg!(target_os = "macos") {
329 f.write_char('โฅ')?;
330 } else {
331 write!(f, "alt-")?;
332 }
333 }
334 if self.modifiers.platform {
335 #[cfg(target_os = "macos")]
336 f.write_char('โ')?;
337
338 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
339 f.write_char('โ')?;
340
341 #[cfg(target_os = "windows")]
342 f.write_char('โ')?;
343 }
344 if self.modifiers.shift {
345 if cfg!(target_os = "macos") {
346 f.write_char('โง')?;
347 } else {
348 write!(f, "shift-")?;
349 }
350 }
351 let key = match self.key.as_str() {
352 "backspace" if cfg!(target_os = "macos") => 'โซ',
353 "up" if cfg!(target_os = "macos") => 'โ',
354 "down" if cfg!(target_os = "macos") => 'โ',
355 "left" if cfg!(target_os = "macos") => 'โ',
356 "right" if cfg!(target_os = "macos") => 'โ',
357 "tab" if cfg!(target_os = "macos") => 'โฅ',
358 "escape" if cfg!(target_os = "macos") => 'โ',
359 "shift" if cfg!(target_os = "macos") => 'โง',
360 "control" if cfg!(target_os = "macos") => 'โ',
361 "alt" if cfg!(target_os = "macos") => 'โฅ',
362 "platform" if cfg!(target_os = "macos") => 'โ',
363 key => {
364 if key.len() == 1 {
365 key.chars().next().unwrap().to_ascii_uppercase()
366 } else {
367 return f.write_str(key);
368 }
369 }
370 };
371 f.write_char(key)
372 }
373}
374
375/// The state of the modifier keys at some point in time
376#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
377pub struct Modifiers {
378 /// The control key
379 #[serde(default)]
380 pub control: bool,
381
382 /// The alt key
383 /// Sometimes also known as the 'meta' key
384 #[serde(default)]
385 pub alt: bool,
386
387 /// The shift key
388 #[serde(default)]
389 pub shift: bool,
390
391 /// The command key, on macos
392 /// the windows key, on windows
393 /// the super key, on linux
394 #[serde(default)]
395 pub platform: bool,
396
397 /// The function key
398 #[serde(default)]
399 pub function: bool,
400}
401
402impl Modifiers {
403 /// Returns whether any modifier key is pressed.
404 pub fn modified(&self) -> bool {
405 self.control || self.alt || self.shift || self.platform || self.function
406 }
407
408 /// Whether the semantically 'secondary' modifier key is pressed.
409 ///
410 /// On macOS, this is the command key.
411 /// On Linux and Windows, this is the control key.
412 pub fn secondary(&self) -> bool {
413 #[cfg(target_os = "macos")]
414 {
415 self.platform
416 }
417
418 #[cfg(not(target_os = "macos"))]
419 {
420 self.control
421 }
422 }
423
424 /// Returns how many modifier keys are pressed.
425 pub fn number_of_modifiers(&self) -> u8 {
426 self.control as u8
427 + self.alt as u8
428 + self.shift as u8
429 + self.platform as u8
430 + self.function as u8
431 }
432
433 /// Returns [`Modifiers`] with no modifiers.
434 pub fn none() -> Modifiers {
435 Default::default()
436 }
437
438 /// Returns [`Modifiers`] with just the command key.
439 pub fn command() -> Modifiers {
440 Modifiers {
441 platform: true,
442 ..Default::default()
443 }
444 }
445
446 /// A Returns [`Modifiers`] with just the secondary key pressed.
447 pub fn secondary_key() -> Modifiers {
448 #[cfg(target_os = "macos")]
449 {
450 Modifiers {
451 platform: true,
452 ..Default::default()
453 }
454 }
455
456 #[cfg(not(target_os = "macos"))]
457 {
458 Modifiers {
459 control: true,
460 ..Default::default()
461 }
462 }
463 }
464
465 /// Returns [`Modifiers`] with just the windows key.
466 pub fn windows() -> Modifiers {
467 Modifiers {
468 platform: true,
469 ..Default::default()
470 }
471 }
472
473 /// Returns [`Modifiers`] with just the super key.
474 pub fn super_key() -> Modifiers {
475 Modifiers {
476 platform: true,
477 ..Default::default()
478 }
479 }
480
481 /// Returns [`Modifiers`] with just control.
482 pub fn control() -> Modifiers {
483 Modifiers {
484 control: true,
485 ..Default::default()
486 }
487 }
488
489 /// Returns [`Modifiers`] with just alt.
490 pub fn alt() -> Modifiers {
491 Modifiers {
492 alt: true,
493 ..Default::default()
494 }
495 }
496
497 /// Returns [`Modifiers`] with just shift.
498 pub fn shift() -> Modifiers {
499 Modifiers {
500 shift: true,
501 ..Default::default()
502 }
503 }
504
505 /// Returns [`Modifiers`] with command + shift.
506 pub fn command_shift() -> Modifiers {
507 Modifiers {
508 shift: true,
509 platform: true,
510 ..Default::default()
511 }
512 }
513
514 /// Returns [`Modifiers`] with command + shift.
515 pub fn control_shift() -> Modifiers {
516 Modifiers {
517 shift: true,
518 control: true,
519 ..Default::default()
520 }
521 }
522
523 /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
524 pub fn is_subset_of(&self, other: &Modifiers) -> bool {
525 (other.control || !self.control)
526 && (other.alt || !self.alt)
527 && (other.shift || !self.shift)
528 && (other.platform || !self.platform)
529 && (other.function || !self.function)
530 }
531}