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            f.write_char('^')?;
322        }
323        if self.modifiers.alt {
324            f.write_char('โŒฅ')?;
325        }
326        if self.modifiers.platform {
327            #[cfg(target_os = "macos")]
328            f.write_char('โŒ˜')?;
329
330            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
331            f.write_char('โ–')?;
332
333            #[cfg(target_os = "windows")]
334            f.write_char('โŠž')?;
335        }
336        if self.modifiers.shift {
337            f.write_char('โ‡ง')?;
338        }
339        let key = match self.key.as_str() {
340            "backspace" => 'โŒซ',
341            "up" => 'โ†‘',
342            "down" => 'โ†“',
343            "left" => 'โ†',
344            "right" => 'โ†’',
345            "tab" => 'โ‡ฅ',
346            "escape" => 'โŽ‹',
347            "shift" => 'โ‡ง',
348            "control" => 'โŒƒ',
349            "alt" => 'โŒฅ',
350            "platform" => 'โŒ˜',
351            key => {
352                if key.len() == 1 {
353                    key.chars().next().unwrap().to_ascii_uppercase()
354                } else {
355                    return f.write_str(key);
356                }
357            }
358        };
359        f.write_char(key)
360    }
361}
362
363/// The state of the modifier keys at some point in time
364#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
365pub struct Modifiers {
366    /// The control key
367    #[serde(default)]
368    pub control: bool,
369
370    /// The alt key
371    /// Sometimes also known as the 'meta' key
372    #[serde(default)]
373    pub alt: bool,
374
375    /// The shift key
376    #[serde(default)]
377    pub shift: bool,
378
379    /// The command key, on macos
380    /// the windows key, on windows
381    /// the super key, on linux
382    #[serde(default)]
383    pub platform: bool,
384
385    /// The function key
386    #[serde(default)]
387    pub function: bool,
388}
389
390impl Modifiers {
391    /// Returns whether any modifier key is pressed.
392    pub fn modified(&self) -> bool {
393        self.control || self.alt || self.shift || self.platform || self.function
394    }
395
396    /// Whether the semantically 'secondary' modifier key is pressed.
397    ///
398    /// On macOS, this is the command key.
399    /// On Linux and Windows, this is the control key.
400    pub fn secondary(&self) -> bool {
401        #[cfg(target_os = "macos")]
402        {
403            self.platform
404        }
405
406        #[cfg(not(target_os = "macos"))]
407        {
408            self.control
409        }
410    }
411
412    /// Returns how many modifier keys are pressed.
413    pub fn number_of_modifiers(&self) -> u8 {
414        self.control as u8
415            + self.alt as u8
416            + self.shift as u8
417            + self.platform as u8
418            + self.function as u8
419    }
420
421    /// Returns [`Modifiers`] with no modifiers.
422    pub fn none() -> Modifiers {
423        Default::default()
424    }
425
426    /// Returns [`Modifiers`] with just the command key.
427    pub fn command() -> Modifiers {
428        Modifiers {
429            platform: true,
430            ..Default::default()
431        }
432    }
433
434    /// A Returns [`Modifiers`] with just the secondary key pressed.
435    pub fn secondary_key() -> Modifiers {
436        #[cfg(target_os = "macos")]
437        {
438            Modifiers {
439                platform: true,
440                ..Default::default()
441            }
442        }
443
444        #[cfg(not(target_os = "macos"))]
445        {
446            Modifiers {
447                control: true,
448                ..Default::default()
449            }
450        }
451    }
452
453    /// Returns [`Modifiers`] with just the windows key.
454    pub fn windows() -> Modifiers {
455        Modifiers {
456            platform: true,
457            ..Default::default()
458        }
459    }
460
461    /// Returns [`Modifiers`] with just the super key.
462    pub fn super_key() -> Modifiers {
463        Modifiers {
464            platform: true,
465            ..Default::default()
466        }
467    }
468
469    /// Returns [`Modifiers`] with just control.
470    pub fn control() -> Modifiers {
471        Modifiers {
472            control: true,
473            ..Default::default()
474        }
475    }
476
477    /// Returns [`Modifiers`] with just alt.
478    pub fn alt() -> Modifiers {
479        Modifiers {
480            alt: true,
481            ..Default::default()
482        }
483    }
484
485    /// Returns [`Modifiers`] with just shift.
486    pub fn shift() -> Modifiers {
487        Modifiers {
488            shift: true,
489            ..Default::default()
490        }
491    }
492
493    /// Returns [`Modifiers`] with command + shift.
494    pub fn command_shift() -> Modifiers {
495        Modifiers {
496            shift: true,
497            platform: true,
498            ..Default::default()
499        }
500    }
501
502    /// Returns [`Modifiers`] with command + shift.
503    pub fn control_shift() -> Modifiers {
504        Modifiers {
505            shift: true,
506            control: true,
507            ..Default::default()
508        }
509    }
510
511    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
512    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
513        (other.control || !self.control)
514            && (other.alt || !self.alt)
515            && (other.shift || !self.shift)
516            && (other.platform || !self.platform)
517            && (other.function || !self.function)
518    }
519}