Skip to main content

gpui_types/
keyboard.rs

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