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