keystroke.rs

  1use schemars::JsonSchema;
  2use serde::{Deserialize, Serialize};
  3use std::{
  4    error::Error,
  5    fmt::{Display, Write},
  6};
  7
  8/// TODO:
  9pub struct KeybindKeystroke {
 10    /// TODO:
 11    pub inner: Keystroke,
 12    /// TODO:
 13    pub modifiers: Modifiers,
 14    /// TODO:
 15    pub key: String,
 16}
 17
 18/// A keystroke and associated metadata generated by the platform
 19#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
 20pub struct Keystroke {
 21    /// the state of the modifier keys at the time the keystroke was generated
 22    pub modifiers: Modifiers,
 23
 24    /// key is the character printed on the key that was pressed
 25    /// e.g. for option-s, key is "s"
 26    pub key: String,
 27
 28    /// key_char is the character that could have been typed when
 29    /// this binding was pressed.
 30    /// e.g. for s this is "s", for option-s "รŸ", and cmd-s None
 31    pub key_char: Option<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
276fn is_printable_key(key: &str) -> bool {
277    !matches!(
278        key,
279        "f1" | "f2"
280            | "f3"
281            | "f4"
282            | "f5"
283            | "f6"
284            | "f7"
285            | "f8"
286            | "f9"
287            | "f10"
288            | "f11"
289            | "f12"
290            | "f13"
291            | "f14"
292            | "f15"
293            | "f16"
294            | "f17"
295            | "f18"
296            | "f19"
297            | "f20"
298            | "f21"
299            | "f22"
300            | "f23"
301            | "f24"
302            | "f25"
303            | "f26"
304            | "f27"
305            | "f28"
306            | "f29"
307            | "f30"
308            | "f31"
309            | "f32"
310            | "f33"
311            | "f34"
312            | "f35"
313            | "backspace"
314            | "delete"
315            | "left"
316            | "right"
317            | "up"
318            | "down"
319            | "pageup"
320            | "pagedown"
321            | "insert"
322            | "home"
323            | "end"
324            | "back"
325            | "forward"
326            | "escape"
327    )
328}
329
330impl std::fmt::Display for Keystroke {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        if self.modifiers.control {
333            #[cfg(target_os = "macos")]
334            f.write_char('^')?;
335
336            #[cfg(not(target_os = "macos"))]
337            write!(f, "ctrl-")?;
338        }
339        if self.modifiers.alt {
340            #[cfg(target_os = "macos")]
341            f.write_char('โŒฅ')?;
342
343            #[cfg(not(target_os = "macos"))]
344            write!(f, "alt-")?;
345        }
346        if self.modifiers.platform {
347            #[cfg(target_os = "macos")]
348            f.write_char('โŒ˜')?;
349
350            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
351            f.write_char('โ–')?;
352
353            #[cfg(target_os = "windows")]
354            f.write_char('โŠž')?;
355        }
356        if self.modifiers.shift {
357            #[cfg(target_os = "macos")]
358            f.write_char('โ‡ง')?;
359
360            #[cfg(not(target_os = "macos"))]
361            write!(f, "shift-")?;
362        }
363        let key = match self.key.as_str() {
364            #[cfg(target_os = "macos")]
365            "backspace" => 'โŒซ',
366            #[cfg(target_os = "macos")]
367            "up" => 'โ†‘',
368            #[cfg(target_os = "macos")]
369            "down" => 'โ†“',
370            #[cfg(target_os = "macos")]
371            "left" => 'โ†',
372            #[cfg(target_os = "macos")]
373            "right" => 'โ†’',
374            #[cfg(target_os = "macos")]
375            "tab" => 'โ‡ฅ',
376            #[cfg(target_os = "macos")]
377            "escape" => 'โŽ‹',
378            #[cfg(target_os = "macos")]
379            "shift" => 'โ‡ง',
380            #[cfg(target_os = "macos")]
381            "control" => 'โŒƒ',
382            #[cfg(target_os = "macos")]
383            "alt" => 'โŒฅ',
384            #[cfg(target_os = "macos")]
385            "platform" => 'โŒ˜',
386
387            key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
388            key => return f.write_str(key),
389        };
390        f.write_char(key)
391    }
392}
393
394/// The state of the modifier keys at some point in time
395#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
396pub struct Modifiers {
397    /// The control key
398    #[serde(default)]
399    pub control: bool,
400
401    /// The alt key
402    /// Sometimes also known as the 'meta' key
403    #[serde(default)]
404    pub alt: bool,
405
406    /// The shift key
407    #[serde(default)]
408    pub shift: bool,
409
410    /// The command key, on macos
411    /// the windows key, on windows
412    /// the super key, on linux
413    #[serde(default)]
414    pub platform: bool,
415
416    /// The function key
417    #[serde(default)]
418    pub function: bool,
419}
420
421impl Modifiers {
422    /// Returns whether any modifier key is pressed.
423    pub fn modified(&self) -> bool {
424        self.control || self.alt || self.shift || self.platform || self.function
425    }
426
427    /// Whether the semantically 'secondary' modifier key is pressed.
428    ///
429    /// On macOS, this is the command key.
430    /// On Linux and Windows, this is the control key.
431    pub fn secondary(&self) -> bool {
432        #[cfg(target_os = "macos")]
433        {
434            self.platform
435        }
436
437        #[cfg(not(target_os = "macos"))]
438        {
439            self.control
440        }
441    }
442
443    /// Returns how many modifier keys are pressed.
444    pub fn number_of_modifiers(&self) -> u8 {
445        self.control as u8
446            + self.alt as u8
447            + self.shift as u8
448            + self.platform as u8
449            + self.function as u8
450    }
451
452    /// Returns [`Modifiers`] with no modifiers.
453    pub fn none() -> Modifiers {
454        Default::default()
455    }
456
457    /// Returns [`Modifiers`] with just the command key.
458    pub fn command() -> Modifiers {
459        Modifiers {
460            platform: true,
461            ..Default::default()
462        }
463    }
464
465    /// A Returns [`Modifiers`] with just the secondary key pressed.
466    pub fn secondary_key() -> Modifiers {
467        #[cfg(target_os = "macos")]
468        {
469            Modifiers {
470                platform: true,
471                ..Default::default()
472            }
473        }
474
475        #[cfg(not(target_os = "macos"))]
476        {
477            Modifiers {
478                control: true,
479                ..Default::default()
480            }
481        }
482    }
483
484    /// Returns [`Modifiers`] with just the windows key.
485    pub fn windows() -> Modifiers {
486        Modifiers {
487            platform: true,
488            ..Default::default()
489        }
490    }
491
492    /// Returns [`Modifiers`] with just the super key.
493    pub fn super_key() -> Modifiers {
494        Modifiers {
495            platform: true,
496            ..Default::default()
497        }
498    }
499
500    /// Returns [`Modifiers`] with just control.
501    pub fn control() -> Modifiers {
502        Modifiers {
503            control: true,
504            ..Default::default()
505        }
506    }
507
508    /// Returns [`Modifiers`] with just alt.
509    pub fn alt() -> Modifiers {
510        Modifiers {
511            alt: true,
512            ..Default::default()
513        }
514    }
515
516    /// Returns [`Modifiers`] with just shift.
517    pub fn shift() -> Modifiers {
518        Modifiers {
519            shift: true,
520            ..Default::default()
521        }
522    }
523
524    /// Returns [`Modifiers`] with command + shift.
525    pub fn command_shift() -> Modifiers {
526        Modifiers {
527            shift: true,
528            platform: true,
529            ..Default::default()
530        }
531    }
532
533    /// Returns [`Modifiers`] with command + shift.
534    pub fn control_shift() -> Modifiers {
535        Modifiers {
536            shift: true,
537            control: true,
538            ..Default::default()
539        }
540    }
541
542    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
543    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
544        (other.control || !self.control)
545            && (other.alt || !self.alt)
546            && (other.shift || !self.shift)
547            && (other.platform || !self.platform)
548            && (other.function || !self.function)
549    }
550}
551
552/// The state of the capslock key at some point in time
553#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
554pub struct Capslock {
555    /// The capslock key is on
556    #[serde(default)]
557    pub on: bool,
558}