keystroke.rs

  1use anyhow::anyhow;
  2use serde::Deserialize;
  3use smallvec::SmallVec;
  4use std::fmt::Write;
  5
  6/// A keystroke and associated metadata generated by the platform
  7#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
  8pub struct Keystroke {
  9    /// the state of the modifier keys at the time the keystroke was generated
 10    pub modifiers: Modifiers,
 11
 12    /// key is the character printed on the key that was pressed
 13    /// e.g. for option-s, key is "s"
 14    pub key: String,
 15
 16    /// ime_key is the character inserted by the IME engine when that key was pressed.
 17    /// e.g. for option-s, ime_key is "รŸ"
 18    pub ime_key: Option<String>,
 19}
 20
 21impl Keystroke {
 22    /// When matching a key we cannot know whether the user intended to type
 23    /// the ime_key or the key itself. On some non-US keyboards keys we use in our
 24    /// bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
 25    /// and on some keyboards the IME handler converts a sequence of keys into a
 26    /// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
 27    ///
 28    /// This method generates a list of potential keystroke candidates that could be matched
 29    /// against when resolving a keybinding.
 30    pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
 31        let mut possibilities = SmallVec::new();
 32        match self.ime_key.as_ref() {
 33            Some(ime_key) => {
 34                if ime_key != &self.key {
 35                    possibilities.push(Keystroke {
 36                        modifiers: Modifiers {
 37                            control: self.modifiers.control,
 38                            alt: false,
 39                            shift: false,
 40                            platform: false,
 41                            function: false,
 42                        },
 43                        key: ime_key.to_string(),
 44                        ime_key: None,
 45                    });
 46                }
 47                possibilities.push(Keystroke {
 48                    ime_key: None,
 49                    ..self.clone()
 50                });
 51            }
 52            None => possibilities.push(self.clone()),
 53        }
 54        possibilities
 55    }
 56
 57    /// key syntax is:
 58    /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
 59    /// ime_key syntax is only used for generating test events,
 60    /// when matching a key with an ime_key set will be matched without it.
 61    pub fn parse(source: &str) -> anyhow::Result<Self> {
 62        let mut control = false;
 63        let mut alt = false;
 64        let mut shift = false;
 65        let mut platform = false;
 66        let mut function = false;
 67        let mut key = None;
 68        let mut ime_key = None;
 69
 70        let mut components = source.split('-').peekable();
 71        while let Some(component) = components.next() {
 72            match component {
 73                "ctrl" => control = true,
 74                "alt" => alt = true,
 75                "shift" => shift = true,
 76                "fn" => function = true,
 77                "cmd" | "super" | "win" => platform = true,
 78                _ => {
 79                    if let Some(next) = components.peek() {
 80                        if next.is_empty() && source.ends_with('-') {
 81                            key = Some(String::from("-"));
 82                            break;
 83                        } else if next.len() > 1 && next.starts_with('>') {
 84                            key = Some(String::from(component));
 85                            ime_key = Some(String::from(&next[1..]));
 86                            components.next();
 87                        } else {
 88                            return Err(anyhow!("Invalid keystroke `{}`", source));
 89                        }
 90                    } else {
 91                        key = Some(String::from(component));
 92                    }
 93                }
 94            }
 95        }
 96
 97        let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
 98
 99        Ok(Keystroke {
100            modifiers: Modifiers {
101                control,
102                alt,
103                shift,
104                platform,
105                function,
106            },
107            key,
108            ime_key,
109        })
110    }
111
112    /// Returns a new keystroke with the ime_key filled.
113    /// This is used for dispatch_keystroke where we want users to
114    /// be able to simulate typing "space", etc.
115    pub fn with_simulated_ime(mut self) -> Self {
116        if self.ime_key.is_none()
117            && !self.modifiers.platform
118            && !self.modifiers.control
119            && !self.modifiers.function
120            && !self.modifiers.alt
121        {
122            self.ime_key = match self.key.as_str() {
123                "space" => Some(" ".into()),
124                "tab" => Some("\t".into()),
125                "enter" => Some("\n".into()),
126                "up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end"
127                | "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6"
128                | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None,
129                key => {
130                    if self.modifiers.shift {
131                        Some(key.to_uppercase())
132                    } else {
133                        Some(key.into())
134                    }
135                }
136            }
137        }
138        self
139    }
140}
141
142impl std::fmt::Display for Keystroke {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        if self.modifiers.control {
145            f.write_char('^')?;
146        }
147        if self.modifiers.alt {
148            f.write_char('โŒฅ')?;
149        }
150        if self.modifiers.platform {
151            #[cfg(target_os = "macos")]
152            f.write_char('โŒ˜')?;
153
154            #[cfg(target_os = "linux")]
155            f.write_char('โ–')?;
156
157            #[cfg(target_os = "windows")]
158            f.write_char('โŠž')?;
159        }
160        if self.modifiers.shift {
161            f.write_char('โ‡ง')?;
162        }
163        let key = match self.key.as_str() {
164            "backspace" => 'โŒซ',
165            "up" => 'โ†‘',
166            "down" => 'โ†“',
167            "left" => 'โ†',
168            "right" => 'โ†’',
169            "tab" => 'โ‡ฅ',
170            "escape" => 'โŽ‹',
171            key => {
172                if key.len() == 1 {
173                    key.chars().next().unwrap().to_ascii_uppercase()
174                } else {
175                    return f.write_str(key);
176                }
177            }
178        };
179        f.write_char(key)
180    }
181}
182
183/// The state of the modifier keys at some point in time
184#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
185pub struct Modifiers {
186    /// The control key
187    pub control: bool,
188
189    /// The alt key
190    /// Sometimes also known as the 'meta' key
191    pub alt: bool,
192
193    /// The shift key
194    pub shift: bool,
195
196    /// The command key, on macos
197    /// the windows key, on windows
198    /// the super key, on linux
199    pub platform: bool,
200
201    /// The function key
202    pub function: bool,
203}
204
205impl Modifiers {
206    /// Returns true if any modifier key is pressed
207    pub fn modified(&self) -> bool {
208        self.control || self.alt || self.shift || self.platform || self.function
209    }
210
211    /// Whether the semantically 'secondary' modifier key is pressed
212    /// On macos, this is the command key
213    /// On windows and linux, this is the control key
214    pub fn secondary(&self) -> bool {
215        #[cfg(target_os = "macos")]
216        {
217            return self.platform;
218        }
219
220        #[cfg(not(target_os = "macos"))]
221        {
222            return self.control;
223        }
224    }
225
226    /// helper method for Modifiers with no modifiers
227    pub fn none() -> Modifiers {
228        Default::default()
229    }
230
231    /// helper method for Modifiers with just the command key
232    pub fn command() -> Modifiers {
233        Modifiers {
234            platform: true,
235            ..Default::default()
236        }
237    }
238
239    /// A helper method for Modifiers with just the secondary key pressed
240    pub fn secondary_key() -> Modifiers {
241        #[cfg(target_os = "macos")]
242        {
243            Modifiers {
244                platform: true,
245                ..Default::default()
246            }
247        }
248
249        #[cfg(not(target_os = "macos"))]
250        {
251            Modifiers {
252                control: true,
253                ..Default::default()
254            }
255        }
256    }
257
258    /// helper method for Modifiers with just the windows key
259    pub fn windows() -> Modifiers {
260        Modifiers {
261            platform: true,
262            ..Default::default()
263        }
264    }
265
266    /// helper method for Modifiers with just the super key
267    pub fn super_key() -> Modifiers {
268        Modifiers {
269            platform: true,
270            ..Default::default()
271        }
272    }
273
274    /// helper method for Modifiers with just control
275    pub fn control() -> Modifiers {
276        Modifiers {
277            control: true,
278            ..Default::default()
279        }
280    }
281
282    /// helper method for Modifiers with just shift
283    pub fn shift() -> Modifiers {
284        Modifiers {
285            shift: true,
286            ..Default::default()
287        }
288    }
289
290    /// helper method for Modifiers with command + shift
291    pub fn command_shift() -> Modifiers {
292        Modifiers {
293            shift: true,
294            platform: true,
295            ..Default::default()
296        }
297    }
298
299    /// Checks if this Modifiers is a subset of another Modifiers
300    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
301        (other.control || !self.control)
302            && (other.alt || !self.alt)
303            && (other.shift || !self.shift)
304            && (other.platform || !self.platform)
305            && (other.function || !self.function)
306    }
307}