keystroke.rs

  1use schemars::JsonSchema;
  2use serde::{Deserialize, Serialize};
  3use std::{
  4    error::Error,
  5    fmt::{Display, Write},
  6};
  7
  8/// A keystroke and associated metadata generated by the platform
  9#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
 10pub struct Keystroke {
 11    /// the state of the modifier keys at the time the keystroke was generated
 12    pub modifiers: Modifiers,
 13
 14    /// key is the character printed on the key that was pressed
 15    /// e.g. for option-s, key is "s"
 16    pub key: String,
 17
 18    /// key_char is the character that could have been typed when
 19    /// this binding was pressed.
 20    /// e.g. for s this is "s", for option-s "รŸ", and cmd-s None
 21    pub key_char: Option<String>,
 22}
 23
 24/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
 25/// markdown to display it.
 26#[derive(Debug)]
 27pub struct InvalidKeystrokeError {
 28    /// The invalid keystroke.
 29    pub keystroke: String,
 30}
 31
 32impl Error for InvalidKeystrokeError {}
 33
 34impl Display for InvalidKeystrokeError {
 35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 36        write!(
 37            f,
 38            "Invalid keystroke \"{}\". {}",
 39            self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
 40        )
 41    }
 42}
 43
 44/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
 45pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of lowercase modifiers \
 46    (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
 47    followed by a lowercase key, separated by `-`.";
 48
 49impl Keystroke {
 50    /// When matching a key we cannot know whether the user intended to type
 51    /// the key_char or the key itself. On some non-US keyboards keys we use in our
 52    /// bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
 53    /// and on some keyboards the IME handler converts a sequence of keys into a
 54    /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
 55    ///
 56    /// This method assumes that `self` was typed and `target' is in the keymap, and checks
 57    /// both possibilities for self against the target.
 58    pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
 59        if let Some(key_char) = self
 60            .key_char
 61            .as_ref()
 62            .filter(|key_char| key_char != &&self.key)
 63        {
 64            let ime_modifiers = Modifiers {
 65                control: self.modifiers.control,
 66                platform: self.modifiers.platform,
 67                ..Default::default()
 68            };
 69
 70            if &target.key == key_char && target.modifiers == ime_modifiers {
 71                return true;
 72            }
 73        }
 74
 75        target.modifiers == self.modifiers && target.key == self.key
 76    }
 77
 78    /// key syntax is:
 79    /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
 80    /// key_char syntax is only used for generating test events,
 81    /// secondary means "cmd" on macOS and "ctrl" on other platforms
 82    /// when matching a key with an key_char set will be matched without it.
 83    pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
 84        return Self::parse_impl(source, true);
 85    }
 86
 87    /// Parse a keystroke case-insensitively. This means
 88    /// keystrokes like `ctrl-T` will not be rejected.
 89    /// Useful in tests to allow more concise keystroke inputs,
 90    /// e.g., `simulate_keystrokes("ctrl-T")` instead of `simulate_keystrokes("ctrl-shift-t")`.
 91    /// This also allows `simulate_input` style functions to support capital letters,
 92    /// e.g., `simulate_input("Title Case")` can work by just parsing each character as a keystroke
 93    /// and dispatching it, instead of needing to parse something like
 94    /// `simulate_input("shift-title shift-case")`.
 95    #[cfg(any(test, feature = "test-support"))]
 96    pub fn parse_case_insensitive(
 97        source: &str,
 98    ) -> std::result::Result<Self, InvalidKeystrokeError> {
 99        return Self::parse_impl(source, false);
100    }
101
102    fn parse_impl(
103        source: &str,
104        case_sensitive: bool,
105    ) -> std::result::Result<Self, InvalidKeystrokeError> {
106        let mut control = false;
107        let mut alt = false;
108        let mut shift = false;
109        let mut platform = false;
110        let mut function = false;
111        let mut key = None;
112        let mut key_char = None;
113
114        let mut components = source.split('-').peekable();
115        while let Some(component) = components.next() {
116            if component.eq_ignore_ascii_case("ctrl") {
117                control = true;
118                continue;
119            }
120            if component.eq_ignore_ascii_case("alt") {
121                alt = true;
122                continue;
123            }
124            if component.eq_ignore_ascii_case("shift") {
125                shift = true;
126                continue;
127            }
128            if component.eq_ignore_ascii_case("fn") {
129                function = true;
130                continue;
131            }
132            if component.eq_ignore_ascii_case("secondary") {
133                if cfg!(target_os = "macos") {
134                    platform = true;
135                } else {
136                    control = true;
137                };
138                continue;
139            }
140
141            let is_platform = component.eq_ignore_ascii_case("cmd")
142                || component.eq_ignore_ascii_case("super")
143                || component.eq_ignore_ascii_case("win");
144
145            if is_platform {
146                platform = true;
147                continue;
148            }
149
150            let mut key_str = component.to_string();
151
152            if let Some(next) = components.peek() {
153                if next.is_empty() && source.ends_with('-') {
154                    key = Some(String::from("-"));
155                    break;
156                } else if next.len() > 1 && next.starts_with('>') {
157                    key = Some(key_str);
158                    key_char = Some(String::from(&next[1..]));
159                    components.next();
160                } else {
161                    return Err(InvalidKeystrokeError {
162                        keystroke: source.to_owned(),
163                    });
164                }
165                continue;
166            }
167
168            if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
169                if case_sensitive {
170                    return Err(InvalidKeystrokeError {
171                        keystroke: source.to_owned(),
172                    });
173                } else {
174                    // Convert to shift + lowercase char if parsing case insensitively
175                    shift = true;
176                    key_str.make_ascii_lowercase();
177                }
178            } else if case_sensitive {
179                // convert ascii chars to lowercase so that named keys like "tab" and "enter"
180                // are accepted case insensitively and stored how we expect so they are matched properly
181                key_str.make_ascii_lowercase()
182            }
183            key = Some(key_str);
184        }
185
186        // Allow for the user to specify a keystroke modifier as the key itself
187        // This sets the `key` to the modifier, and disables the modifier
188        if key.is_none() {
189            if shift {
190                key = Some("shift".to_string());
191                shift = false;
192            } else if control {
193                key = Some("control".to_string());
194                control = false;
195            } else if alt {
196                key = Some("alt".to_string());
197                alt = false;
198            } else if platform {
199                key = Some("platform".to_string());
200                platform = false;
201            } else if function {
202                key = Some("function".to_string());
203                function = false;
204            }
205        }
206
207        let key = key.ok_or_else(|| InvalidKeystrokeError {
208            keystroke: source.to_owned(),
209        })?;
210
211        Ok(Keystroke {
212            modifiers: Modifiers {
213                control,
214                alt,
215                shift,
216                platform,
217                function,
218            },
219            key,
220            key_char,
221        })
222    }
223
224    /// Produces a representation of this key that Parse can understand.
225    pub fn unparse(&self) -> String {
226        let mut str = String::new();
227        if self.modifiers.function {
228            str.push_str("fn-");
229        }
230        if self.modifiers.control {
231            str.push_str("ctrl-");
232        }
233        if self.modifiers.alt {
234            str.push_str("alt-");
235        }
236        if self.modifiers.platform {
237            #[cfg(target_os = "macos")]
238            str.push_str("cmd-");
239
240            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
241            str.push_str("super-");
242
243            #[cfg(target_os = "windows")]
244            str.push_str("win-");
245        }
246        if self.modifiers.shift {
247            str.push_str("shift-");
248        }
249        str.push_str(&self.key);
250        str
251    }
252
253    /// Returns true if this keystroke left
254    /// the ime system in an incomplete state.
255    pub fn is_ime_in_progress(&self) -> bool {
256        self.key_char.is_none()
257            && (is_printable_key(&self.key) || self.key.is_empty())
258            && !(self.modifiers.platform
259                || self.modifiers.control
260                || self.modifiers.function
261                || self.modifiers.alt)
262    }
263
264    /// Returns a new keystroke with the key_char filled.
265    /// This is used for dispatch_keystroke where we want users to
266    /// be able to simulate typing "space", etc.
267    pub fn with_simulated_ime(mut self) -> Self {
268        if self.key_char.is_none()
269            && !self.modifiers.platform
270            && !self.modifiers.control
271            && !self.modifiers.function
272            && !self.modifiers.alt
273        {
274            self.key_char = match self.key.as_str() {
275                "space" => Some(" ".into()),
276                "tab" => Some("\t".into()),
277                "enter" => Some("\n".into()),
278                key if !is_printable_key(key) || key.is_empty() => None,
279                key => {
280                    if self.modifiers.shift {
281                        Some(key.to_uppercase())
282                    } else {
283                        Some(key.into())
284                    }
285                }
286            }
287        }
288        self
289    }
290}
291
292fn is_printable_key(key: &str) -> bool {
293    !matches!(
294        key,
295        "f1" | "f2"
296            | "f3"
297            | "f4"
298            | "f5"
299            | "f6"
300            | "f7"
301            | "f8"
302            | "f9"
303            | "f10"
304            | "f11"
305            | "f12"
306            | "f13"
307            | "f14"
308            | "f15"
309            | "f16"
310            | "f17"
311            | "f18"
312            | "f19"
313            | "f20"
314            | "f21"
315            | "f22"
316            | "f23"
317            | "f24"
318            | "f25"
319            | "f26"
320            | "f27"
321            | "f28"
322            | "f29"
323            | "f30"
324            | "f31"
325            | "f32"
326            | "f33"
327            | "f34"
328            | "f35"
329            | "backspace"
330            | "delete"
331            | "left"
332            | "right"
333            | "up"
334            | "down"
335            | "pageup"
336            | "pagedown"
337            | "insert"
338            | "home"
339            | "end"
340            | "back"
341            | "forward"
342            | "escape"
343    )
344}
345
346impl std::fmt::Display for Keystroke {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        if self.modifiers.control {
349            f.write_char('^')?;
350        }
351        if self.modifiers.alt {
352            f.write_char('โŒฅ')?;
353        }
354        if self.modifiers.platform {
355            #[cfg(target_os = "macos")]
356            f.write_char('โŒ˜')?;
357
358            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
359            f.write_char('โ–')?;
360
361            #[cfg(target_os = "windows")]
362            f.write_char('โŠž')?;
363        }
364        if self.modifiers.shift {
365            f.write_char('โ‡ง')?;
366        }
367        let key = match self.key.as_str() {
368            "backspace" => 'โŒซ',
369            "up" => 'โ†‘',
370            "down" => 'โ†“',
371            "left" => 'โ†',
372            "right" => 'โ†’',
373            "tab" => 'โ‡ฅ',
374            "escape" => 'โŽ‹',
375            "shift" => 'โ‡ง',
376            "control" => 'โŒƒ',
377            "alt" => 'โŒฅ',
378            "platform" => 'โŒ˜',
379            key => {
380                if key.len() == 1 {
381                    key.chars().next().unwrap().to_ascii_uppercase()
382                } else {
383                    return f.write_str(key);
384                }
385            }
386        };
387        f.write_char(key)
388    }
389}
390
391/// The state of the modifier keys at some point in time
392#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
393pub struct Modifiers {
394    /// The control key
395    #[serde(default)]
396    pub control: bool,
397
398    /// The alt key
399    /// Sometimes also known as the 'meta' key
400    #[serde(default)]
401    pub alt: bool,
402
403    /// The shift key
404    #[serde(default)]
405    pub shift: bool,
406
407    /// The command key, on macos
408    /// the windows key, on windows
409    /// the super key, on linux
410    #[serde(default)]
411    pub platform: bool,
412
413    /// The function key
414    #[serde(default)]
415    pub function: bool,
416}
417
418impl Modifiers {
419    /// Returns whether any modifier key is pressed.
420    pub fn modified(&self) -> bool {
421        self.control || self.alt || self.shift || self.platform || self.function
422    }
423
424    /// Whether the semantically 'secondary' modifier key is pressed.
425    ///
426    /// On macOS, this is the command key.
427    /// On Linux and Windows, this is the control key.
428    pub fn secondary(&self) -> bool {
429        #[cfg(target_os = "macos")]
430        {
431            self.platform
432        }
433
434        #[cfg(not(target_os = "macos"))]
435        {
436            self.control
437        }
438    }
439
440    /// Returns how many modifier keys are pressed.
441    pub fn number_of_modifiers(&self) -> u8 {
442        self.control as u8
443            + self.alt as u8
444            + self.shift as u8
445            + self.platform as u8
446            + self.function as u8
447    }
448
449    /// Returns [`Modifiers`] with no modifiers.
450    pub fn none() -> Modifiers {
451        Default::default()
452    }
453
454    /// Returns [`Modifiers`] with just the command key.
455    pub fn command() -> Modifiers {
456        Modifiers {
457            platform: true,
458            ..Default::default()
459        }
460    }
461
462    /// A Returns [`Modifiers`] with just the secondary key pressed.
463    pub fn secondary_key() -> Modifiers {
464        #[cfg(target_os = "macos")]
465        {
466            Modifiers {
467                platform: true,
468                ..Default::default()
469            }
470        }
471
472        #[cfg(not(target_os = "macos"))]
473        {
474            Modifiers {
475                control: true,
476                ..Default::default()
477            }
478        }
479    }
480
481    /// Returns [`Modifiers`] with just the windows key.
482    pub fn windows() -> Modifiers {
483        Modifiers {
484            platform: true,
485            ..Default::default()
486        }
487    }
488
489    /// Returns [`Modifiers`] with just the super key.
490    pub fn super_key() -> Modifiers {
491        Modifiers {
492            platform: true,
493            ..Default::default()
494        }
495    }
496
497    /// Returns [`Modifiers`] with just control.
498    pub fn control() -> Modifiers {
499        Modifiers {
500            control: true,
501            ..Default::default()
502        }
503    }
504
505    /// Returns [`Modifiers`] with just alt.
506    pub fn alt() -> Modifiers {
507        Modifiers {
508            alt: true,
509            ..Default::default()
510        }
511    }
512
513    /// Returns [`Modifiers`] with just shift.
514    pub fn shift() -> Modifiers {
515        Modifiers {
516            shift: true,
517            ..Default::default()
518        }
519    }
520
521    /// Returns [`Modifiers`] with command + shift.
522    pub fn command_shift() -> Modifiers {
523        Modifiers {
524            shift: true,
525            platform: true,
526            ..Default::default()
527        }
528    }
529
530    /// Returns [`Modifiers`] with command + shift.
531    pub fn control_shift() -> Modifiers {
532        Modifiers {
533            shift: true,
534            control: true,
535            ..Default::default()
536        }
537    }
538
539    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
540    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
541        (other.control || !self.control)
542            && (other.alt || !self.alt)
543            && (other.shift || !self.shift)
544            && (other.platform || !self.platform)
545            && (other.function || !self.function)
546    }
547}