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