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