Skip to main content

gpui/platform/
keystroke.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{
4    error::Error,
5    fmt::{Display, Write},
6};
7
8use crate::PlatformKeyboardMapper;
9
10/// A type that can be viewed as a [`Keystroke`].
11///
12/// Matching code uses this so that it accepts both a [`Keystroke`] that was typed and a
13/// [`KeybindingKeystroke`] from a keymap.
14pub trait AsKeystroke {
15    /// Returns the [`Keystroke`] this value represents.
16    fn as_keystroke(&self) -> &Keystroke;
17}
18
19/// A key press, with the modifier keys that were held when it happened.
20///
21/// The platform creates a keystroke for each key press. To write one by hand, as a binding or a
22/// test does, parse it from a string with [`Keystroke::parse`]:
23///
24/// ```
25/// # use gpui::Keystroke;
26/// let keystroke = Keystroke::parse("ctrl-shift-z").unwrap();
27/// assert!(keystroke.modifiers.control);
28/// assert!(keystroke.modifiers.shift);
29/// assert_eq!(keystroke.key, "z");
30/// ```
31///
32/// See [keyboard input](crate::_documentation::guides::keyboard) for the bigger picture.
33#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
34pub struct Keystroke {
35    /// The modifier keys held when the key was pressed.
36    pub modifiers: Modifiers,
37
38    /// The key that was pressed, named after the character printed on it, such as `s`, or a
39    /// name such as `enter` or `pageup` for keys without a character.
40    ///
41    /// For option-s on macOS this is `s`, not `ß`. On layouts without ASCII letters, such as Thai,
42    /// this is the ASCII equivalent of the key, and the typed character is in `key_char`.
43    pub key: String,
44
45    /// The text the key would type with the current layout and modifiers, if any.
46    ///
47    /// This is `Some("s")` for `s`, `Some("ß")` for option-s on a US macOS layout, and `None` for
48    /// `cmd-s`, because `cmd` doesn't type text.
49    pub key_char: Option<String>,
50}
51
52/// A keystroke as it appears in a keybinding, ready to display to the user.
53///
54/// A [`KeyBinding`](crate::KeyBinding) stores its keystrokes as `KeybindingKeystroke`s. On
55/// Windows, the modifiers and key shown to the user can differ from the ones used for matching.
56#[derive(Debug, Clone, Eq, PartialEq, Hash)]
57pub struct KeybindingKeystroke {
58    /// The GPUI representation of the keystroke.
59    inner: Keystroke,
60    /// The modifiers to display.
61    #[cfg(target_os = "windows")]
62    display_modifiers: Modifiers,
63    /// The key to display.
64    #[cfg(target_os = "windows")]
65    display_key: String,
66}
67
68/// Error returned by [`Keystroke::parse`] when a string isn't a valid keystroke.
69///
70/// It is a plain error type rather than `anyhow::Error`.
71#[derive(Debug)]
72pub struct InvalidKeystrokeError {
73    /// The source text that failed to parse.
74    pub keystroke: String,
75}
76
77impl Error for InvalidKeystrokeError {}
78
79impl Display for InvalidKeystrokeError {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(
82            f,
83            "Invalid keystroke \"{}\". {}",
84            self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
85        )
86    }
87}
88
89/// The sentence that [`InvalidKeystrokeError`] appends to its message. It describes the syntax
90/// [`Keystroke::parse`] expects.
91pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
92    (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
93    followed by a key, separated by `-`.";
94
95impl Keystroke {
96    /// Returns whether this keystroke, as typed, matches a keystroke in a keymap.
97    ///
98    /// `self` is the keystroke the user typed and `target` is the one from the binding. Matching
99    /// first compares modifiers and key. It also accepts a `target` whose key is this keystroke's
100    /// `key_char`, when the target's modifiers are exactly this keystroke's `ctrl` and `cmd` (on
101    /// Windows, when the target has no modifiers). That covers layouts where a binding
102    /// key sits behind option, such as `$` on a Czech keyboard typed as `alt-ç`, and input methods
103    /// that turn a sequence of keys into one character.
104    pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
105        #[cfg(not(target_os = "windows"))]
106        if let Some(key_char) = self
107            .key_char
108            .as_ref()
109            .filter(|key_char| key_char != &&self.key)
110        {
111            let ime_modifiers = Modifiers {
112                control: self.modifiers.control,
113                platform: self.modifiers.platform,
114                ..Default::default()
115            };
116
117            if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
118                return true;
119            }
120        }
121
122        #[cfg(target_os = "windows")]
123        if let Some(key_char) = self
124            .key_char
125            .as_ref()
126            .filter(|key_char| key_char != &&self.key)
127        {
128            // On Windows, if key_char is set, then the typed keystroke produced the key_char
129            if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
130                return true;
131            }
132        }
133
134        target.inner.modifiers == self.modifiers && target.inner.key == self.key
135    }
136
137    /// Parses a keystroke from text such as `cmd-s`, `ctrl-shift-tab` or `cmd--`.
138    ///
139    /// The syntax is zero or more modifiers, then a key, separated by `-`:
140    ///
141    /// - **Modifiers** are `ctrl`, `alt`, `shift`, `fn`, `secondary`, `cmd`, `super` and `win`.
142    ///   They can come in any order and any case. `cmd`, `super` and `win` all set
143    ///   [`Modifiers::platform`]. `secondary` sets `cmd` on macOS and `ctrl` on other platforms,
144    ///   which is what [`Modifiers::secondary`] reports.
145    /// - **The key** is the last part. Any other part must be a modifier. Key names are
146    ///   case-insensitive and stored in lowercase, so `Tab` and `tab` are the same key. Other
147    ///   text is accepted as a key without checking, but the names that platforms produce are
148    ///   `space`, `tab`, `enter`, `backspace`, `escape`, `delete`, `insert`, `home`, `end`,
149    ///   `pageup`, `pagedown`, `up`, `down`, `left`, `right` and function keys such as `f1`.
150    /// - **A single uppercase letter** sets `shift` and uses the lowercase letter as the key, so
151    ///   `S` is `shift-s`.
152    /// - **A key of `-`** is written as an extra dash, so `cmd--` is `cmd` and `-`.
153    /// - **A modifier with no key** becomes the key. The first of `shift`, `ctrl`, `alt`, `cmd` and
154    ///   `fn` that is present is used, in that order. Its key is named `shift`, `control`, `alt`,
155    ///   `platform` or `function`, and that modifier is cleared. So `ctrl` alone is the key
156    ///   `control` with no modifiers set.
157    /// - **`key->key_char`** sets `key_char` to the text after `->`, for example `alt-s->ß`.
158    ///   [`Keystroke::should_match`] ignores it when matching a binding.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`InvalidKeystrokeError`] if a part before the key isn't a modifier, as in
163    /// `ctrl-a-b`, or if `->` isn't followed by a key character.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// # use gpui::Keystroke;
169    /// let save = Keystroke::parse("secondary-s").unwrap();
170    /// assert!(save.modifiers.secondary());
171    /// assert_eq!(save.key, "s");
172    ///
173    /// let shifted = Keystroke::parse("S").unwrap();
174    /// assert!(shifted.modifiers.shift);
175    /// assert_eq!(shifted.key, "s");
176    ///
177    /// assert!(Keystroke::parse("ctrl-a-b").is_err());
178    /// ```
179    ///
180    /// See [keyboard input](crate::_documentation::guides::keyboard) for how bindings use these.
181    pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
182        let mut modifiers = Modifiers::none();
183        let mut key = None;
184        let mut key_char = None;
185
186        let mut components = source.split('-').peekable();
187        while let Some(component) = components.next() {
188            if component.eq_ignore_ascii_case("ctrl") {
189                modifiers.control = true;
190                continue;
191            }
192            if component.eq_ignore_ascii_case("alt") {
193                modifiers.alt = true;
194                continue;
195            }
196            if component.eq_ignore_ascii_case("shift") {
197                modifiers.shift = true;
198                continue;
199            }
200            if component.eq_ignore_ascii_case("fn") {
201                modifiers.function = true;
202                continue;
203            }
204            if component.eq_ignore_ascii_case("secondary") {
205                if cfg!(target_os = "macos") {
206                    modifiers.platform = true;
207                } else {
208                    modifiers.control = true;
209                };
210                continue;
211            }
212
213            let is_platform = component.eq_ignore_ascii_case("cmd")
214                || component.eq_ignore_ascii_case("super")
215                || component.eq_ignore_ascii_case("win");
216
217            if is_platform {
218                modifiers.platform = true;
219                continue;
220            }
221
222            let mut key_str = component.to_string();
223
224            if let Some(next) = components.peek() {
225                if next.is_empty() && source.ends_with('-') {
226                    key = Some(String::from("-"));
227                    break;
228                } else if next.len() > 1 && next.starts_with('>') {
229                    key = Some(key_str);
230                    key_char = Some(String::from(&next[1..]));
231                    components.next();
232                } else {
233                    return Err(InvalidKeystrokeError {
234                        keystroke: source.to_owned(),
235                    });
236                }
237                continue;
238            }
239
240            if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
241                // Convert to shift + lowercase char
242                modifiers.shift = true;
243                key_str.make_ascii_lowercase();
244            } else {
245                // convert ascii chars to lowercase so that named keys like "tab" and "enter"
246                // are accepted case insensitively and stored how we expect so they are matched properly
247                key_str.make_ascii_lowercase()
248            }
249            key = Some(key_str);
250        }
251
252        // Allow for the user to specify a keystroke modifier as the key itself
253        // This sets the `key` to the modifier, and disables the modifier
254        key = key.or_else(|| {
255            use std::mem;
256            // std::mem::take clears bool incase its true
257            if mem::take(&mut modifiers.shift) {
258                Some("shift".to_string())
259            } else if mem::take(&mut modifiers.control) {
260                Some("control".to_string())
261            } else if mem::take(&mut modifiers.alt) {
262                Some("alt".to_string())
263            } else if mem::take(&mut modifiers.platform) {
264                Some("platform".to_string())
265            } else if mem::take(&mut modifiers.function) {
266                Some("function".to_string())
267            } else {
268                None
269            }
270        });
271
272        let key = key.ok_or_else(|| InvalidKeystrokeError {
273            keystroke: source.to_owned(),
274        })?;
275
276        Ok(Keystroke {
277            modifiers,
278            key,
279            key_char,
280        })
281    }
282
283    /// Returns text that [`Keystroke::parse`] reads back as this keystroke.
284    ///
285    /// The platform's name for `cmd` is used: `cmd` on macOS, `super` on Linux and `win` on
286    /// Windows. `key_char` is not included, so the result doesn't round-trip when it is set.
287    pub fn unparse(&self) -> String {
288        unparse(&self.modifiers, &self.key)
289    }
290
291    /// Returns whether an input method is still composing text for this keystroke.
292    ///
293    /// This is true when `key_char` is `None`, the key is printable or empty (not a named key such
294    /// as `escape` or `f1`), and none of `cmd`, `ctrl`, `fn` or `alt` is held. `shift` alone
295    /// doesn't count.
296    pub fn is_ime_in_progress(&self) -> bool {
297        self.key_char.is_none()
298            && (is_printable_key(&self.key) || self.key.is_empty())
299            && !(self.modifiers.platform
300                || self.modifiers.control
301                || self.modifiers.function
302                || self.modifiers.alt)
303    }
304
305    /// Returns this keystroke with `key_char` set to the text it would type.
306    ///
307    /// Simulated keystrokes use this so that they type text as a real key press does. It only
308    /// applies when `key_char` is `None` and none of `cmd`, `ctrl`, `fn` or `alt` is held. `space`,
309    /// `tab` and `enter` type a space, a tab and a newline. Other printable keys type their
310    /// character, uppercased when `shift` is held.
311    pub fn with_simulated_ime(mut self) -> Self {
312        if self.key_char.is_none()
313            && !self.modifiers.platform
314            && !self.modifiers.control
315            && !self.modifiers.function
316            && !self.modifiers.alt
317        {
318            self.key_char = match self.key.as_str() {
319                "space" => Some(" ".into()),
320                "tab" => Some("\t".into()),
321                "enter" => Some("\n".into()),
322                key if !is_printable_key(key) || key.is_empty() => None,
323                key => {
324                    if self.modifiers.shift {
325                        Some(key.to_uppercase())
326                    } else {
327                        Some(key.into())
328                    }
329                }
330            }
331        }
332        self
333    }
334}
335
336impl KeybindingKeystroke {
337    #[cfg(target_os = "windows")]
338    #[expect(missing_docs)]
339    pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
340        KeybindingKeystroke {
341            inner,
342            display_modifiers,
343            display_key,
344        }
345    }
346
347    /// Creates a keybinding keystroke from `inner`, mapped by `keyboard_mapper`.
348    ///
349    /// The platform's [`PlatformKeyboardMapper`](crate::PlatformKeyboardMapper) decides what
350    /// changes. On macOS, when `use_key_equivalents` is true and the current layout types a
351    /// different character for the key, the key is replaced with that character.
352    pub fn new_with_mapper(
353        inner: Keystroke,
354        use_key_equivalents: bool,
355        keyboard_mapper: &dyn PlatformKeyboardMapper,
356    ) -> Self {
357        keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
358    }
359
360    /// Creates a keybinding keystroke from `keystroke` with no platform-specific mapping.
361    pub fn from_keystroke(keystroke: Keystroke) -> Self {
362        #[cfg(target_os = "windows")]
363        {
364            let key = keystroke.key.clone();
365            let modifiers = keystroke.modifiers;
366            KeybindingKeystroke {
367                inner: keystroke,
368                display_modifiers: modifiers,
369                display_key: key,
370            }
371        }
372        #[cfg(not(target_os = "windows"))]
373        {
374            KeybindingKeystroke { inner: keystroke }
375        }
376    }
377
378    /// Returns the [`Keystroke`] that matching uses.
379    pub fn inner(&self) -> &Keystroke {
380        &self.inner
381    }
382
383    /// Returns the modifiers.
384    ///
385    /// Platform-specific behavior:
386    /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
387    /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
388    /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
389    pub fn modifiers(&self) -> &Modifiers {
390        #[cfg(target_os = "windows")]
391        {
392            &self.display_modifiers
393        }
394        #[cfg(not(target_os = "windows"))]
395        {
396            &self.inner.modifiers
397        }
398    }
399
400    /// Returns the key.
401    ///
402    /// Platform-specific behavior:
403    /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
404    /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
405    pub fn key(&self) -> &str {
406        #[cfg(target_os = "windows")]
407        {
408            &self.display_key
409        }
410        #[cfg(not(target_os = "windows"))]
411        {
412            &self.inner.key
413        }
414    }
415
416    /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
417    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
418        self.inner.modifiers = modifiers;
419        #[cfg(target_os = "windows")]
420        {
421            self.display_modifiers = modifiers;
422        }
423    }
424
425    /// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
426    pub fn set_key(&mut self, key: String) {
427        #[cfg(target_os = "windows")]
428        {
429            self.display_key = key.clone();
430        }
431        self.inner.key = key;
432    }
433
434    /// Returns text that [`Keystroke::parse`] reads back as this keystroke, using the displayed
435    /// modifiers and key on Windows. See [`Keystroke::unparse`].
436    pub fn unparse(&self) -> String {
437        #[cfg(target_os = "windows")]
438        {
439            unparse(&self.display_modifiers, &self.display_key)
440        }
441        #[cfg(not(target_os = "windows"))]
442        {
443            unparse(&self.inner.modifiers, &self.inner.key)
444        }
445    }
446
447    /// Clears `key_char`, so that matching and display use only `key` and the modifiers.
448    pub fn remove_key_char(&mut self) {
449        self.inner.key_char = None;
450    }
451}
452
453fn is_printable_key(key: &str) -> bool {
454    !matches!(
455        key,
456        "f1" | "f2"
457            | "f3"
458            | "f4"
459            | "f5"
460            | "f6"
461            | "f7"
462            | "f8"
463            | "f9"
464            | "f10"
465            | "f11"
466            | "f12"
467            | "f13"
468            | "f14"
469            | "f15"
470            | "f16"
471            | "f17"
472            | "f18"
473            | "f19"
474            | "f20"
475            | "f21"
476            | "f22"
477            | "f23"
478            | "f24"
479            | "f25"
480            | "f26"
481            | "f27"
482            | "f28"
483            | "f29"
484            | "f30"
485            | "f31"
486            | "f32"
487            | "f33"
488            | "f34"
489            | "f35"
490            | "backspace"
491            | "delete"
492            | "left"
493            | "right"
494            | "up"
495            | "down"
496            | "pageup"
497            | "pagedown"
498            | "insert"
499            | "home"
500            | "end"
501            | "back"
502            | "forward"
503            | "escape"
504    )
505}
506
507impl std::fmt::Display for Keystroke {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        display_modifiers(&self.modifiers, f)?;
510        display_key(&self.key, f)
511    }
512}
513
514impl std::fmt::Display for KeybindingKeystroke {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        display_modifiers(self.modifiers(), f)?;
517        display_key(self.key(), f)
518    }
519}
520
521/// The modifier keys held at some point in time.
522///
523/// Each field is `true` when that key is down. Use [`Modifiers::secondary`] for the shortcut key
524/// that matches each platform's convention, and the constructors such as [`Modifiers::shift`] to
525/// build a value.
526#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
527pub struct Modifiers {
528    /// The control key.
529    #[serde(default)]
530    pub control: bool,
531
532    /// The alt key, also known as option on macOS.
533    #[serde(default)]
534    pub alt: bool,
535
536    /// The shift key.
537    #[serde(default)]
538    pub shift: bool,
539
540    /// The platform key: command on macOS, the Windows key on Windows, and the super key on Linux.
541    #[serde(default)]
542    pub platform: bool,
543
544    /// The function key.
545    #[serde(default)]
546    pub function: bool,
547}
548
549impl Modifiers {
550    /// Returns whether any modifier key is held.
551    pub fn modified(&self) -> bool {
552        self.control || self.alt || self.shift || self.platform || self.function
553    }
554
555    /// Returns whether the platform's shortcut modifier is held.
556    ///
557    /// This is `platform` (command) on macOS and `control` on Linux and Windows. Use it for
558    /// shortcuts such as save and copy, so that they follow each platform's convention.
559    ///
560    /// # Examples
561    ///
562    /// ```
563    /// # use gpui::Modifiers;
564    /// assert!(Modifiers::secondary_key().secondary());
565    /// assert!(!Modifiers::shift().secondary());
566    /// ```
567    pub fn secondary(&self) -> bool {
568        #[cfg(target_os = "macos")]
569        {
570            self.platform
571        }
572
573        #[cfg(not(target_os = "macos"))]
574        {
575            self.control
576        }
577    }
578
579    /// Returns how many modifier keys are held.
580    pub fn number_of_modifiers(&self) -> u8 {
581        self.control as u8
582            + self.alt as u8
583            + self.shift as u8
584            + self.platform as u8
585            + self.function as u8
586    }
587
588    /// Returns [`Modifiers`] with no modifiers held.
589    pub fn none() -> Modifiers {
590        Default::default()
591    }
592
593    /// Returns [`Modifiers`] with only `platform` held: command on macOS, the Windows key on
594    /// Windows and the super key on Linux.
595    pub fn command() -> Modifiers {
596        Modifiers {
597            platform: true,
598            ..Default::default()
599        }
600    }
601
602    /// Returns [`Modifiers`] with only the [secondary](Modifiers::secondary) key held: `platform`
603    /// on macOS and `control` elsewhere.
604    pub fn secondary_key() -> Modifiers {
605        #[cfg(target_os = "macos")]
606        {
607            Modifiers {
608                platform: true,
609                ..Default::default()
610            }
611        }
612
613        #[cfg(not(target_os = "macos"))]
614        {
615            Modifiers {
616                control: true,
617                ..Default::default()
618            }
619        }
620    }
621
622    /// Returns [`Modifiers`] with only the Windows key held. This is `platform`, the same as
623    /// [`Modifiers::command`].
624    pub fn windows() -> Modifiers {
625        Modifiers {
626            platform: true,
627            ..Default::default()
628        }
629    }
630
631    /// Returns [`Modifiers`] with only the super key held. This is `platform`, the same as
632    /// [`Modifiers::command`].
633    pub fn super_key() -> Modifiers {
634        Modifiers {
635            platform: true,
636            ..Default::default()
637        }
638    }
639
640    /// Returns [`Modifiers`] with only control held.
641    pub fn control() -> Modifiers {
642        Modifiers {
643            control: true,
644            ..Default::default()
645        }
646    }
647
648    /// Returns [`Modifiers`] with only alt held.
649    pub fn alt() -> Modifiers {
650        Modifiers {
651            alt: true,
652            ..Default::default()
653        }
654    }
655
656    /// Returns [`Modifiers`] with only shift held.
657    pub fn shift() -> Modifiers {
658        Modifiers {
659            shift: true,
660            ..Default::default()
661        }
662    }
663
664    /// Returns [`Modifiers`] with only the function key held.
665    pub fn function() -> Modifiers {
666        Modifiers {
667            function: true,
668            ..Default::default()
669        }
670    }
671
672    /// Returns [`Modifiers`] with `platform` and `shift` held.
673    pub fn command_shift() -> Modifiers {
674        Modifiers {
675            shift: true,
676            platform: true,
677            ..Default::default()
678        }
679    }
680
681    /// Returns [`Modifiers`] with `control` and `shift` held.
682    pub fn control_shift() -> Modifiers {
683        Modifiers {
684            shift: true,
685            control: true,
686            ..Default::default()
687        }
688    }
689
690    /// Returns whether every modifier held in `self` is also held in `other`.
691    ///
692    /// `Modifiers::none()` is a subset of every value, and no value is a subset of `none()` unless
693    /// it is `none()` itself.
694    ///
695    /// # Examples
696    ///
697    /// ```
698    /// # use gpui::Modifiers;
699    /// let ctrl_shift = Modifiers::control_shift();
700    /// assert!(Modifiers::shift().is_subset_of(&ctrl_shift));
701    /// assert!(!Modifiers::alt().is_subset_of(&ctrl_shift));
702    /// ```
703    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
704        (*other & *self) == *self
705    }
706}
707
708impl std::ops::BitOr for Modifiers {
709    type Output = Self;
710
711    fn bitor(mut self, other: Self) -> Self::Output {
712        self |= other;
713        self
714    }
715}
716
717impl std::ops::BitOrAssign for Modifiers {
718    fn bitor_assign(&mut self, other: Self) {
719        self.control |= other.control;
720        self.alt |= other.alt;
721        self.shift |= other.shift;
722        self.platform |= other.platform;
723        self.function |= other.function;
724    }
725}
726
727impl std::ops::BitXor for Modifiers {
728    type Output = Self;
729    fn bitxor(mut self, rhs: Self) -> Self::Output {
730        self ^= rhs;
731        self
732    }
733}
734
735impl std::ops::BitXorAssign for Modifiers {
736    fn bitxor_assign(&mut self, other: Self) {
737        self.control ^= other.control;
738        self.alt ^= other.alt;
739        self.shift ^= other.shift;
740        self.platform ^= other.platform;
741        self.function ^= other.function;
742    }
743}
744
745impl std::ops::BitAnd for Modifiers {
746    type Output = Self;
747    fn bitand(mut self, rhs: Self) -> Self::Output {
748        self &= rhs;
749        self
750    }
751}
752
753impl std::ops::BitAndAssign for Modifiers {
754    fn bitand_assign(&mut self, other: Self) {
755        self.control &= other.control;
756        self.alt &= other.alt;
757        self.shift &= other.shift;
758        self.platform &= other.platform;
759        self.function &= other.function;
760    }
761}
762
763/// The state of the caps lock key at some point in time.
764#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
765pub struct Capslock {
766    /// Whether caps lock is on.
767    #[serde(default)]
768    pub on: bool,
769}
770
771impl AsKeystroke for Keystroke {
772    fn as_keystroke(&self) -> &Keystroke {
773        self
774    }
775}
776
777impl AsKeystroke for KeybindingKeystroke {
778    fn as_keystroke(&self) -> &Keystroke {
779        &self.inner
780    }
781}
782
783impl<T: AsKeystroke + ?Sized> AsKeystroke for &T {
784    fn as_keystroke(&self) -> &Keystroke {
785        (**self).as_keystroke()
786    }
787}
788
789fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
790    if modifiers.control {
791        #[cfg(target_os = "macos")]
792        f.write_char('^')?;
793
794        #[cfg(not(target_os = "macos"))]
795        write!(f, "ctrl-")?;
796    }
797    if modifiers.alt {
798        #[cfg(target_os = "macos")]
799        f.write_char('⌥')?;
800
801        #[cfg(not(target_os = "macos"))]
802        write!(f, "alt-")?;
803    }
804    if modifiers.platform {
805        #[cfg(target_os = "macos")]
806        f.write_char('⌘')?;
807
808        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
809        f.write_char('❖')?;
810
811        #[cfg(target_os = "windows")]
812        f.write_char('⊞')?;
813    }
814    if modifiers.shift {
815        #[cfg(target_os = "macos")]
816        f.write_char('⇧')?;
817
818        #[cfg(not(target_os = "macos"))]
819        write!(f, "shift-")?;
820    }
821    Ok(())
822}
823
824fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
825    let key = match key {
826        #[cfg(target_os = "macos")]
827        "backspace" => '⌫',
828        #[cfg(target_os = "macos")]
829        "up" => '↑',
830        #[cfg(target_os = "macos")]
831        "down" => '↓',
832        #[cfg(target_os = "macos")]
833        "left" => '←',
834        #[cfg(target_os = "macos")]
835        "right" => '→',
836        #[cfg(target_os = "macos")]
837        "tab" => '⇥',
838        #[cfg(target_os = "macos")]
839        "escape" => '⎋',
840        #[cfg(target_os = "macos")]
841        "shift" => '⇧',
842        #[cfg(target_os = "macos")]
843        "control" => '⌃',
844        #[cfg(target_os = "macos")]
845        "alt" => '⌥',
846        #[cfg(target_os = "macos")]
847        "platform" => '⌘',
848
849        key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
850        key => return f.write_str(key),
851    };
852    f.write_char(key)
853}
854
855#[inline]
856fn unparse(modifiers: &Modifiers, key: &str) -> String {
857    let mut result = String::new();
858    if modifiers.function {
859        result.push_str("fn-");
860    }
861    if modifiers.control {
862        result.push_str("ctrl-");
863    }
864    if modifiers.alt {
865        result.push_str("alt-");
866    }
867    if modifiers.platform {
868        #[cfg(target_os = "macos")]
869        result.push_str("cmd-");
870
871        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
872        result.push_str("super-");
873
874        #[cfg(target_os = "windows")]
875        result.push_str("win-");
876    }
877    if modifiers.shift {
878        result.push_str("shift-");
879    }
880    result.push_str(&key);
881    result
882}