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