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