gpui/platform/keystroke.rs
1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{
4 error::Error,
5 fmt::{Display, Write},
6};
7
8use crate::PlatformKeyboardMapper;
9
10/// A type that can be viewed as a [`Keystroke`].
11///
12/// Matching code uses this so that it accepts both a [`Keystroke`] that was typed and a
13/// [`KeybindingKeystroke`] from a keymap.
14pub trait AsKeystroke {
15 /// Returns the [`Keystroke`] this value represents.
16 fn as_keystroke(&self) -> &Keystroke;
17}
18
19/// A key press, with the modifier keys that were held when it happened.
20///
21/// The platform creates a keystroke for each key press. To write one by hand, as a binding or a
22/// test does, parse it from a string with [`Keystroke::parse`]:
23///
24/// ```
25/// # use gpui::Keystroke;
26/// let keystroke = Keystroke::parse("ctrl-shift-z").unwrap();
27/// assert!(keystroke.modifiers.control);
28/// assert!(keystroke.modifiers.shift);
29/// assert_eq!(keystroke.key, "z");
30/// ```
31///
32/// See [keyboard input](crate::_documentation::guides::keyboard) for the bigger picture.
33#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
34pub struct Keystroke {
35 /// The modifier keys held when the key was pressed.
36 pub modifiers: Modifiers,
37
38 /// The key that was pressed, named after the character printed on it, such as `s`, or a
39 /// name such as `enter` or `pageup` for keys without a character.
40 ///
41 /// For option-s on macOS this is `s`, not `ß`. On layouts without ASCII letters, such as Thai,
42 /// this is the ASCII equivalent of the key, and the typed character is in `key_char`.
43 pub key: String,
44
45 /// The text the key would type with the current layout and modifiers, if any.
46 ///
47 /// This is `Some("s")` for `s`, `Some("ß")` for option-s on a US macOS layout, and `None` for
48 /// `cmd-s`, because `cmd` doesn't type text.
49 pub key_char: Option<String>,
50}
51
52/// A keystroke as it appears in a keybinding, ready to display to the user.
53///
54/// A [`KeyBinding`](crate::KeyBinding) stores its keystrokes as `KeybindingKeystroke`s. On
55/// Windows, the modifiers and key shown to the user can differ from the ones used for matching.
56#[derive(Debug, Clone, Eq, PartialEq, Hash)]
57pub struct KeybindingKeystroke {
58 /// The GPUI representation of the keystroke.
59 inner: Keystroke,
60 /// The modifiers to display.
61 #[cfg(target_os = "windows")]
62 display_modifiers: Modifiers,
63 /// The key to display.
64 #[cfg(target_os = "windows")]
65 display_key: String,
66}
67
68/// Error returned by [`Keystroke::parse`] when a string isn't a valid keystroke.
69///
70/// It is a plain error type rather than `anyhow::Error`.
71#[derive(Debug)]
72pub struct InvalidKeystrokeError {
73 /// The source text that failed to parse.
74 pub keystroke: String,
75}
76
77impl Error for InvalidKeystrokeError {}
78
79impl Display for InvalidKeystrokeError {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(
82 f,
83 "Invalid keystroke \"{}\". {}",
84 self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
85 )
86 }
87}
88
89/// The sentence that [`InvalidKeystrokeError`] appends to its message. It describes the syntax
90/// [`Keystroke::parse`] expects.
91pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
92 (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
93 followed by a key, separated by `-`.";
94
95impl Keystroke {
96 /// Returns whether this keystroke, as typed, matches a keystroke in a keymap.
97 ///
98 /// `self` is the keystroke the user typed and `target` is the one from the binding. Matching
99 /// first compares modifiers and key. It also accepts a `target` whose key is this keystroke's
100 /// `key_char`, when the target's modifiers are exactly this keystroke's `ctrl` and `cmd` (on
101 /// Windows, when the target has no modifiers). That covers layouts where a binding
102 /// key sits behind option, such as `$` on a Czech keyboard typed as `alt-ç`, and input methods
103 /// that turn a sequence of keys into one character.
104 pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
105 #[cfg(not(target_os = "windows"))]
106 if let Some(key_char) = self
107 .key_char
108 .as_ref()
109 .filter(|key_char| key_char != &&self.key)
110 {
111 let ime_modifiers = Modifiers {
112 control: self.modifiers.control,
113 platform: self.modifiers.platform,
114 ..Default::default()
115 };
116
117 if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
118 return true;
119 }
120 }
121
122 #[cfg(target_os = "windows")]
123 if let Some(key_char) = self
124 .key_char
125 .as_ref()
126 .filter(|key_char| key_char != &&self.key)
127 {
128 // On Windows, if key_char is set, then the typed keystroke produced the key_char
129 if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
130 return true;
131 }
132 }
133
134 target.inner.modifiers == self.modifiers && target.inner.key == self.key
135 }
136
137 /// Parses a keystroke from text such as `cmd-s`, `ctrl-shift-tab` or `cmd--`.
138 ///
139 /// The syntax is zero or more modifiers, then a key, separated by `-`:
140 ///
141 /// - **Modifiers** are `ctrl`, `alt`, `shift`, `fn`, `secondary`, `cmd`, `super` and `win`.
142 /// They can come in any order and any case. `cmd`, `super` and `win` all set
143 /// [`Modifiers::platform`]. `secondary` sets `cmd` on macOS and `ctrl` on other platforms,
144 /// which is what [`Modifiers::secondary`] reports.
145 /// - **The key** is the last part. Any other part must be a modifier. Key names are
146 /// case-insensitive and stored in lowercase, so `Tab` and `tab` are the same key. Other
147 /// text is accepted as a key without checking, but the names that platforms produce are
148 /// `space`, `tab`, `enter`, `backspace`, `escape`, `delete`, `insert`, `home`, `end`,
149 /// `pageup`, `pagedown`, `up`, `down`, `left`, `right` and function keys such as `f1`.
150 /// - **A single uppercase letter** sets `shift` and uses the lowercase letter as the key, so
151 /// `S` is `shift-s`.
152 /// - **A key of `-`** is written as an extra dash, so `cmd--` is `cmd` and `-`.
153 /// - **A modifier with no key** becomes the key. The first of `shift`, `ctrl`, `alt`, `cmd` and
154 /// `fn` that is present is used, in that order. Its key is named `shift`, `control`, `alt`,
155 /// `platform` or `function`, and that modifier is cleared. So `ctrl` alone is the key
156 /// `control` with no modifiers set.
157 /// - **`key->key_char`** sets `key_char` to the text after `->`, for example `alt-s->ß`.
158 /// [`Keystroke::should_match`] ignores it when matching a binding.
159 ///
160 /// # Errors
161 ///
162 /// Returns [`InvalidKeystrokeError`] if a part before the key isn't a modifier, as in
163 /// `ctrl-a-b`, or if `->` isn't followed by a key character.
164 ///
165 /// # Examples
166 ///
167 /// ```
168 /// # use gpui::Keystroke;
169 /// let save = Keystroke::parse("secondary-s").unwrap();
170 /// assert!(save.modifiers.secondary());
171 /// assert_eq!(save.key, "s");
172 ///
173 /// let shifted = Keystroke::parse("S").unwrap();
174 /// assert!(shifted.modifiers.shift);
175 /// assert_eq!(shifted.key, "s");
176 ///
177 /// assert!(Keystroke::parse("ctrl-a-b").is_err());
178 /// ```
179 ///
180 /// See [keyboard input](crate::_documentation::guides::keyboard) for how bindings use these.
181 pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
182 let mut modifiers = Modifiers::none();
183 let mut key = None;
184 let mut key_char = None;
185
186 let mut components = source.split('-').peekable();
187 while let Some(component) = components.next() {
188 if component.eq_ignore_ascii_case("ctrl") {
189 modifiers.control = true;
190 continue;
191 }
192 if component.eq_ignore_ascii_case("alt") {
193 modifiers.alt = true;
194 continue;
195 }
196 if component.eq_ignore_ascii_case("shift") {
197 modifiers.shift = true;
198 continue;
199 }
200 if component.eq_ignore_ascii_case("fn") {
201 modifiers.function = true;
202 continue;
203 }
204 if component.eq_ignore_ascii_case("secondary") {
205 if cfg!(target_os = "macos") {
206 modifiers.platform = true;
207 } else {
208 modifiers.control = true;
209 };
210 continue;
211 }
212
213 let is_platform = component.eq_ignore_ascii_case("cmd")
214 || component.eq_ignore_ascii_case("super")
215 || component.eq_ignore_ascii_case("win");
216
217 if is_platform {
218 modifiers.platform = true;
219 continue;
220 }
221
222 let mut key_str = component.to_string();
223
224 if let Some(next) = components.peek() {
225 if next.is_empty() && source.ends_with('-') {
226 key = Some(String::from("-"));
227 break;
228 } else if next.len() > 1 && next.starts_with('>') {
229 key = Some(key_str);
230 key_char = Some(String::from(&next[1..]));
231 components.next();
232 } else {
233 return Err(InvalidKeystrokeError {
234 keystroke: source.to_owned(),
235 });
236 }
237 continue;
238 }
239
240 if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
241 // Convert to shift + lowercase char
242 modifiers.shift = true;
243 key_str.make_ascii_lowercase();
244 } else {
245 // convert ascii chars to lowercase so that named keys like "tab" and "enter"
246 // are accepted case insensitively and stored how we expect so they are matched properly
247 key_str.make_ascii_lowercase()
248 }
249 key = Some(key_str);
250 }
251
252 // Allow for the user to specify a keystroke modifier as the key itself
253 // This sets the `key` to the modifier, and disables the modifier
254 key = key.or_else(|| {
255 use std::mem;
256 // std::mem::take clears bool incase its true
257 if mem::take(&mut modifiers.shift) {
258 Some("shift".to_string())
259 } else if mem::take(&mut modifiers.control) {
260 Some("control".to_string())
261 } else if mem::take(&mut modifiers.alt) {
262 Some("alt".to_string())
263 } else if mem::take(&mut modifiers.platform) {
264 Some("platform".to_string())
265 } else if mem::take(&mut modifiers.function) {
266 Some("function".to_string())
267 } else {
268 None
269 }
270 });
271
272 let key = key.ok_or_else(|| InvalidKeystrokeError {
273 keystroke: source.to_owned(),
274 })?;
275
276 Ok(Keystroke {
277 modifiers,
278 key,
279 key_char,
280 })
281 }
282
283 /// Returns text that [`Keystroke::parse`] reads back as this keystroke.
284 ///
285 /// The platform's name for `cmd` is used: `cmd` on macOS, `super` on Linux and `win` on
286 /// Windows. `key_char` is not included, so the result doesn't round-trip when it is set.
287 pub fn unparse(&self) -> String {
288 unparse(&self.modifiers, &self.key)
289 }
290
291 /// Returns whether an input method is still composing text for this keystroke.
292 ///
293 /// This is true when `key_char` is `None`, the key is printable or empty (not a named key such
294 /// as `escape` or `f1`), and none of `cmd`, `ctrl`, `fn` or `alt` is held. `shift` alone
295 /// doesn't count.
296 pub fn is_ime_in_progress(&self) -> bool {
297 self.key_char.is_none()
298 && (is_printable_key(&self.key) || self.key.is_empty())
299 && !(self.modifiers.platform
300 || self.modifiers.control
301 || self.modifiers.function
302 || self.modifiers.alt)
303 }
304
305 /// Returns this keystroke with `key_char` set to the text it would type.
306 ///
307 /// Simulated keystrokes use this so that they type text as a real key press does. It only
308 /// applies when `key_char` is `None` and none of `cmd`, `ctrl`, `fn` or `alt` is held. `space`,
309 /// `tab` and `enter` type a space, a tab and a newline. Other printable keys type their
310 /// character, uppercased when `shift` is held.
311 pub fn with_simulated_ime(mut self) -> Self {
312 if self.key_char.is_none()
313 && !self.modifiers.platform
314 && !self.modifiers.control
315 && !self.modifiers.function
316 && !self.modifiers.alt
317 {
318 self.key_char = match self.key.as_str() {
319 "space" => Some(" ".into()),
320 "tab" => Some("\t".into()),
321 "enter" => Some("\n".into()),
322 key if !is_printable_key(key) || key.is_empty() => None,
323 key => {
324 if self.modifiers.shift {
325 Some(key.to_uppercase())
326 } else {
327 Some(key.into())
328 }
329 }
330 }
331 }
332 self
333 }
334}
335
336impl KeybindingKeystroke {
337 #[cfg(target_os = "windows")]
338 #[expect(missing_docs)]
339 pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
340 KeybindingKeystroke {
341 inner,
342 display_modifiers,
343 display_key,
344 }
345 }
346
347 /// Creates a keybinding keystroke from `inner`, mapped by `keyboard_mapper`.
348 ///
349 /// The platform's [`PlatformKeyboardMapper`](crate::PlatformKeyboardMapper) decides what
350 /// changes. On macOS, when `use_key_equivalents` is true and the current layout types a
351 /// different character for the key, the key is replaced with that character.
352 pub fn new_with_mapper(
353 inner: Keystroke,
354 use_key_equivalents: bool,
355 keyboard_mapper: &dyn PlatformKeyboardMapper,
356 ) -> Self {
357 keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
358 }
359
360 /// Creates a keybinding keystroke from `keystroke` with no platform-specific mapping.
361 pub fn from_keystroke(keystroke: Keystroke) -> Self {
362 #[cfg(target_os = "windows")]
363 {
364 let key = keystroke.key.clone();
365 let modifiers = keystroke.modifiers;
366 KeybindingKeystroke {
367 inner: keystroke,
368 display_modifiers: modifiers,
369 display_key: key,
370 }
371 }
372 #[cfg(not(target_os = "windows"))]
373 {
374 KeybindingKeystroke { inner: keystroke }
375 }
376 }
377
378 /// Returns the [`Keystroke`] that matching uses.
379 pub fn inner(&self) -> &Keystroke {
380 &self.inner
381 }
382
383 /// Returns the modifiers.
384 ///
385 /// Platform-specific behavior:
386 /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
387 /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
388 /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
389 pub fn modifiers(&self) -> &Modifiers {
390 #[cfg(target_os = "windows")]
391 {
392 &self.display_modifiers
393 }
394 #[cfg(not(target_os = "windows"))]
395 {
396 &self.inner.modifiers
397 }
398 }
399
400 /// Returns the key.
401 ///
402 /// Platform-specific behavior:
403 /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
404 /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
405 pub fn key(&self) -> &str {
406 #[cfg(target_os = "windows")]
407 {
408 &self.display_key
409 }
410 #[cfg(not(target_os = "windows"))]
411 {
412 &self.inner.key
413 }
414 }
415
416 /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
417 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
418 self.inner.modifiers = modifiers;
419 #[cfg(target_os = "windows")]
420 {
421 self.display_modifiers = modifiers;
422 }
423 }
424
425 /// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
426 pub fn set_key(&mut self, key: String) {
427 #[cfg(target_os = "windows")]
428 {
429 self.display_key = key.clone();
430 }
431 self.inner.key = key;
432 }
433
434 /// Returns text that [`Keystroke::parse`] reads back as this keystroke, using the displayed
435 /// modifiers and key on Windows. See [`Keystroke::unparse`].
436 pub fn unparse(&self) -> String {
437 #[cfg(target_os = "windows")]
438 {
439 unparse(&self.display_modifiers, &self.display_key)
440 }
441 #[cfg(not(target_os = "windows"))]
442 {
443 unparse(&self.inner.modifiers, &self.inner.key)
444 }
445 }
446
447 /// Clears `key_char`, so that matching and display use only `key` and the modifiers.
448 pub fn remove_key_char(&mut self) {
449 self.inner.key_char = None;
450 }
451}
452
453fn is_printable_key(key: &str) -> bool {
454 !matches!(
455 key,
456 "f1" | "f2"
457 | "f3"
458 | "f4"
459 | "f5"
460 | "f6"
461 | "f7"
462 | "f8"
463 | "f9"
464 | "f10"
465 | "f11"
466 | "f12"
467 | "f13"
468 | "f14"
469 | "f15"
470 | "f16"
471 | "f17"
472 | "f18"
473 | "f19"
474 | "f20"
475 | "f21"
476 | "f22"
477 | "f23"
478 | "f24"
479 | "f25"
480 | "f26"
481 | "f27"
482 | "f28"
483 | "f29"
484 | "f30"
485 | "f31"
486 | "f32"
487 | "f33"
488 | "f34"
489 | "f35"
490 | "backspace"
491 | "delete"
492 | "left"
493 | "right"
494 | "up"
495 | "down"
496 | "pageup"
497 | "pagedown"
498 | "insert"
499 | "home"
500 | "end"
501 | "back"
502 | "forward"
503 | "escape"
504 )
505}
506
507impl std::fmt::Display for Keystroke {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509 display_modifiers(&self.modifiers, f)?;
510 display_key(&self.key, f)
511 }
512}
513
514impl std::fmt::Display for KeybindingKeystroke {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 display_modifiers(self.modifiers(), f)?;
517 display_key(self.key(), f)
518 }
519}
520
521/// The modifier keys held at some point in time.
522///
523/// Each field is `true` when that key is down. Use [`Modifiers::secondary`] for the shortcut key
524/// that matches each platform's convention, and the constructors such as [`Modifiers::shift`] to
525/// build a value.
526#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
527pub struct Modifiers {
528 /// The control key.
529 #[serde(default)]
530 pub control: bool,
531
532 /// The alt key, also known as option on macOS.
533 #[serde(default)]
534 pub alt: bool,
535
536 /// The shift key.
537 #[serde(default)]
538 pub shift: bool,
539
540 /// The platform key: command on macOS, the Windows key on Windows, and the super key on Linux.
541 #[serde(default)]
542 pub platform: bool,
543
544 /// The function key.
545 #[serde(default)]
546 pub function: bool,
547}
548
549impl Modifiers {
550 /// Returns whether any modifier key is held.
551 pub fn modified(&self) -> bool {
552 self.control || self.alt || self.shift || self.platform || self.function
553 }
554
555 /// Returns whether the platform's shortcut modifier is held.
556 ///
557 /// This is `platform` (command) on macOS and `control` on Linux and Windows. Use it for
558 /// shortcuts such as save and copy, so that they follow each platform's convention.
559 ///
560 /// # Examples
561 ///
562 /// ```
563 /// # use gpui::Modifiers;
564 /// assert!(Modifiers::secondary_key().secondary());
565 /// assert!(!Modifiers::shift().secondary());
566 /// ```
567 pub fn secondary(&self) -> bool {
568 #[cfg(target_os = "macos")]
569 {
570 self.platform
571 }
572
573 #[cfg(not(target_os = "macos"))]
574 {
575 self.control
576 }
577 }
578
579 /// Returns how many modifier keys are held.
580 pub fn number_of_modifiers(&self) -> u8 {
581 self.control as u8
582 + self.alt as u8
583 + self.shift as u8
584 + self.platform as u8
585 + self.function as u8
586 }
587
588 /// Returns [`Modifiers`] with no modifiers held.
589 pub fn none() -> Modifiers {
590 Default::default()
591 }
592
593 /// Returns [`Modifiers`] with only `platform` held: command on macOS, the Windows key on
594 /// Windows and the super key on Linux.
595 pub fn command() -> Modifiers {
596 Modifiers {
597 platform: true,
598 ..Default::default()
599 }
600 }
601
602 /// Returns [`Modifiers`] with only the [secondary](Modifiers::secondary) key held: `platform`
603 /// on macOS and `control` elsewhere.
604 pub fn secondary_key() -> Modifiers {
605 #[cfg(target_os = "macos")]
606 {
607 Modifiers {
608 platform: true,
609 ..Default::default()
610 }
611 }
612
613 #[cfg(not(target_os = "macos"))]
614 {
615 Modifiers {
616 control: true,
617 ..Default::default()
618 }
619 }
620 }
621
622 /// Returns [`Modifiers`] with only the Windows key held. This is `platform`, the same as
623 /// [`Modifiers::command`].
624 pub fn windows() -> Modifiers {
625 Modifiers {
626 platform: true,
627 ..Default::default()
628 }
629 }
630
631 /// Returns [`Modifiers`] with only the super key held. This is `platform`, the same as
632 /// [`Modifiers::command`].
633 pub fn super_key() -> Modifiers {
634 Modifiers {
635 platform: true,
636 ..Default::default()
637 }
638 }
639
640 /// Returns [`Modifiers`] with only control held.
641 pub fn control() -> Modifiers {
642 Modifiers {
643 control: true,
644 ..Default::default()
645 }
646 }
647
648 /// Returns [`Modifiers`] with only alt held.
649 pub fn alt() -> Modifiers {
650 Modifiers {
651 alt: true,
652 ..Default::default()
653 }
654 }
655
656 /// Returns [`Modifiers`] with only shift held.
657 pub fn shift() -> Modifiers {
658 Modifiers {
659 shift: true,
660 ..Default::default()
661 }
662 }
663
664 /// Returns [`Modifiers`] with only the function key held.
665 pub fn function() -> Modifiers {
666 Modifiers {
667 function: true,
668 ..Default::default()
669 }
670 }
671
672 /// Returns [`Modifiers`] with `platform` and `shift` held.
673 pub fn command_shift() -> Modifiers {
674 Modifiers {
675 shift: true,
676 platform: true,
677 ..Default::default()
678 }
679 }
680
681 /// Returns [`Modifiers`] with `control` and `shift` held.
682 pub fn control_shift() -> Modifiers {
683 Modifiers {
684 shift: true,
685 control: true,
686 ..Default::default()
687 }
688 }
689
690 /// Returns whether every modifier held in `self` is also held in `other`.
691 ///
692 /// `Modifiers::none()` is a subset of every value, and no value is a subset of `none()` unless
693 /// it is `none()` itself.
694 ///
695 /// # Examples
696 ///
697 /// ```
698 /// # use gpui::Modifiers;
699 /// let ctrl_shift = Modifiers::control_shift();
700 /// assert!(Modifiers::shift().is_subset_of(&ctrl_shift));
701 /// assert!(!Modifiers::alt().is_subset_of(&ctrl_shift));
702 /// ```
703 pub fn is_subset_of(&self, other: &Modifiers) -> bool {
704 (*other & *self) == *self
705 }
706}
707
708impl std::ops::BitOr for Modifiers {
709 type Output = Self;
710
711 fn bitor(mut self, other: Self) -> Self::Output {
712 self |= other;
713 self
714 }
715}
716
717impl std::ops::BitOrAssign for Modifiers {
718 fn bitor_assign(&mut self, other: Self) {
719 self.control |= other.control;
720 self.alt |= other.alt;
721 self.shift |= other.shift;
722 self.platform |= other.platform;
723 self.function |= other.function;
724 }
725}
726
727impl std::ops::BitXor for Modifiers {
728 type Output = Self;
729 fn bitxor(mut self, rhs: Self) -> Self::Output {
730 self ^= rhs;
731 self
732 }
733}
734
735impl std::ops::BitXorAssign for Modifiers {
736 fn bitxor_assign(&mut self, other: Self) {
737 self.control ^= other.control;
738 self.alt ^= other.alt;
739 self.shift ^= other.shift;
740 self.platform ^= other.platform;
741 self.function ^= other.function;
742 }
743}
744
745impl std::ops::BitAnd for Modifiers {
746 type Output = Self;
747 fn bitand(mut self, rhs: Self) -> Self::Output {
748 self &= rhs;
749 self
750 }
751}
752
753impl std::ops::BitAndAssign for Modifiers {
754 fn bitand_assign(&mut self, other: Self) {
755 self.control &= other.control;
756 self.alt &= other.alt;
757 self.shift &= other.shift;
758 self.platform &= other.platform;
759 self.function &= other.function;
760 }
761}
762
763/// The state of the caps lock key at some point in time.
764#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
765pub struct Capslock {
766 /// Whether caps lock is on.
767 #[serde(default)]
768 pub on: bool,
769}
770
771impl AsKeystroke for Keystroke {
772 fn as_keystroke(&self) -> &Keystroke {
773 self
774 }
775}
776
777impl AsKeystroke for KeybindingKeystroke {
778 fn as_keystroke(&self) -> &Keystroke {
779 &self.inner
780 }
781}
782
783impl<T: AsKeystroke + ?Sized> AsKeystroke for &T {
784 fn as_keystroke(&self) -> &Keystroke {
785 (**self).as_keystroke()
786 }
787}
788
789fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
790 if modifiers.control {
791 #[cfg(target_os = "macos")]
792 f.write_char('^')?;
793
794 #[cfg(not(target_os = "macos"))]
795 write!(f, "ctrl-")?;
796 }
797 if modifiers.alt {
798 #[cfg(target_os = "macos")]
799 f.write_char('⌥')?;
800
801 #[cfg(not(target_os = "macos"))]
802 write!(f, "alt-")?;
803 }
804 if modifiers.platform {
805 #[cfg(target_os = "macos")]
806 f.write_char('⌘')?;
807
808 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
809 f.write_char('❖')?;
810
811 #[cfg(target_os = "windows")]
812 f.write_char('⊞')?;
813 }
814 if modifiers.shift {
815 #[cfg(target_os = "macos")]
816 f.write_char('⇧')?;
817
818 #[cfg(not(target_os = "macos"))]
819 write!(f, "shift-")?;
820 }
821 Ok(())
822}
823
824fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
825 let key = match key {
826 #[cfg(target_os = "macos")]
827 "backspace" => '⌫',
828 #[cfg(target_os = "macos")]
829 "up" => '↑',
830 #[cfg(target_os = "macos")]
831 "down" => '↓',
832 #[cfg(target_os = "macos")]
833 "left" => '←',
834 #[cfg(target_os = "macos")]
835 "right" => '→',
836 #[cfg(target_os = "macos")]
837 "tab" => '⇥',
838 #[cfg(target_os = "macos")]
839 "escape" => '⎋',
840 #[cfg(target_os = "macos")]
841 "shift" => '⇧',
842 #[cfg(target_os = "macos")]
843 "control" => '⌃',
844 #[cfg(target_os = "macos")]
845 "alt" => '⌥',
846 #[cfg(target_os = "macos")]
847 "platform" => '⌘',
848
849 key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
850 key => return f.write_str(key),
851 };
852 f.write_char(key)
853}
854
855#[inline]
856fn unparse(modifiers: &Modifiers, key: &str) -> String {
857 let mut result = String::new();
858 if modifiers.function {
859 result.push_str("fn-");
860 }
861 if modifiers.control {
862 result.push_str("ctrl-");
863 }
864 if modifiers.alt {
865 result.push_str("alt-");
866 }
867 if modifiers.platform {
868 #[cfg(target_os = "macos")]
869 result.push_str("cmd-");
870
871 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
872 result.push_str("super-");
873
874 #[cfg(target_os = "windows")]
875 result.push_str("win-");
876 }
877 if modifiers.shift {
878 result.push_str("shift-");
879 }
880 result.push_str(&key);
881 result
882}