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