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