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