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 modifiers \
 46    (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
 47    followed by a 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        let mut control = false;
 85        let mut alt = false;
 86        let mut shift = false;
 87        let mut platform = false;
 88        let mut function = false;
 89        let mut key = None;
 90        let mut key_char = None;
 91
 92        let mut components = source.split('-').peekable();
 93        while let Some(component) = components.next() {
 94            if component.eq_ignore_ascii_case("ctrl") {
 95                control = true;
 96                continue;
 97            }
 98            if component.eq_ignore_ascii_case("alt") {
 99                alt = true;
100                continue;
101            }
102            if component.eq_ignore_ascii_case("shift") {
103                shift = true;
104                continue;
105            }
106            if component.eq_ignore_ascii_case("fn") {
107                function = true;
108                continue;
109            }
110            if component.eq_ignore_ascii_case("secondary") {
111                if cfg!(target_os = "macos") {
112                    platform = true;
113                } else {
114                    control = true;
115                };
116                continue;
117            }
118
119            let is_platform = component.eq_ignore_ascii_case("cmd")
120                || component.eq_ignore_ascii_case("super")
121                || component.eq_ignore_ascii_case("win");
122
123            if is_platform {
124                platform = true;
125                continue;
126            }
127
128            let mut key_str = component.to_string();
129
130            if let Some(next) = components.peek() {
131                if next.is_empty() && source.ends_with('-') {
132                    key = Some(String::from("-"));
133                    break;
134                } else if next.len() > 1 && next.starts_with('>') {
135                    key = Some(key_str);
136                    key_char = Some(String::from(&next[1..]));
137                    components.next();
138                } else {
139                    return Err(InvalidKeystrokeError {
140                        keystroke: source.to_owned(),
141                    });
142                }
143                continue;
144            }
145
146            if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
147                // Convert to shift + lowercase char
148                shift = true;
149                key_str.make_ascii_lowercase();
150            } else {
151                // convert ascii chars to lowercase so that named keys like "tab" and "enter"
152                // are accepted case insensitively and stored how we expect so they are matched properly
153                key_str.make_ascii_lowercase()
154            }
155            key = Some(key_str);
156        }
157
158        // Allow for the user to specify a keystroke modifier as the key itself
159        // This sets the `key` to the modifier, and disables the modifier
160        if key.is_none() {
161            if shift {
162                key = Some("shift".to_string());
163                shift = false;
164            } else if control {
165                key = Some("control".to_string());
166                control = false;
167            } else if alt {
168                key = Some("alt".to_string());
169                alt = false;
170            } else if platform {
171                key = Some("platform".to_string());
172                platform = false;
173            } else if function {
174                key = Some("function".to_string());
175                function = false;
176            }
177        }
178
179        let key = key.ok_or_else(|| InvalidKeystrokeError {
180            keystroke: source.to_owned(),
181        })?;
182
183        Ok(Keystroke {
184            modifiers: Modifiers {
185                control,
186                alt,
187                shift,
188                platform,
189                function,
190            },
191            key,
192            key_char,
193        })
194    }
195
196    /// Produces a representation of this key that Parse can understand.
197    pub fn unparse(&self) -> String {
198        let mut str = String::new();
199        if self.modifiers.function {
200            str.push_str("fn-");
201        }
202        if self.modifiers.control {
203            str.push_str("ctrl-");
204        }
205        if self.modifiers.alt {
206            str.push_str("alt-");
207        }
208        if self.modifiers.platform {
209            #[cfg(target_os = "macos")]
210            str.push_str("cmd-");
211
212            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
213            str.push_str("super-");
214
215            #[cfg(target_os = "windows")]
216            str.push_str("win-");
217        }
218        if self.modifiers.shift {
219            str.push_str("shift-");
220        }
221        str.push_str(&self.key);
222        str
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
264fn is_printable_key(key: &str) -> bool {
265    !matches!(
266        key,
267        "f1" | "f2"
268            | "f3"
269            | "f4"
270            | "f5"
271            | "f6"
272            | "f7"
273            | "f8"
274            | "f9"
275            | "f10"
276            | "f11"
277            | "f12"
278            | "f13"
279            | "f14"
280            | "f15"
281            | "f16"
282            | "f17"
283            | "f18"
284            | "f19"
285            | "f20"
286            | "f21"
287            | "f22"
288            | "f23"
289            | "f24"
290            | "f25"
291            | "f26"
292            | "f27"
293            | "f28"
294            | "f29"
295            | "f30"
296            | "f31"
297            | "f32"
298            | "f33"
299            | "f34"
300            | "f35"
301            | "backspace"
302            | "delete"
303            | "left"
304            | "right"
305            | "up"
306            | "down"
307            | "pageup"
308            | "pagedown"
309            | "insert"
310            | "home"
311            | "end"
312            | "back"
313            | "forward"
314            | "escape"
315    )
316}
317
318impl std::fmt::Display for Keystroke {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        if self.modifiers.control {
321            if cfg!(target_os = "macos") {
322                f.write_char('^')?;
323            } else {
324                write!(f, "ctrl-")?;
325            }
326        }
327        if self.modifiers.alt {
328            if cfg!(target_os = "macos") {
329                f.write_char('โŒฅ')?;
330            } else {
331                write!(f, "alt-")?;
332            }
333        }
334        if self.modifiers.platform {
335            #[cfg(target_os = "macos")]
336            f.write_char('โŒ˜')?;
337
338            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
339            f.write_char('โ–')?;
340
341            #[cfg(target_os = "windows")]
342            f.write_char('โŠž')?;
343        }
344        if self.modifiers.shift {
345            if cfg!(target_os = "macos") {
346                f.write_char('โ‡ง')?;
347            } else {
348                write!(f, "shift-")?;
349            }
350        }
351        let key = match self.key.as_str() {
352            "backspace" if cfg!(target_os = "macos") => 'โŒซ',
353            "up" if cfg!(target_os = "macos") => 'โ†‘',
354            "down" if cfg!(target_os = "macos") => 'โ†“',
355            "left" if cfg!(target_os = "macos") => 'โ†',
356            "right" if cfg!(target_os = "macos") => 'โ†’',
357            "tab" if cfg!(target_os = "macos") => 'โ‡ฅ',
358            "escape" if cfg!(target_os = "macos") => 'โŽ‹',
359            "shift" if cfg!(target_os = "macos") => 'โ‡ง',
360            "control" if cfg!(target_os = "macos") => 'โŒƒ',
361            "alt" if cfg!(target_os = "macos") => 'โŒฅ',
362            "platform" if cfg!(target_os = "macos") => 'โŒ˜',
363            key => {
364                if key.len() == 1 {
365                    key.chars().next().unwrap().to_ascii_uppercase()
366                } else {
367                    return f.write_str(key);
368                }
369            }
370        };
371        f.write_char(key)
372    }
373}
374
375/// The state of the modifier keys at some point in time
376#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
377pub struct Modifiers {
378    /// The control key
379    #[serde(default)]
380    pub control: bool,
381
382    /// The alt key
383    /// Sometimes also known as the 'meta' key
384    #[serde(default)]
385    pub alt: bool,
386
387    /// The shift key
388    #[serde(default)]
389    pub shift: bool,
390
391    /// The command key, on macos
392    /// the windows key, on windows
393    /// the super key, on linux
394    #[serde(default)]
395    pub platform: bool,
396
397    /// The function key
398    #[serde(default)]
399    pub function: bool,
400}
401
402impl Modifiers {
403    /// Returns whether any modifier key is pressed.
404    pub fn modified(&self) -> bool {
405        self.control || self.alt || self.shift || self.platform || self.function
406    }
407
408    /// Whether the semantically 'secondary' modifier key is pressed.
409    ///
410    /// On macOS, this is the command key.
411    /// On Linux and Windows, this is the control key.
412    pub fn secondary(&self) -> bool {
413        #[cfg(target_os = "macos")]
414        {
415            self.platform
416        }
417
418        #[cfg(not(target_os = "macos"))]
419        {
420            self.control
421        }
422    }
423
424    /// Returns how many modifier keys are pressed.
425    pub fn number_of_modifiers(&self) -> u8 {
426        self.control as u8
427            + self.alt as u8
428            + self.shift as u8
429            + self.platform as u8
430            + self.function as u8
431    }
432
433    /// Returns [`Modifiers`] with no modifiers.
434    pub fn none() -> Modifiers {
435        Default::default()
436    }
437
438    /// Returns [`Modifiers`] with just the command key.
439    pub fn command() -> Modifiers {
440        Modifiers {
441            platform: true,
442            ..Default::default()
443        }
444    }
445
446    /// A Returns [`Modifiers`] with just the secondary key pressed.
447    pub fn secondary_key() -> Modifiers {
448        #[cfg(target_os = "macos")]
449        {
450            Modifiers {
451                platform: true,
452                ..Default::default()
453            }
454        }
455
456        #[cfg(not(target_os = "macos"))]
457        {
458            Modifiers {
459                control: true,
460                ..Default::default()
461            }
462        }
463    }
464
465    /// Returns [`Modifiers`] with just the windows key.
466    pub fn windows() -> Modifiers {
467        Modifiers {
468            platform: true,
469            ..Default::default()
470        }
471    }
472
473    /// Returns [`Modifiers`] with just the super key.
474    pub fn super_key() -> Modifiers {
475        Modifiers {
476            platform: true,
477            ..Default::default()
478        }
479    }
480
481    /// Returns [`Modifiers`] with just control.
482    pub fn control() -> Modifiers {
483        Modifiers {
484            control: true,
485            ..Default::default()
486        }
487    }
488
489    /// Returns [`Modifiers`] with just alt.
490    pub fn alt() -> Modifiers {
491        Modifiers {
492            alt: true,
493            ..Default::default()
494        }
495    }
496
497    /// Returns [`Modifiers`] with just shift.
498    pub fn shift() -> Modifiers {
499        Modifiers {
500            shift: true,
501            ..Default::default()
502        }
503    }
504
505    /// Returns [`Modifiers`] with command + shift.
506    pub fn command_shift() -> Modifiers {
507        Modifiers {
508            shift: true,
509            platform: true,
510            ..Default::default()
511        }
512    }
513
514    /// Returns [`Modifiers`] with command + shift.
515    pub fn control_shift() -> Modifiers {
516        Modifiers {
517            shift: true,
518            control: true,
519            ..Default::default()
520        }
521    }
522
523    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
524    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
525        (other.control || !self.control)
526            && (other.alt || !self.alt)
527            && (other.shift || !self.shift)
528            && (other.platform || !self.platform)
529            && (other.function || !self.function)
530    }
531}