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::{OperatingSystem, 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    /// Parses a static keystroke with explicit, platform-independent modifiers.
116    ///
117    /// Syntax: `[ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]`.
118    /// The `key_char` suffix is used for generating test events.
119    /// Use [`Self::parse_with_os`] for dynamic input or the `secondary-` alias.
120    pub fn parse(source: &'static str) -> std::result::Result<Self, InvalidKeystrokeError> {
121        Self::parse_internal(source, None)
122    }
123
124    /// Parses a keystroke, resolving `secondary-` using the supplied host OS.
125    pub fn parse_with_os(
126        source: &str,
127        os: OperatingSystem,
128    ) -> std::result::Result<Self, InvalidKeystrokeError> {
129        Self::parse_internal(source, Some(os))
130    }
131
132    fn parse_internal(
133        source: &str,
134        os: Option<OperatingSystem>,
135    ) -> std::result::Result<Self, InvalidKeystrokeError> {
136        let mut modifiers = Modifiers::none();
137        let mut key = None;
138        let mut key_char = None;
139
140        let mut components = source.split('-').peekable();
141        while let Some(component) = components.next() {
142            if component.eq_ignore_ascii_case("ctrl") {
143                modifiers.control = true;
144                continue;
145            }
146            if component.eq_ignore_ascii_case("alt") {
147                modifiers.alt = true;
148                continue;
149            }
150            if component.eq_ignore_ascii_case("shift") {
151                modifiers.shift = true;
152                continue;
153            }
154            if component.eq_ignore_ascii_case("fn") {
155                modifiers.function = true;
156                continue;
157            }
158            if component.eq_ignore_ascii_case("secondary") {
159                let os = os.ok_or_else(|| InvalidKeystrokeError {
160                    keystroke: source.to_owned(),
161                })?;
162                if os == OperatingSystem::Mac {
163                    modifiers.platform = true;
164                } else {
165                    modifiers.control = true;
166                };
167                continue;
168            }
169
170            let is_platform = component.eq_ignore_ascii_case("cmd")
171                || component.eq_ignore_ascii_case("super")
172                || component.eq_ignore_ascii_case("win");
173
174            if is_platform {
175                modifiers.platform = true;
176                continue;
177            }
178
179            let mut key_str = component.to_string();
180
181            if let Some(next) = components.peek() {
182                if next.is_empty() && source.ends_with('-') {
183                    key = Some(String::from("-"));
184                    break;
185                } else if next.len() > 1 && next.starts_with('>') {
186                    key = Some(key_str);
187                    key_char = Some(String::from(&next[1..]));
188                    components.next();
189                } else {
190                    return Err(InvalidKeystrokeError {
191                        keystroke: source.to_owned(),
192                    });
193                }
194                continue;
195            }
196
197            if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
198                // Convert to shift + lowercase char
199                modifiers.shift = true;
200                key_str.make_ascii_lowercase();
201            } else {
202                // convert ascii chars to lowercase so that named keys like "tab" and "enter"
203                // are accepted case insensitively and stored how we expect so they are matched properly
204                key_str.make_ascii_lowercase()
205            }
206            key = Some(key_str);
207        }
208
209        // Allow for the user to specify a keystroke modifier as the key itself
210        // This sets the `key` to the modifier, and disables the modifier
211        key = key.or_else(|| {
212            use std::mem;
213            // std::mem::take clears bool incase its true
214            if mem::take(&mut modifiers.shift) {
215                Some("shift".to_string())
216            } else if mem::take(&mut modifiers.control) {
217                Some("control".to_string())
218            } else if mem::take(&mut modifiers.alt) {
219                Some("alt".to_string())
220            } else if mem::take(&mut modifiers.platform) {
221                Some("platform".to_string())
222            } else if mem::take(&mut modifiers.function) {
223                Some("function".to_string())
224            } else {
225                None
226            }
227        });
228
229        let key = key.ok_or_else(|| InvalidKeystrokeError {
230            keystroke: source.to_owned(),
231        })?;
232
233        Ok(Keystroke {
234            modifiers,
235            key,
236            key_char,
237        })
238    }
239
240    /// Serializes this keystroke with the supplied host OS's modifier spelling.
241    pub fn unparse(&self, os: OperatingSystem) -> String {
242        unparse(&self.modifiers, &self.key, os)
243    }
244
245    /// Formats this keystroke using the supplied host OS's key labels.
246    pub fn display(&self, os: OperatingSystem) -> KeystrokeDisplay<'_> {
247        KeystrokeDisplay {
248            modifiers: &self.modifiers,
249            key: &self.key,
250            os,
251        }
252    }
253
254    /// Returns true if this keystroke left
255    /// the ime system in an incomplete state.
256    pub fn is_ime_in_progress(&self) -> bool {
257        self.key_char.is_none()
258            && (is_printable_key(&self.key) || self.key.is_empty())
259            && !(self.modifiers.platform
260                || self.modifiers.control
261                || self.modifiers.function
262                || self.modifiers.alt)
263    }
264
265    /// Returns a new keystroke with the key_char filled.
266    /// This is used for dispatch_keystroke where we want users to
267    /// be able to simulate typing "space", etc.
268    pub fn with_simulated_ime(mut self) -> Self {
269        if self.key_char.is_none()
270            && !self.modifiers.platform
271            && !self.modifiers.control
272            && !self.modifiers.function
273            && !self.modifiers.alt
274        {
275            self.key_char = match self.key.as_str() {
276                "space" => Some(" ".into()),
277                "tab" => Some("\t".into()),
278                "enter" => Some("\n".into()),
279                key if !is_printable_key(key) || key.is_empty() => None,
280                key => {
281                    if self.modifiers.shift {
282                        Some(key.to_uppercase())
283                    } else {
284                        Some(key.into())
285                    }
286                }
287            }
288        }
289        self
290    }
291
292    /// Returns whether the key itself is a modifier
293    pub fn is_modifier_key(&self) -> bool {
294        matches!(
295            self.key.as_str(),
296            "shift" | "control" | "alt" | "platform" | "function"
297        )
298    }
299}
300
301impl KeybindingKeystroke {
302    /// Formats this binding using the supplied host OS's key labels.
303    pub fn display(&self, os: OperatingSystem) -> KeystrokeDisplay<'_> {
304        KeystrokeDisplay {
305            modifiers: self.modifiers(),
306            key: self.key(),
307            os,
308        }
309    }
310
311    #[cfg(target_os = "windows")]
312    #[expect(missing_docs)]
313    pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
314        KeybindingKeystroke {
315            inner,
316            display_modifiers,
317            display_key,
318        }
319    }
320
321    /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
322    pub fn new_with_mapper(
323        inner: Keystroke,
324        use_key_equivalents: bool,
325        keyboard_mapper: &dyn PlatformKeyboardMapper,
326    ) -> Self {
327        keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
328    }
329
330    /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
331    pub fn from_keystroke(keystroke: Keystroke) -> Self {
332        #[cfg(target_os = "windows")]
333        {
334            let key = keystroke.key.clone();
335            let modifiers = keystroke.modifiers;
336            KeybindingKeystroke {
337                inner: keystroke,
338                display_modifiers: modifiers,
339                display_key: key,
340            }
341        }
342        #[cfg(not(target_os = "windows"))]
343        {
344            KeybindingKeystroke { inner: keystroke }
345        }
346    }
347
348    /// Returns the GPUI representation of the keystroke.
349    pub fn inner(&self) -> &Keystroke {
350        &self.inner
351    }
352
353    /// Returns the modifiers.
354    ///
355    /// Platform-specific behavior:
356    /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
357    /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
358    /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
359    pub fn modifiers(&self) -> &Modifiers {
360        #[cfg(target_os = "windows")]
361        {
362            &self.display_modifiers
363        }
364        #[cfg(not(target_os = "windows"))]
365        {
366            &self.inner.modifiers
367        }
368    }
369
370    /// Returns the key.
371    ///
372    /// Platform-specific behavior:
373    /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
374    /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
375    pub fn key(&self) -> &str {
376        #[cfg(target_os = "windows")]
377        {
378            &self.display_key
379        }
380        #[cfg(not(target_os = "windows"))]
381        {
382            &self.inner.key
383        }
384    }
385
386    /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
387    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
388        self.inner.modifiers = modifiers;
389        #[cfg(target_os = "windows")]
390        {
391            self.display_modifiers = modifiers;
392        }
393    }
394
395    /// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
396    pub fn set_key(&mut self, key: String) {
397        #[cfg(target_os = "windows")]
398        {
399            self.display_key = key.clone();
400        }
401        self.inner.key = key;
402    }
403
404    /// Serializes this binding with the supplied host OS's modifier spelling.
405    pub fn unparse(&self, os: OperatingSystem) -> String {
406        #[cfg(target_os = "windows")]
407        {
408            unparse(&self.display_modifiers, &self.display_key, os)
409        }
410        #[cfg(not(target_os = "windows"))]
411        {
412            unparse(&self.inner.modifiers, &self.inner.key, os)
413        }
414    }
415
416    /// Removes the key_char
417    pub fn remove_key_char(&mut self) {
418        self.inner.key_char = None;
419    }
420}
421
422fn is_printable_key(key: &str) -> bool {
423    !matches!(
424        key,
425        "f1" | "f2"
426            | "f3"
427            | "f4"
428            | "f5"
429            | "f6"
430            | "f7"
431            | "f8"
432            | "f9"
433            | "f10"
434            | "f11"
435            | "f12"
436            | "f13"
437            | "f14"
438            | "f15"
439            | "f16"
440            | "f17"
441            | "f18"
442            | "f19"
443            | "f20"
444            | "f21"
445            | "f22"
446            | "f23"
447            | "f24"
448            | "f25"
449            | "f26"
450            | "f27"
451            | "f28"
452            | "f29"
453            | "f30"
454            | "f31"
455            | "f32"
456            | "f33"
457            | "f34"
458            | "f35"
459            | "backspace"
460            | "delete"
461            | "left"
462            | "right"
463            | "up"
464            | "down"
465            | "pageup"
466            | "pagedown"
467            | "insert"
468            | "home"
469            | "end"
470            | "back"
471            | "forward"
472            | "escape"
473    )
474}
475
476/// A keystroke formatted with an explicit host operating system.
477pub struct KeystrokeDisplay<'a> {
478    modifiers: &'a Modifiers,
479    key: &'a str,
480    os: OperatingSystem,
481}
482
483impl std::fmt::Display for KeystrokeDisplay<'_> {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        display_modifiers(self.modifiers, self.os, f)?;
486        display_key(self.key, self.os, f)
487    }
488}
489
490/// The state of the modifier keys at some point in time
491#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
492pub struct Modifiers {
493    /// The control key
494    #[serde(default)]
495    pub control: bool,
496
497    /// The alt key
498    /// Sometimes also known as the 'meta' key
499    #[serde(default)]
500    pub alt: bool,
501
502    /// The shift key
503    #[serde(default)]
504    pub shift: bool,
505
506    /// The command key, on macos
507    /// the windows key, on windows
508    /// the super key, on linux
509    #[serde(default)]
510    pub platform: bool,
511
512    /// The function key
513    #[serde(default)]
514    pub function: bool,
515}
516
517impl Modifiers {
518    /// Returns whether any modifier key is pressed.
519    pub fn modified(&self) -> bool {
520        self.control || self.alt || self.shift || self.platform || self.function
521    }
522
523    /// Whether the semantically 'secondary' modifier key is pressed.
524    ///
525    /// On macOS, this is the command key.
526    /// On Linux and Windows, this is the control key.
527    pub fn secondary(&self, cx: &crate::App) -> bool {
528        if cx.platform_info().is_mac() {
529            self.platform
530        } else {
531            self.control
532        }
533    }
534
535    /// Returns how many modifier keys are pressed.
536    pub fn number_of_modifiers(&self) -> u8 {
537        self.control as u8
538            + self.alt as u8
539            + self.shift as u8
540            + self.platform as u8
541            + self.function as u8
542    }
543
544    /// Returns [`Modifiers`] with no modifiers.
545    pub fn none() -> Modifiers {
546        Default::default()
547    }
548
549    /// Returns [`Modifiers`] with just the command key.
550    pub fn command() -> Modifiers {
551        Modifiers {
552            platform: true,
553            ..Default::default()
554        }
555    }
556
557    /// Returns [`Modifiers`] with just the secondary key pressed.
558    pub fn secondary_key(cx: &crate::App) -> Modifiers {
559        if cx.platform_info().is_mac() {
560            Modifiers {
561                platform: true,
562                ..Default::default()
563            }
564        } else {
565            Modifiers {
566                control: true,
567                ..Default::default()
568            }
569        }
570    }
571
572    /// Returns [`Modifiers`] with just the windows key.
573    pub fn windows() -> Modifiers {
574        Modifiers {
575            platform: true,
576            ..Default::default()
577        }
578    }
579
580    /// Returns [`Modifiers`] with just the super key.
581    pub fn super_key() -> Modifiers {
582        Modifiers {
583            platform: true,
584            ..Default::default()
585        }
586    }
587
588    /// Returns [`Modifiers`] with just control.
589    pub fn control() -> Modifiers {
590        Modifiers {
591            control: true,
592            ..Default::default()
593        }
594    }
595
596    /// Returns [`Modifiers`] with just alt.
597    pub fn alt() -> Modifiers {
598        Modifiers {
599            alt: true,
600            ..Default::default()
601        }
602    }
603
604    /// Returns [`Modifiers`] with just shift.
605    pub fn shift() -> Modifiers {
606        Modifiers {
607            shift: true,
608            ..Default::default()
609        }
610    }
611
612    /// Returns [`Modifiers`] with just function.
613    pub fn function() -> Modifiers {
614        Modifiers {
615            function: true,
616            ..Default::default()
617        }
618    }
619
620    /// Returns [`Modifiers`] with command + shift.
621    pub fn command_shift() -> Modifiers {
622        Modifiers {
623            shift: true,
624            platform: true,
625            ..Default::default()
626        }
627    }
628
629    /// Returns [`Modifiers`] with command + shift.
630    pub fn control_shift() -> Modifiers {
631        Modifiers {
632            shift: true,
633            control: true,
634            ..Default::default()
635        }
636    }
637
638    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
639    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
640        (*other & *self) == *self
641    }
642}
643
644impl std::ops::BitOr for Modifiers {
645    type Output = Self;
646
647    fn bitor(mut self, other: Self) -> Self::Output {
648        self |= other;
649        self
650    }
651}
652
653impl std::ops::BitOrAssign for Modifiers {
654    fn bitor_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
663impl std::ops::BitXor for Modifiers {
664    type Output = Self;
665    fn bitxor(mut self, rhs: Self) -> Self::Output {
666        self ^= rhs;
667        self
668    }
669}
670
671impl std::ops::BitXorAssign for Modifiers {
672    fn bitxor_assign(&mut self, other: Self) {
673        self.control ^= other.control;
674        self.alt ^= other.alt;
675        self.shift ^= other.shift;
676        self.platform ^= other.platform;
677        self.function ^= other.function;
678    }
679}
680
681impl std::ops::BitAnd for Modifiers {
682    type Output = Self;
683    fn bitand(mut self, rhs: Self) -> Self::Output {
684        self &= rhs;
685        self
686    }
687}
688
689impl std::ops::BitAndAssign for Modifiers {
690    fn bitand_assign(&mut self, other: Self) {
691        self.control &= other.control;
692        self.alt &= other.alt;
693        self.shift &= other.shift;
694        self.platform &= other.platform;
695        self.function &= other.function;
696    }
697}
698
699/// The state of the capslock key at some point in time
700#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
701pub struct Capslock {
702    /// The capslock key is on
703    #[serde(default)]
704    pub on: bool,
705}
706
707impl AsKeystroke for Keystroke {
708    fn as_keystroke(&self) -> &Keystroke {
709        self
710    }
711}
712
713impl AsKeystroke for KeybindingKeystroke {
714    fn as_keystroke(&self) -> &Keystroke {
715        &self.inner
716    }
717}
718
719impl<T: AsKeystroke + ?Sized> AsKeystroke for &T {
720    fn as_keystroke(&self) -> &Keystroke {
721        (**self).as_keystroke()
722    }
723}
724
725fn display_modifiers(
726    modifiers: &Modifiers,
727    os: OperatingSystem,
728    f: &mut std::fmt::Formatter<'_>,
729) -> std::fmt::Result {
730    if modifiers.control {
731        f.write_str(if os == OperatingSystem::Mac {
732            "^"
733        } else {
734            "ctrl-"
735        })?;
736    }
737    if modifiers.alt {
738        f.write_str(if os == OperatingSystem::Mac {
739            "⌥"
740        } else {
741            "alt-"
742        })?;
743    }
744    if modifiers.platform {
745        f.write_str(match os {
746            OperatingSystem::Mac => "⌘",
747            OperatingSystem::Windows => "⊞",
748            OperatingSystem::Linux | OperatingSystem::FreeBsd => "❖",
749            OperatingSystem::Ios | OperatingSystem::Android | OperatingSystem::Unknown => "cmd-",
750        })?;
751    }
752    if modifiers.shift {
753        f.write_str(if os == OperatingSystem::Mac {
754            "⇧"
755        } else {
756            "shift-"
757        })?;
758    }
759    Ok(())
760}
761
762fn display_key(
763    key: &str,
764    os: OperatingSystem,
765    f: &mut std::fmt::Formatter<'_>,
766) -> std::fmt::Result {
767    let key = match key {
768        "backspace" if os == OperatingSystem::Mac => '⌫',
769        "up" if os == OperatingSystem::Mac => '↑',
770        "down" if os == OperatingSystem::Mac => '↓',
771        "left" if os == OperatingSystem::Mac => '←',
772        "right" if os == OperatingSystem::Mac => '→',
773        "tab" if os == OperatingSystem::Mac => '⇥',
774        "escape" if os == OperatingSystem::Mac => '⎋',
775        "shift" if os == OperatingSystem::Mac => '⇧',
776        "control" if os == OperatingSystem::Mac => '⌃',
777        "alt" if os == OperatingSystem::Mac => '⌥',
778        "platform" if os == OperatingSystem::Mac => '⌘',
779
780        key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
781        key => return f.write_str(key),
782    };
783    f.write_char(key)
784}
785
786#[inline]
787fn unparse(modifiers: &Modifiers, key: &str, os: OperatingSystem) -> String {
788    let mut result = String::new();
789    if modifiers.function {
790        result.push_str("fn-");
791    }
792    if modifiers.control {
793        result.push_str("ctrl-");
794    }
795    if modifiers.alt {
796        result.push_str("alt-");
797    }
798    if modifiers.platform {
799        result.push_str(match os {
800            OperatingSystem::Windows => "win-",
801            OperatingSystem::Linux
802            | OperatingSystem::FreeBsd
803            | OperatingSystem::Android
804            | OperatingSystem::Unknown => "super-",
805            OperatingSystem::Mac | OperatingSystem::Ios => "cmd-",
806        });
807    }
808    if modifiers.shift {
809        result.push_str("shift-");
810    }
811    result.push_str(&key);
812    result
813}