1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{
4 error::Error,
5 fmt::{Display, Write},
6};
7
8use crate::PlatformKeyboardMapper;
9
10/// This is a helper trait so that we can simplify the implementation of some functions
11pub trait AsKeystroke {
12 /// Returns the GPUI representation of the keystroke.
13 fn as_keystroke(&self) -> &Keystroke;
14}
15
16/// A keystroke and associated metadata generated by the platform
17#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
18pub struct Keystroke {
19 /// the state of the modifier keys at the time the keystroke was generated
20 pub modifiers: Modifiers,
21
22 /// key is the character printed on the key that was pressed
23 /// e.g. for option-s, key is "s"
24 /// On layouts that do not have ascii keys (e.g. Thai)
25 /// this will be the ASCII-equivalent character (q instead of เน),
26 /// and the typed character will be present in key_char.
27 pub key: String,
28
29 /// key_char is the character that could have been typed when
30 /// this binding was pressed.
31 /// e.g. for s this is "s", for option-s "ร", and cmd-s None
32 pub key_char: Option<String>,
33}
34
35/// Represents a keystroke that can be used in keybindings and displayed to the user.
36#[derive(Debug, Clone, Eq, PartialEq, Hash)]
37pub struct KeybindingKeystroke {
38 /// The GPUI representation of the keystroke.
39 inner: Keystroke,
40 /// The modifiers to display.
41 #[cfg(target_os = "windows")]
42 display_modifiers: Modifiers,
43 /// The key to display.
44 #[cfg(target_os = "windows")]
45 display_key: String,
46}
47
48/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
49/// markdown to display it.
50#[derive(Debug)]
51pub struct InvalidKeystrokeError {
52 /// The invalid keystroke.
53 pub keystroke: String,
54}
55
56impl Error for InvalidKeystrokeError {}
57
58impl Display for InvalidKeystrokeError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(
61 f,
62 "Invalid keystroke \"{}\". {}",
63 self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
64 )
65 }
66}
67
68/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
69pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
70 (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
71 followed by a key, separated by `-`.";
72
73impl Keystroke {
74 /// When matching a key we cannot know whether the user intended to type
75 /// the key_char or the key itself. On some non-US keyboards keys we use in our
76 /// bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
77 /// and on some keyboards the IME handler converts a sequence of keys into a
78 /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
79 ///
80 /// This method assumes that `self` was typed and `target' is in the keymap, and checks
81 /// both possibilities for self against the target.
82 pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
83 #[cfg(not(target_os = "windows"))]
84 if let Some(key_char) = self
85 .key_char
86 .as_ref()
87 .filter(|key_char| key_char != &&self.key)
88 {
89 let ime_modifiers = Modifiers {
90 control: self.modifiers.control,
91 platform: self.modifiers.platform,
92 ..Default::default()
93 };
94
95 if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
96 return true;
97 }
98 }
99
100 #[cfg(target_os = "windows")]
101 if let Some(key_char) = self
102 .key_char
103 .as_ref()
104 .filter(|key_char| key_char != &&self.key)
105 {
106 // On Windows, if key_char is set, then the typed keystroke produced the key_char
107 if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
108 return true;
109 }
110 }
111
112 target.inner.modifiers == self.modifiers && target.inner.key == self.key
113 }
114
115 /// key syntax is:
116 /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
117 /// key_char syntax is only used for generating test events,
118 /// secondary means "cmd" on macOS and "ctrl" on other platforms
119 /// when matching a key with an key_char set will be matched without it.
120 pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
121 let mut modifiers = Modifiers::none();
122 let mut key = None;
123 let mut key_char = None;
124
125 let mut components = source.split('-').peekable();
126 while let Some(component) = components.next() {
127 if component.eq_ignore_ascii_case("ctrl") {
128 modifiers.control = true;
129 continue;
130 }
131 if component.eq_ignore_ascii_case("alt") {
132 modifiers.alt = true;
133 continue;
134 }
135 if component.eq_ignore_ascii_case("shift") {
136 modifiers.shift = true;
137 continue;
138 }
139 if component.eq_ignore_ascii_case("fn") {
140 modifiers.function = true;
141 continue;
142 }
143 if component.eq_ignore_ascii_case("secondary") {
144 if cfg!(target_os = "macos") {
145 modifiers.platform = true;
146 } else {
147 modifiers.control = true;
148 };
149 continue;
150 }
151
152 let is_platform = component.eq_ignore_ascii_case("cmd")
153 || component.eq_ignore_ascii_case("super")
154 || component.eq_ignore_ascii_case("win");
155
156 if is_platform {
157 modifiers.platform = true;
158 continue;
159 }
160
161 let mut key_str = component.to_string();
162
163 if let Some(next) = components.peek() {
164 if next.is_empty() && source.ends_with('-') {
165 key = Some(String::from("-"));
166 break;
167 } else if next.len() > 1 && next.starts_with('>') {
168 key = Some(key_str);
169 key_char = Some(String::from(&next[1..]));
170 components.next();
171 } else {
172 return Err(InvalidKeystrokeError {
173 keystroke: source.to_owned(),
174 });
175 }
176 continue;
177 }
178
179 if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
180 // Convert to shift + lowercase char
181 modifiers.shift = true;
182 key_str.make_ascii_lowercase();
183 } else {
184 // convert ascii chars to lowercase so that named keys like "tab" and "enter"
185 // are accepted case insensitively and stored how we expect so they are matched properly
186 key_str.make_ascii_lowercase()
187 }
188 key = Some(key_str);
189 }
190
191 // Allow for the user to specify a keystroke modifier as the key itself
192 // This sets the `key` to the modifier, and disables the modifier
193 key = key.or_else(|| {
194 use std::mem;
195 // std::mem::take clears bool incase its true
196 if mem::take(&mut modifiers.shift) {
197 Some("shift".to_string())
198 } else if mem::take(&mut modifiers.control) {
199 Some("control".to_string())
200 } else if mem::take(&mut modifiers.alt) {
201 Some("alt".to_string())
202 } else if mem::take(&mut modifiers.platform) {
203 Some("platform".to_string())
204 } else if mem::take(&mut modifiers.function) {
205 Some("function".to_string())
206 } else {
207 None
208 }
209 });
210
211 let key = key.ok_or_else(|| InvalidKeystrokeError {
212 keystroke: source.to_owned(),
213 })?;
214
215 Ok(Keystroke {
216 modifiers,
217 key,
218 key_char,
219 })
220 }
221
222 /// Produces a representation of this key that Parse can understand.
223 pub fn unparse(&self) -> String {
224 unparse(&self.modifiers, &self.key)
225 }
226
227 /// Returns true if this keystroke left
228 /// the ime system in an incomplete state.
229 pub fn is_ime_in_progress(&self) -> bool {
230 self.key_char.is_none()
231 && (is_printable_key(&self.key) || self.key.is_empty())
232 && !(self.modifiers.platform
233 || self.modifiers.control
234 || self.modifiers.function
235 || self.modifiers.alt)
236 }
237
238 /// Returns a new keystroke with the key_char filled.
239 /// This is used for dispatch_keystroke where we want users to
240 /// be able to simulate typing "space", etc.
241 pub fn with_simulated_ime(mut self) -> Self {
242 if self.key_char.is_none()
243 && !self.modifiers.platform
244 && !self.modifiers.control
245 && !self.modifiers.function
246 && !self.modifiers.alt
247 {
248 self.key_char = match self.key.as_str() {
249 "space" => Some(" ".into()),
250 "tab" => Some("\t".into()),
251 "enter" => Some("\n".into()),
252 key if !is_printable_key(key) || key.is_empty() => None,
253 key => {
254 if self.modifiers.shift {
255 Some(key.to_uppercase())
256 } else {
257 Some(key.into())
258 }
259 }
260 }
261 }
262 self
263 }
264}
265
266impl KeybindingKeystroke {
267 #[cfg(target_os = "windows")]
268 #[expect(missing_docs)]
269 pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
270 KeybindingKeystroke {
271 inner,
272 display_modifiers,
273 display_key,
274 }
275 }
276
277 /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
278 pub fn new_with_mapper(
279 inner: Keystroke,
280 use_key_equivalents: bool,
281 keyboard_mapper: &dyn PlatformKeyboardMapper,
282 ) -> Self {
283 keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
284 }
285
286 /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
287 pub fn from_keystroke(keystroke: Keystroke) -> Self {
288 #[cfg(target_os = "windows")]
289 {
290 let key = keystroke.key.clone();
291 let modifiers = keystroke.modifiers;
292 KeybindingKeystroke {
293 inner: keystroke,
294 display_modifiers: modifiers,
295 display_key: key,
296 }
297 }
298 #[cfg(not(target_os = "windows"))]
299 {
300 KeybindingKeystroke { inner: keystroke }
301 }
302 }
303
304 /// Returns the GPUI representation of the keystroke.
305 pub fn inner(&self) -> &Keystroke {
306 &self.inner
307 }
308
309 /// Returns the modifiers.
310 ///
311 /// Platform-specific behavior:
312 /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
313 /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
314 /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
315 pub fn modifiers(&self) -> &Modifiers {
316 #[cfg(target_os = "windows")]
317 {
318 &self.display_modifiers
319 }
320 #[cfg(not(target_os = "windows"))]
321 {
322 &self.inner.modifiers
323 }
324 }
325
326 /// Returns the key.
327 ///
328 /// Platform-specific behavior:
329 /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
330 /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
331 pub fn key(&self) -> &str {
332 #[cfg(target_os = "windows")]
333 {
334 &self.display_key
335 }
336 #[cfg(not(target_os = "windows"))]
337 {
338 &self.inner.key
339 }
340 }
341
342 /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
343 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
344 self.inner.modifiers = modifiers;
345 #[cfg(target_os = "windows")]
346 {
347 self.display_modifiers = modifiers;
348 }
349 }
350
351 /// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
352 pub fn set_key(&mut self, key: String) {
353 #[cfg(target_os = "windows")]
354 {
355 self.display_key = key.clone();
356 }
357 self.inner.key = key;
358 }
359
360 /// Produces a representation of this key that Parse can understand.
361 pub fn unparse(&self) -> String {
362 #[cfg(target_os = "windows")]
363 {
364 unparse(&self.display_modifiers, &self.display_key)
365 }
366 #[cfg(not(target_os = "windows"))]
367 {
368 unparse(&self.inner.modifiers, &self.inner.key)
369 }
370 }
371
372 /// Removes the key_char
373 pub fn remove_key_char(&mut self) {
374 self.inner.key_char = None;
375 }
376}
377
378fn is_printable_key(key: &str) -> bool {
379 !matches!(
380 key,
381 "f1" | "f2"
382 | "f3"
383 | "f4"
384 | "f5"
385 | "f6"
386 | "f7"
387 | "f8"
388 | "f9"
389 | "f10"
390 | "f11"
391 | "f12"
392 | "f13"
393 | "f14"
394 | "f15"
395 | "f16"
396 | "f17"
397 | "f18"
398 | "f19"
399 | "f20"
400 | "f21"
401 | "f22"
402 | "f23"
403 | "f24"
404 | "f25"
405 | "f26"
406 | "f27"
407 | "f28"
408 | "f29"
409 | "f30"
410 | "f31"
411 | "f32"
412 | "f33"
413 | "f34"
414 | "f35"
415 | "backspace"
416 | "delete"
417 | "left"
418 | "right"
419 | "up"
420 | "down"
421 | "pageup"
422 | "pagedown"
423 | "insert"
424 | "home"
425 | "end"
426 | "back"
427 | "forward"
428 | "escape"
429 )
430}
431
432impl std::fmt::Display for Keystroke {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 display_modifiers(&self.modifiers, f)?;
435 display_key(&self.key, f)
436 }
437}
438
439impl std::fmt::Display for KeybindingKeystroke {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 display_modifiers(self.modifiers(), f)?;
442 display_key(self.key(), f)
443 }
444}
445
446/// The state of the modifier keys at some point in time
447#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
448pub struct Modifiers {
449 /// The control key
450 #[serde(default)]
451 pub control: bool,
452
453 /// The alt key
454 /// Sometimes also known as the 'meta' key
455 #[serde(default)]
456 pub alt: bool,
457
458 /// The shift key
459 #[serde(default)]
460 pub shift: bool,
461
462 /// The command key, on macos
463 /// the windows key, on windows
464 /// the super key, on linux
465 #[serde(default)]
466 pub platform: bool,
467
468 /// The function key
469 #[serde(default)]
470 pub function: bool,
471}
472
473impl Modifiers {
474 /// Returns whether any modifier key is pressed.
475 pub fn modified(&self) -> bool {
476 self.control || self.alt || self.shift || self.platform || self.function
477 }
478
479 /// Whether the semantically 'secondary' modifier key is pressed.
480 ///
481 /// On macOS, this is the command key.
482 /// On Linux and Windows, this is the control key.
483 pub fn secondary(&self) -> bool {
484 #[cfg(target_os = "macos")]
485 {
486 self.platform
487 }
488
489 #[cfg(not(target_os = "macos"))]
490 {
491 self.control
492 }
493 }
494
495 /// Returns how many modifier keys are pressed.
496 pub fn number_of_modifiers(&self) -> u8 {
497 self.control as u8
498 + self.alt as u8
499 + self.shift as u8
500 + self.platform as u8
501 + self.function as u8
502 }
503
504 /// Returns [`Modifiers`] with no modifiers.
505 pub fn none() -> Modifiers {
506 Default::default()
507 }
508
509 /// Returns [`Modifiers`] with just the command key.
510 pub fn command() -> Modifiers {
511 Modifiers {
512 platform: true,
513 ..Default::default()
514 }
515 }
516
517 /// A Returns [`Modifiers`] with just the secondary key pressed.
518 pub fn secondary_key() -> Modifiers {
519 #[cfg(target_os = "macos")]
520 {
521 Modifiers {
522 platform: true,
523 ..Default::default()
524 }
525 }
526
527 #[cfg(not(target_os = "macos"))]
528 {
529 Modifiers {
530 control: true,
531 ..Default::default()
532 }
533 }
534 }
535
536 /// Returns [`Modifiers`] with just the windows key.
537 pub fn windows() -> Modifiers {
538 Modifiers {
539 platform: true,
540 ..Default::default()
541 }
542 }
543
544 /// Returns [`Modifiers`] with just the super key.
545 pub fn super_key() -> Modifiers {
546 Modifiers {
547 platform: true,
548 ..Default::default()
549 }
550 }
551
552 /// Returns [`Modifiers`] with just control.
553 pub fn control() -> Modifiers {
554 Modifiers {
555 control: true,
556 ..Default::default()
557 }
558 }
559
560 /// Returns [`Modifiers`] with just alt.
561 pub fn alt() -> Modifiers {
562 Modifiers {
563 alt: true,
564 ..Default::default()
565 }
566 }
567
568 /// Returns [`Modifiers`] with just shift.
569 pub fn shift() -> Modifiers {
570 Modifiers {
571 shift: true,
572 ..Default::default()
573 }
574 }
575
576 /// Returns [`Modifiers`] with just function.
577 pub fn function() -> Modifiers {
578 Modifiers {
579 function: true,
580 ..Default::default()
581 }
582 }
583
584 /// Returns [`Modifiers`] with command + shift.
585 pub fn command_shift() -> Modifiers {
586 Modifiers {
587 shift: true,
588 platform: true,
589 ..Default::default()
590 }
591 }
592
593 /// Returns [`Modifiers`] with command + shift.
594 pub fn control_shift() -> Modifiers {
595 Modifiers {
596 shift: true,
597 control: true,
598 ..Default::default()
599 }
600 }
601
602 /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
603 pub fn is_subset_of(&self, other: &Modifiers) -> bool {
604 (*other & *self) == *self
605 }
606}
607
608impl std::ops::BitOr for Modifiers {
609 type Output = Self;
610
611 fn bitor(mut self, other: Self) -> Self::Output {
612 self |= other;
613 self
614 }
615}
616
617impl std::ops::BitOrAssign for Modifiers {
618 fn bitor_assign(&mut self, other: Self) {
619 self.control |= other.control;
620 self.alt |= other.alt;
621 self.shift |= other.shift;
622 self.platform |= other.platform;
623 self.function |= other.function;
624 }
625}
626
627impl std::ops::BitXor for Modifiers {
628 type Output = Self;
629 fn bitxor(mut self, rhs: Self) -> Self::Output {
630 self ^= rhs;
631 self
632 }
633}
634
635impl std::ops::BitXorAssign for Modifiers {
636 fn bitxor_assign(&mut self, other: Self) {
637 self.control ^= other.control;
638 self.alt ^= other.alt;
639 self.shift ^= other.shift;
640 self.platform ^= other.platform;
641 self.function ^= other.function;
642 }
643}
644
645impl std::ops::BitAnd for Modifiers {
646 type Output = Self;
647 fn bitand(mut self, rhs: Self) -> Self::Output {
648 self &= rhs;
649 self
650 }
651}
652
653impl std::ops::BitAndAssign for Modifiers {
654 fn bitand_assign(&mut self, other: Self) {
655 self.control &= other.control;
656 self.alt &= other.alt;
657 self.shift &= other.shift;
658 self.platform &= other.platform;
659 self.function &= other.function;
660 }
661}
662
663/// The state of the capslock key at some point in time
664#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
665pub struct Capslock {
666 /// The capslock key is on
667 #[serde(default)]
668 pub on: bool,
669}
670
671impl AsKeystroke for Keystroke {
672 fn as_keystroke(&self) -> &Keystroke {
673 self
674 }
675}
676
677impl AsKeystroke for KeybindingKeystroke {
678 fn as_keystroke(&self) -> &Keystroke {
679 &self.inner
680 }
681}
682
683fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684 if modifiers.control {
685 #[cfg(target_os = "macos")]
686 f.write_char('^')?;
687
688 #[cfg(not(target_os = "macos"))]
689 write!(f, "ctrl-")?;
690 }
691 if modifiers.alt {
692 #[cfg(target_os = "macos")]
693 f.write_char('โฅ')?;
694
695 #[cfg(not(target_os = "macos"))]
696 write!(f, "alt-")?;
697 }
698 if modifiers.platform {
699 #[cfg(target_os = "macos")]
700 f.write_char('โ')?;
701
702 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
703 f.write_char('โ')?;
704
705 #[cfg(target_os = "windows")]
706 f.write_char('โ')?;
707 }
708 if modifiers.shift {
709 #[cfg(target_os = "macos")]
710 f.write_char('โง')?;
711
712 #[cfg(not(target_os = "macos"))]
713 write!(f, "shift-")?;
714 }
715 Ok(())
716}
717
718fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
719 let key = match key {
720 #[cfg(target_os = "macos")]
721 "backspace" => 'โซ',
722 #[cfg(target_os = "macos")]
723 "up" => 'โ',
724 #[cfg(target_os = "macos")]
725 "down" => 'โ',
726 #[cfg(target_os = "macos")]
727 "left" => 'โ',
728 #[cfg(target_os = "macos")]
729 "right" => 'โ',
730 #[cfg(target_os = "macos")]
731 "tab" => 'โฅ',
732 #[cfg(target_os = "macos")]
733 "escape" => 'โ',
734 #[cfg(target_os = "macos")]
735 "shift" => 'โง',
736 #[cfg(target_os = "macos")]
737 "control" => 'โ',
738 #[cfg(target_os = "macos")]
739 "alt" => 'โฅ',
740 #[cfg(target_os = "macos")]
741 "platform" => 'โ',
742
743 key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
744 key => return f.write_str(key),
745 };
746 f.write_char(key)
747}
748
749#[inline]
750fn unparse(modifiers: &Modifiers, key: &str) -> String {
751 let mut result = String::new();
752 if modifiers.function {
753 result.push_str("fn-");
754 }
755 if modifiers.control {
756 result.push_str("ctrl-");
757 }
758 if modifiers.alt {
759 result.push_str("alt-");
760 }
761 if modifiers.platform {
762 #[cfg(target_os = "macos")]
763 result.push_str("cmd-");
764
765 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
766 result.push_str("super-");
767
768 #[cfg(target_os = "windows")]
769 result.push_str("win-");
770 }
771 if modifiers.shift {
772 result.push_str("shift-");
773 }
774 result.push_str(&key);
775 result
776}