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