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                #[cfg(target_os = "macos")]
 78                "cmd" => platform = true,
 79                #[cfg(target_os = "linux")]
 80                "super" => platform = true,
 81                #[cfg(target_os = "windows")]
 82                "win" => platform = true,
 83                _ => {
 84                    if let Some(next) = components.peek() {
 85                        if next.is_empty() && source.ends_with('-') {
 86                            key = Some(String::from("-"));
 87                            break;
 88                        } else if next.len() > 1 && next.starts_with('>') {
 89                            key = Some(String::from(component));
 90                            ime_key = Some(String::from(&next[1..]));
 91                            components.next();
 92                        } else {
 93                            return Err(anyhow!("Invalid keystroke `{}`", source));
 94                        }
 95                    } else {
 96                        key = Some(String::from(component));
 97                    }
 98                }
 99            }
100        }
101
102        let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
103
104        Ok(Keystroke {
105            modifiers: Modifiers {
106                control,
107                alt,
108                shift,
109                platform,
110                function,
111            },
112            key,
113            ime_key,
114        })
115    }
116
117    /// Returns a new keystroke with the ime_key filled.
118    /// This is used for dispatch_keystroke where we want users to
119    /// be able to simulate typing "space", etc.
120    pub fn with_simulated_ime(mut self) -> Self {
121        if self.ime_key.is_none()
122            && !self.modifiers.platform
123            && !self.modifiers.control
124            && !self.modifiers.function
125            && !self.modifiers.alt
126        {
127            self.ime_key = match self.key.as_str() {
128                "space" => Some(" ".into()),
129                "tab" => Some("\t".into()),
130                "enter" => Some("\n".into()),
131                "up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end"
132                | "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6"
133                | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None,
134                key => {
135                    if self.modifiers.shift {
136                        Some(key.to_uppercase())
137                    } else {
138                        Some(key.into())
139                    }
140                }
141            }
142        }
143        self
144    }
145}
146
147impl std::fmt::Display for Keystroke {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        if self.modifiers.control {
150            f.write_char('^')?;
151        }
152        if self.modifiers.alt {
153            f.write_char('โŒฅ')?;
154        }
155        if self.modifiers.platform {
156            #[cfg(target_os = "macos")]
157            f.write_char('โŒ˜')?;
158
159            #[cfg(target_os = "linux")]
160            f.write_char('โ–')?;
161
162            #[cfg(target_os = "windows")]
163            f.write_char('โŠž')?;
164        }
165        if self.modifiers.shift {
166            f.write_char('โ‡ง')?;
167        }
168        let key = match self.key.as_str() {
169            "backspace" => 'โŒซ',
170            "up" => 'โ†‘',
171            "down" => 'โ†“',
172            "left" => 'โ†',
173            "right" => 'โ†’',
174            "tab" => 'โ‡ฅ',
175            "escape" => 'โŽ‹',
176            key => {
177                if key.len() == 1 {
178                    key.chars().next().unwrap().to_ascii_uppercase()
179                } else {
180                    return f.write_str(key);
181                }
182            }
183        };
184        f.write_char(key)
185    }
186}
187
188/// The state of the modifier keys at some point in time
189#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
190pub struct Modifiers {
191    /// The control key
192    pub control: bool,
193
194    /// The alt key
195    /// Sometimes also known as the 'meta' key
196    pub alt: bool,
197
198    /// The shift key
199    pub shift: bool,
200
201    /// The command key, on macos
202    /// the windows key, on windows
203    /// the super key, on linux
204    pub platform: bool,
205
206    /// The function key
207    pub function: bool,
208}
209
210impl Modifiers {
211    /// Returns true if any modifier key is pressed
212    pub fn modified(&self) -> bool {
213        self.control || self.alt || self.shift || self.platform || self.function
214    }
215
216    /// Whether the semantically 'secondary' modifier key is pressed
217    /// On macos, this is the command key
218    /// On windows and linux, this is the control key
219    pub fn secondary(&self) -> bool {
220        #[cfg(target_os = "macos")]
221        {
222            return self.platform;
223        }
224
225        #[cfg(not(target_os = "macos"))]
226        {
227            return self.control;
228        }
229    }
230
231    /// helper method for Modifiers with no modifiers
232    pub fn none() -> Modifiers {
233        Default::default()
234    }
235
236    /// helper method for Modifiers with just the command key
237    pub fn command() -> Modifiers {
238        Modifiers {
239            platform: true,
240            ..Default::default()
241        }
242    }
243
244    /// A helper method for Modifiers with just the secondary key pressed
245    pub fn secondary_key() -> Modifiers {
246        #[cfg(target_os = "macos")]
247        {
248            Modifiers {
249                platform: true,
250                ..Default::default()
251            }
252        }
253
254        #[cfg(not(target_os = "macos"))]
255        {
256            Modifiers {
257                control: true,
258                ..Default::default()
259            }
260        }
261    }
262
263    /// helper method for Modifiers with just the windows key
264    pub fn windows() -> Modifiers {
265        Modifiers {
266            platform: true,
267            ..Default::default()
268        }
269    }
270
271    /// helper method for Modifiers with just the super key
272    pub fn super_key() -> Modifiers {
273        Modifiers {
274            platform: true,
275            ..Default::default()
276        }
277    }
278
279    /// helper method for Modifiers with just control
280    pub fn control() -> Modifiers {
281        Modifiers {
282            control: true,
283            ..Default::default()
284        }
285    }
286
287    /// helper method for Modifiers with just shift
288    pub fn shift() -> Modifiers {
289        Modifiers {
290            shift: true,
291            ..Default::default()
292        }
293    }
294
295    /// helper method for Modifiers with command + shift
296    pub fn command_shift() -> Modifiers {
297        Modifiers {
298            shift: true,
299            platform: true,
300            ..Default::default()
301        }
302    }
303
304    /// Checks if this Modifiers is a subset of another Modifiers
305    pub fn is_subset_of(&self, other: &Modifiers) -> bool {
306        (other.control || !self.control)
307            && (other.alt || !self.alt)
308            && (other.shift || !self.shift)
309            && (other.platform || !self.platform)
310            && (other.function || !self.function)
311    }
312}