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