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