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}