1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{
4 error::Error,
5 fmt::{Display, Write},
6};
7
8use crate::{OperatingSystem, PlatformKeyboardMapper};
9
10pub trait AsKeystroke {
12 fn as_keystroke(&self) -> &Keystroke;
14}
15
16#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
18pub struct Keystroke {
19 pub modifiers: Modifiers,
21
22 pub key: String,
28
29 pub key_char: Option<String>,
33}
34
35#[derive(Debug, Clone, Eq, PartialEq, Hash)]
37pub struct KeybindingKeystroke {
38 inner: Keystroke,
40 #[cfg(target_os = "windows")]
42 display_modifiers: Modifiers,
43 #[cfg(target_os = "windows")]
45 display_key: String,
46}
47
48#[derive(Debug)]
51pub struct InvalidKeystrokeError {
52 pub keystroke: String,
54}
55
56impl Error for InvalidKeystrokeError {}
57
58impl Display for InvalidKeystrokeError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(
61 f,
62 "Invalid keystroke \"{}\". {}",
63 self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
64 )
65 }
66}
67
68pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
70 (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
71 followed by a key, separated by `-`.";
72
73impl Keystroke {
74 pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
83 #[cfg(not(target_os = "windows"))]
84 if let Some(key_char) = self
85 .key_char
86 .as_ref()
87 .filter(|key_char| key_char != &&self.key)
88 {
89 let ime_modifiers = Modifiers {
90 control: self.modifiers.control,
91 platform: self.modifiers.platform,
92 ..Default::default()
93 };
94
95 if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
96 return true;
97 }
98 }
99
100 #[cfg(target_os = "windows")]
101 if let Some(key_char) = self
102 .key_char
103 .as_ref()
104 .filter(|key_char| key_char != &&self.key)
105 {
106 if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
108 return true;
109 }
110 }
111
112 target.inner.modifiers == self.modifiers && target.inner.key == self.key
113 }
114
115 pub fn parse(source: &'static str) -> std::result::Result<Self, InvalidKeystrokeError> {
121 Self::parse_internal(source, None)
122 }
123
124 pub fn parse_with_os(
126 source: &str,
127 os: OperatingSystem,
128 ) -> std::result::Result<Self, InvalidKeystrokeError> {
129 Self::parse_internal(source, Some(os))
130 }
131
132 fn parse_internal(
133 source: &str,
134 os: Option<OperatingSystem>,
135 ) -> std::result::Result<Self, InvalidKeystrokeError> {
136 let mut modifiers = Modifiers::none();
137 let mut key = None;
138 let mut key_char = None;
139
140 let mut components = source.split('-').peekable();
141 while let Some(component) = components.next() {
142 if component.eq_ignore_ascii_case("ctrl") {
143 modifiers.control = true;
144 continue;
145 }
146 if component.eq_ignore_ascii_case("alt") {
147 modifiers.alt = true;
148 continue;
149 }
150 if component.eq_ignore_ascii_case("shift") {
151 modifiers.shift = true;
152 continue;
153 }
154 if component.eq_ignore_ascii_case("fn") {
155 modifiers.function = true;
156 continue;
157 }
158 if component.eq_ignore_ascii_case("secondary") {
159 let os = os.ok_or_else(|| InvalidKeystrokeError {
160 keystroke: source.to_owned(),
161 })?;
162 if os == OperatingSystem::Mac {
163 modifiers.platform = true;
164 } else {
165 modifiers.control = true;
166 };
167 continue;
168 }
169
170 let is_platform = component.eq_ignore_ascii_case("cmd")
171 || component.eq_ignore_ascii_case("super")
172 || component.eq_ignore_ascii_case("win");
173
174 if is_platform {
175 modifiers.platform = true;
176 continue;
177 }
178
179 let mut key_str = component.to_string();
180
181 if let Some(next) = components.peek() {
182 if next.is_empty() && source.ends_with('-') {
183 key = Some(String::from("-"));
184 break;
185 } else if next.len() > 1 && next.starts_with('>') {
186 key = Some(key_str);
187 key_char = Some(String::from(&next[1..]));
188 components.next();
189 } else {
190 return Err(InvalidKeystrokeError {
191 keystroke: source.to_owned(),
192 });
193 }
194 continue;
195 }
196
197 if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
198 modifiers.shift = true;
200 key_str.make_ascii_lowercase();
201 } else {
202 key_str.make_ascii_lowercase()
205 }
206 key = Some(key_str);
207 }
208
209 key = key.or_else(|| {
212 use std::mem;
213 if mem::take(&mut modifiers.shift) {
215 Some("shift".to_string())
216 } else if mem::take(&mut modifiers.control) {
217 Some("control".to_string())
218 } else if mem::take(&mut modifiers.alt) {
219 Some("alt".to_string())
220 } else if mem::take(&mut modifiers.platform) {
221 Some("platform".to_string())
222 } else if mem::take(&mut modifiers.function) {
223 Some("function".to_string())
224 } else {
225 None
226 }
227 });
228
229 let key = key.ok_or_else(|| InvalidKeystrokeError {
230 keystroke: source.to_owned(),
231 })?;
232
233 Ok(Keystroke {
234 modifiers,
235 key,
236 key_char,
237 })
238 }
239
240 pub fn unparse(&self, os: OperatingSystem) -> String {
242 unparse(&self.modifiers, &self.key, os)
243 }
244
245 pub fn display(&self, os: OperatingSystem) -> KeystrokeDisplay<'_> {
247 KeystrokeDisplay {
248 modifiers: &self.modifiers,
249 key: &self.key,
250 os,
251 }
252 }
253
254 pub fn is_ime_in_progress(&self) -> bool {
257 self.key_char.is_none()
258 && (is_printable_key(&self.key) || self.key.is_empty())
259 && !(self.modifiers.platform
260 || self.modifiers.control
261 || self.modifiers.function
262 || self.modifiers.alt)
263 }
264
265 pub fn with_simulated_ime(mut self) -> Self {
269 if self.key_char.is_none()
270 && !self.modifiers.platform
271 && !self.modifiers.control
272 && !self.modifiers.function
273 && !self.modifiers.alt
274 {
275 self.key_char = match self.key.as_str() {
276 "space" => Some(" ".into()),
277 "tab" => Some("\t".into()),
278 "enter" => Some("\n".into()),
279 key if !is_printable_key(key) || key.is_empty() => None,
280 key => {
281 if self.modifiers.shift {
282 Some(key.to_uppercase())
283 } else {
284 Some(key.into())
285 }
286 }
287 }
288 }
289 self
290 }
291
292 pub fn is_modifier_key(&self) -> bool {
294 matches!(
295 self.key.as_str(),
296 "shift" | "control" | "alt" | "platform" | "function"
297 )
298 }
299}
300
301impl KeybindingKeystroke {
302 pub fn display(&self, os: OperatingSystem) -> KeystrokeDisplay<'_> {
304 KeystrokeDisplay {
305 modifiers: self.modifiers(),
306 key: self.key(),
307 os,
308 }
309 }
310
311 #[cfg(target_os = "windows")]
312 #[expect(missing_docs)]
313 pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
314 KeybindingKeystroke {
315 inner,
316 display_modifiers,
317 display_key,
318 }
319 }
320
321 pub fn new_with_mapper(
323 inner: Keystroke,
324 use_key_equivalents: bool,
325 keyboard_mapper: &dyn PlatformKeyboardMapper,
326 ) -> Self {
327 keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
328 }
329
330 pub fn from_keystroke(keystroke: Keystroke) -> Self {
332 #[cfg(target_os = "windows")]
333 {
334 let key = keystroke.key.clone();
335 let modifiers = keystroke.modifiers;
336 KeybindingKeystroke {
337 inner: keystroke,
338 display_modifiers: modifiers,
339 display_key: key,
340 }
341 }
342 #[cfg(not(target_os = "windows"))]
343 {
344 KeybindingKeystroke { inner: keystroke }
345 }
346 }
347
348 pub fn inner(&self) -> &Keystroke {
350 &self.inner
351 }
352
353 pub fn modifiers(&self) -> &Modifiers {
360 #[cfg(target_os = "windows")]
361 {
362 &self.display_modifiers
363 }
364 #[cfg(not(target_os = "windows"))]
365 {
366 &self.inner.modifiers
367 }
368 }
369
370 pub fn key(&self) -> &str {
376 #[cfg(target_os = "windows")]
377 {
378 &self.display_key
379 }
380 #[cfg(not(target_os = "windows"))]
381 {
382 &self.inner.key
383 }
384 }
385
386 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
388 self.inner.modifiers = modifiers;
389 #[cfg(target_os = "windows")]
390 {
391 self.display_modifiers = modifiers;
392 }
393 }
394
395 pub fn set_key(&mut self, key: String) {
397 #[cfg(target_os = "windows")]
398 {
399 self.display_key = key.clone();
400 }
401 self.inner.key = key;
402 }
403
404 pub fn unparse(&self, os: OperatingSystem) -> String {
406 #[cfg(target_os = "windows")]
407 {
408 unparse(&self.display_modifiers, &self.display_key, os)
409 }
410 #[cfg(not(target_os = "windows"))]
411 {
412 unparse(&self.inner.modifiers, &self.inner.key, os)
413 }
414 }
415
416 pub fn remove_key_char(&mut self) {
418 self.inner.key_char = None;
419 }
420}
421
422fn is_printable_key(key: &str) -> bool {
423 !matches!(
424 key,
425 "f1" | "f2"
426 | "f3"
427 | "f4"
428 | "f5"
429 | "f6"
430 | "f7"
431 | "f8"
432 | "f9"
433 | "f10"
434 | "f11"
435 | "f12"
436 | "f13"
437 | "f14"
438 | "f15"
439 | "f16"
440 | "f17"
441 | "f18"
442 | "f19"
443 | "f20"
444 | "f21"
445 | "f22"
446 | "f23"
447 | "f24"
448 | "f25"
449 | "f26"
450 | "f27"
451 | "f28"
452 | "f29"
453 | "f30"
454 | "f31"
455 | "f32"
456 | "f33"
457 | "f34"
458 | "f35"
459 | "backspace"
460 | "delete"
461 | "left"
462 | "right"
463 | "up"
464 | "down"
465 | "pageup"
466 | "pagedown"
467 | "insert"
468 | "home"
469 | "end"
470 | "back"
471 | "forward"
472 | "escape"
473 )
474}
475
476pub struct KeystrokeDisplay<'a> {
478 modifiers: &'a Modifiers,
479 key: &'a str,
480 os: OperatingSystem,
481}
482
483impl std::fmt::Display for KeystrokeDisplay<'_> {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485 display_modifiers(self.modifiers, self.os, f)?;
486 display_key(self.key, self.os, f)
487 }
488}
489
490#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
492pub struct Modifiers {
493 #[serde(default)]
495 pub control: bool,
496
497 #[serde(default)]
500 pub alt: bool,
501
502 #[serde(default)]
504 pub shift: bool,
505
506 #[serde(default)]
510 pub platform: bool,
511
512 #[serde(default)]
514 pub function: bool,
515}
516
517impl Modifiers {
518 pub fn modified(&self) -> bool {
520 self.control || self.alt || self.shift || self.platform || self.function
521 }
522
523 pub fn secondary(&self, cx: &crate::App) -> bool {
528 if cx.platform_info().is_mac() {
529 self.platform
530 } else {
531 self.control
532 }
533 }
534
535 pub fn number_of_modifiers(&self) -> u8 {
537 self.control as u8
538 + self.alt as u8
539 + self.shift as u8
540 + self.platform as u8
541 + self.function as u8
542 }
543
544 pub fn none() -> Modifiers {
546 Default::default()
547 }
548
549 pub fn command() -> Modifiers {
551 Modifiers {
552 platform: true,
553 ..Default::default()
554 }
555 }
556
557 pub fn secondary_key(cx: &crate::App) -> Modifiers {
559 if cx.platform_info().is_mac() {
560 Modifiers {
561 platform: true,
562 ..Default::default()
563 }
564 } else {
565 Modifiers {
566 control: true,
567 ..Default::default()
568 }
569 }
570 }
571
572 pub fn windows() -> Modifiers {
574 Modifiers {
575 platform: true,
576 ..Default::default()
577 }
578 }
579
580 pub fn super_key() -> Modifiers {
582 Modifiers {
583 platform: true,
584 ..Default::default()
585 }
586 }
587
588 pub fn control() -> Modifiers {
590 Modifiers {
591 control: true,
592 ..Default::default()
593 }
594 }
595
596 pub fn alt() -> Modifiers {
598 Modifiers {
599 alt: true,
600 ..Default::default()
601 }
602 }
603
604 pub fn shift() -> Modifiers {
606 Modifiers {
607 shift: true,
608 ..Default::default()
609 }
610 }
611
612 pub fn function() -> Modifiers {
614 Modifiers {
615 function: true,
616 ..Default::default()
617 }
618 }
619
620 pub fn command_shift() -> Modifiers {
622 Modifiers {
623 shift: true,
624 platform: true,
625 ..Default::default()
626 }
627 }
628
629 pub fn control_shift() -> Modifiers {
631 Modifiers {
632 shift: true,
633 control: true,
634 ..Default::default()
635 }
636 }
637
638 pub fn is_subset_of(&self, other: &Modifiers) -> bool {
640 (*other & *self) == *self
641 }
642}
643
644impl std::ops::BitOr for Modifiers {
645 type Output = Self;
646
647 fn bitor(mut self, other: Self) -> Self::Output {
648 self |= other;
649 self
650 }
651}
652
653impl std::ops::BitOrAssign for Modifiers {
654 fn bitor_assign(&mut self, other: Self) {
655 self.control |= other.control;
656 self.alt |= other.alt;
657 self.shift |= other.shift;
658 self.platform |= other.platform;
659 self.function |= other.function;
660 }
661}
662
663impl std::ops::BitXor for Modifiers {
664 type Output = Self;
665 fn bitxor(mut self, rhs: Self) -> Self::Output {
666 self ^= rhs;
667 self
668 }
669}
670
671impl std::ops::BitXorAssign for Modifiers {
672 fn bitxor_assign(&mut self, other: Self) {
673 self.control ^= other.control;
674 self.alt ^= other.alt;
675 self.shift ^= other.shift;
676 self.platform ^= other.platform;
677 self.function ^= other.function;
678 }
679}
680
681impl std::ops::BitAnd for Modifiers {
682 type Output = Self;
683 fn bitand(mut self, rhs: Self) -> Self::Output {
684 self &= rhs;
685 self
686 }
687}
688
689impl std::ops::BitAndAssign for Modifiers {
690 fn bitand_assign(&mut self, other: Self) {
691 self.control &= other.control;
692 self.alt &= other.alt;
693 self.shift &= other.shift;
694 self.platform &= other.platform;
695 self.function &= other.function;
696 }
697}
698
699#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
701pub struct Capslock {
702 #[serde(default)]
704 pub on: bool,
705}
706
707impl AsKeystroke for Keystroke {
708 fn as_keystroke(&self) -> &Keystroke {
709 self
710 }
711}
712
713impl AsKeystroke for KeybindingKeystroke {
714 fn as_keystroke(&self) -> &Keystroke {
715 &self.inner
716 }
717}
718
719impl<T: AsKeystroke + ?Sized> AsKeystroke for &T {
720 fn as_keystroke(&self) -> &Keystroke {
721 (**self).as_keystroke()
722 }
723}
724
725fn display_modifiers(
726 modifiers: &Modifiers,
727 os: OperatingSystem,
728 f: &mut std::fmt::Formatter<'_>,
729) -> std::fmt::Result {
730 if modifiers.control {
731 f.write_str(if os == OperatingSystem::Mac {
732 "^"
733 } else {
734 "ctrl-"
735 })?;
736 }
737 if modifiers.alt {
738 f.write_str(if os == OperatingSystem::Mac {
739 "⌥"
740 } else {
741 "alt-"
742 })?;
743 }
744 if modifiers.platform {
745 f.write_str(match os {
746 OperatingSystem::Mac => "⌘",
747 OperatingSystem::Windows => "⊞",
748 OperatingSystem::Linux | OperatingSystem::FreeBsd => "❖",
749 OperatingSystem::Ios | OperatingSystem::Android | OperatingSystem::Unknown => "cmd-",
750 })?;
751 }
752 if modifiers.shift {
753 f.write_str(if os == OperatingSystem::Mac {
754 "⇧"
755 } else {
756 "shift-"
757 })?;
758 }
759 Ok(())
760}
761
762fn display_key(
763 key: &str,
764 os: OperatingSystem,
765 f: &mut std::fmt::Formatter<'_>,
766) -> std::fmt::Result {
767 let key = match key {
768 "backspace" if os == OperatingSystem::Mac => '⌫',
769 "up" if os == OperatingSystem::Mac => '↑',
770 "down" if os == OperatingSystem::Mac => '↓',
771 "left" if os == OperatingSystem::Mac => '←',
772 "right" if os == OperatingSystem::Mac => '→',
773 "tab" if os == OperatingSystem::Mac => '⇥',
774 "escape" if os == OperatingSystem::Mac => '⎋',
775 "shift" if os == OperatingSystem::Mac => '⇧',
776 "control" if os == OperatingSystem::Mac => '⌃',
777 "alt" if os == OperatingSystem::Mac => '⌥',
778 "platform" if os == OperatingSystem::Mac => '⌘',
779
780 key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
781 key => return f.write_str(key),
782 };
783 f.write_char(key)
784}
785
786#[inline]
787fn unparse(modifiers: &Modifiers, key: &str, os: OperatingSystem) -> String {
788 let mut result = String::new();
789 if modifiers.function {
790 result.push_str("fn-");
791 }
792 if modifiers.control {
793 result.push_str("ctrl-");
794 }
795 if modifiers.alt {
796 result.push_str("alt-");
797 }
798 if modifiers.platform {
799 result.push_str(match os {
800 OperatingSystem::Windows => "win-",
801 OperatingSystem::Linux
802 | OperatingSystem::FreeBsd
803 | OperatingSystem::Android
804 | OperatingSystem::Unknown => "super-",
805 OperatingSystem::Mac | OperatingSystem::Ios => "cmd-",
806 });
807 }
808 if modifiers.shift {
809 result.push_str("shift-");
810 }
811 result.push_str(&key);
812 result
813}