1use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::{
12 error::Error,
13 fmt::{Display, Write},
14};
15
16pub trait AsKeystroke {
18 fn as_keystroke(&self) -> &Keystroke;
20}
21
22#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
24pub struct Keystroke {
25 pub modifiers: Modifiers,
27
28 pub key: String,
34
35 pub key_char: Option<String>,
39}
40
41#[derive(Debug, Clone, Eq, PartialEq, Hash)]
43pub struct KeybindingKeystroke {
44 inner: Keystroke,
46 #[cfg(target_os = "windows")]
48 display_modifiers: Modifiers,
49 #[cfg(target_os = "windows")]
51 display_key: String,
52}
53
54#[derive(Debug)]
57pub struct InvalidKeystrokeError {
58 pub keystroke: String,
60}
61
62impl Error for InvalidKeystrokeError {}
63
64impl Display for InvalidKeystrokeError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(
67 f,
68 "Invalid keystroke \"{}\". {}",
69 self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
70 )
71 }
72}
73
74pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
76 (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
77 followed by a key, separated by `-`.";
78
79impl Keystroke {
80 pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
89 #[cfg(not(target_os = "windows"))]
90 if let Some(key_char) = self
91 .key_char
92 .as_ref()
93 .filter(|key_char| key_char != &&self.key)
94 {
95 let ime_modifiers = Modifiers {
96 control: self.modifiers.control,
97 platform: self.modifiers.platform,
98 ..Default::default()
99 };
100
101 if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
102 return true;
103 }
104 }
105
106 #[cfg(target_os = "windows")]
107 if let Some(key_char) = self
108 .key_char
109 .as_ref()
110 .filter(|key_char| key_char != &&self.key)
111 {
112 if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
114 return true;
115 }
116 }
117
118 target.inner.modifiers == self.modifiers && target.inner.key == self.key
119 }
120
121 pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
127 let mut modifiers = Modifiers::none();
128 let mut key = None;
129 let mut key_char = None;
130
131 let mut components = source.split('-').peekable();
132 while let Some(component) = components.next() {
133 if component.eq_ignore_ascii_case("ctrl") {
134 modifiers.control = true;
135 continue;
136 }
137 if component.eq_ignore_ascii_case("alt") {
138 modifiers.alt = true;
139 continue;
140 }
141 if component.eq_ignore_ascii_case("shift") {
142 modifiers.shift = true;
143 continue;
144 }
145 if component.eq_ignore_ascii_case("fn") {
146 modifiers.function = true;
147 continue;
148 }
149 if component.eq_ignore_ascii_case("secondary") {
150 if cfg!(target_os = "macos") {
151 modifiers.platform = true;
152 } else {
153 modifiers.control = true;
154 };
155 continue;
156 }
157
158 let is_platform = component.eq_ignore_ascii_case("cmd")
159 || component.eq_ignore_ascii_case("super")
160 || component.eq_ignore_ascii_case("win");
161
162 if is_platform {
163 modifiers.platform = true;
164 continue;
165 }
166
167 let mut key_str = component.to_string();
168
169 if let Some(next) = components.peek() {
170 if next.is_empty() && source.ends_with('-') {
171 key = Some(String::from("-"));
172 break;
173 } else if next.len() > 1 && next.starts_with('>') {
174 key = Some(key_str);
175 key_char = Some(String::from(&next[1..]));
176 components.next();
177 } else {
178 return Err(InvalidKeystrokeError {
179 keystroke: source.to_owned(),
180 });
181 }
182 continue;
183 }
184
185 if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
186 modifiers.shift = true;
188 key_str.make_ascii_lowercase();
189 } else {
190 key_str.make_ascii_lowercase()
193 }
194 key = Some(key_str);
195 }
196
197 key = key.or_else(|| {
200 use std::mem;
201 if mem::take(&mut modifiers.shift) {
203 Some("shift".to_string())
204 } else if mem::take(&mut modifiers.control) {
205 Some("control".to_string())
206 } else if mem::take(&mut modifiers.alt) {
207 Some("alt".to_string())
208 } else if mem::take(&mut modifiers.platform) {
209 Some("platform".to_string())
210 } else if mem::take(&mut modifiers.function) {
211 Some("function".to_string())
212 } else {
213 None
214 }
215 });
216
217 let key = key.ok_or_else(|| InvalidKeystrokeError {
218 keystroke: source.to_owned(),
219 })?;
220
221 Ok(Keystroke {
222 modifiers,
223 key,
224 key_char,
225 })
226 }
227
228 pub fn unparse(&self) -> String {
230 unparse(&self.modifiers, &self.key)
231 }
232
233 pub fn is_ime_in_progress(&self) -> bool {
236 self.key_char.is_none()
237 && (is_printable_key(&self.key) || self.key.is_empty())
238 && !(self.modifiers.platform
239 || self.modifiers.control
240 || self.modifiers.function
241 || self.modifiers.alt)
242 }
243
244 pub fn with_simulated_ime(mut self) -> Self {
248 if self.key_char.is_none()
249 && !self.modifiers.platform
250 && !self.modifiers.control
251 && !self.modifiers.function
252 && !self.modifiers.alt
253 {
254 self.key_char = match self.key.as_str() {
255 "space" => Some(" ".into()),
256 "tab" => Some("\t".into()),
257 "enter" => Some("\n".into()),
258 key if !is_printable_key(key) || key.is_empty() => None,
259 key => {
260 if self.modifiers.shift {
261 Some(key.to_uppercase())
262 } else {
263 Some(key.into())
264 }
265 }
266 }
267 }
268 self
269 }
270}
271
272impl KeybindingKeystroke {
273 #[cfg(target_os = "windows")]
274 #[expect(missing_docs)]
275 pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
276 KeybindingKeystroke {
277 inner,
278 display_modifiers,
279 display_key,
280 }
281 }
282
283 pub fn from_keystroke(keystroke: Keystroke) -> Self {
285 #[cfg(target_os = "windows")]
286 {
287 let key = keystroke.key.clone();
288 let modifiers = keystroke.modifiers;
289 KeybindingKeystroke {
290 inner: keystroke,
291 display_modifiers: modifiers,
292 display_key: key,
293 }
294 }
295 #[cfg(not(target_os = "windows"))]
296 {
297 KeybindingKeystroke { inner: keystroke }
298 }
299 }
300
301 pub fn inner(&self) -> &Keystroke {
303 &self.inner
304 }
305
306 pub fn modifiers(&self) -> &Modifiers {
313 #[cfg(target_os = "windows")]
314 {
315 &self.display_modifiers
316 }
317 #[cfg(not(target_os = "windows"))]
318 {
319 &self.inner.modifiers
320 }
321 }
322
323 pub fn key(&self) -> &str {
329 #[cfg(target_os = "windows")]
330 {
331 &self.display_key
332 }
333 #[cfg(not(target_os = "windows"))]
334 {
335 &self.inner.key
336 }
337 }
338
339 pub fn set_modifiers(&mut self, modifiers: Modifiers) {
341 self.inner.modifiers = modifiers;
342 #[cfg(target_os = "windows")]
343 {
344 self.display_modifiers = modifiers;
345 }
346 }
347
348 pub fn set_key(&mut self, key: String) {
350 #[cfg(target_os = "windows")]
351 {
352 self.display_key = key.clone();
353 }
354 self.inner.key = key;
355 }
356
357 pub fn unparse(&self) -> String {
359 #[cfg(target_os = "windows")]
360 {
361 unparse(&self.display_modifiers, &self.display_key)
362 }
363 #[cfg(not(target_os = "windows"))]
364 {
365 unparse(&self.inner.modifiers, &self.inner.key)
366 }
367 }
368
369 pub fn remove_key_char(&mut self) {
371 self.inner.key_char = None;
372 }
373}
374
375fn is_printable_key(key: &str) -> bool {
376 !matches!(
377 key,
378 "f1" | "f2"
379 | "f3"
380 | "f4"
381 | "f5"
382 | "f6"
383 | "f7"
384 | "f8"
385 | "f9"
386 | "f10"
387 | "f11"
388 | "f12"
389 | "f13"
390 | "f14"
391 | "f15"
392 | "f16"
393 | "f17"
394 | "f18"
395 | "f19"
396 | "f20"
397 | "f21"
398 | "f22"
399 | "f23"
400 | "f24"
401 | "f25"
402 | "f26"
403 | "f27"
404 | "f28"
405 | "f29"
406 | "f30"
407 | "f31"
408 | "f32"
409 | "f33"
410 | "f34"
411 | "f35"
412 | "backspace"
413 | "delete"
414 | "left"
415 | "right"
416 | "up"
417 | "down"
418 | "pageup"
419 | "pagedown"
420 | "insert"
421 | "home"
422 | "end"
423 | "back"
424 | "forward"
425 | "escape"
426 )
427}
428
429impl std::fmt::Display for Keystroke {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 display_modifiers(&self.modifiers, f)?;
432 display_key(&self.key, f)
433 }
434}
435
436impl std::fmt::Display for KeybindingKeystroke {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 display_modifiers(self.modifiers(), f)?;
439 display_key(self.key(), f)
440 }
441}
442
443#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
445pub struct Modifiers {
446 #[serde(default)]
448 pub control: bool,
449
450 #[serde(default)]
453 pub alt: bool,
454
455 #[serde(default)]
457 pub shift: bool,
458
459 #[serde(default)]
463 pub platform: bool,
464
465 #[serde(default)]
467 pub function: bool,
468}
469
470impl Modifiers {
471 pub fn modified(&self) -> bool {
473 self.control || self.alt || self.shift || self.platform || self.function
474 }
475
476 pub fn secondary(&self) -> bool {
481 #[cfg(target_os = "macos")]
482 {
483 self.platform
484 }
485
486 #[cfg(not(target_os = "macos"))]
487 {
488 self.control
489 }
490 }
491
492 pub fn number_of_modifiers(&self) -> u8 {
494 self.control as u8
495 + self.alt as u8
496 + self.shift as u8
497 + self.platform as u8
498 + self.function as u8
499 }
500
501 pub fn none() -> Modifiers {
503 Default::default()
504 }
505
506 pub fn command() -> Modifiers {
508 Modifiers {
509 platform: true,
510 ..Default::default()
511 }
512 }
513
514 pub fn secondary_key() -> Modifiers {
516 #[cfg(target_os = "macos")]
517 {
518 Modifiers {
519 platform: true,
520 ..Default::default()
521 }
522 }
523
524 #[cfg(not(target_os = "macos"))]
525 {
526 Modifiers {
527 control: true,
528 ..Default::default()
529 }
530 }
531 }
532
533 pub fn windows() -> Modifiers {
535 Modifiers {
536 platform: true,
537 ..Default::default()
538 }
539 }
540
541 pub fn super_key() -> Modifiers {
543 Modifiers {
544 platform: true,
545 ..Default::default()
546 }
547 }
548
549 pub fn control() -> Modifiers {
551 Modifiers {
552 control: true,
553 ..Default::default()
554 }
555 }
556
557 pub fn alt() -> Modifiers {
559 Modifiers {
560 alt: true,
561 ..Default::default()
562 }
563 }
564
565 pub fn shift() -> Modifiers {
567 Modifiers {
568 shift: true,
569 ..Default::default()
570 }
571 }
572
573 pub fn function() -> Modifiers {
575 Modifiers {
576 function: true,
577 ..Default::default()
578 }
579 }
580
581 pub fn command_shift() -> Modifiers {
583 Modifiers {
584 shift: true,
585 platform: true,
586 ..Default::default()
587 }
588 }
589
590 pub fn control_shift() -> Modifiers {
592 Modifiers {
593 shift: true,
594 control: true,
595 ..Default::default()
596 }
597 }
598
599 pub fn is_subset_of(&self, other: &Modifiers) -> bool {
601 (*other & *self) == *self
602 }
603}
604
605impl std::ops::BitOr for Modifiers {
606 type Output = Self;
607
608 fn bitor(mut self, other: Self) -> Self::Output {
609 self |= other;
610 self
611 }
612}
613
614impl std::ops::BitOrAssign for Modifiers {
615 fn bitor_assign(&mut self, other: Self) {
616 self.control |= other.control;
617 self.alt |= other.alt;
618 self.shift |= other.shift;
619 self.platform |= other.platform;
620 self.function |= other.function;
621 }
622}
623
624impl std::ops::BitXor for Modifiers {
625 type Output = Self;
626 fn bitxor(mut self, rhs: Self) -> Self::Output {
627 self ^= rhs;
628 self
629 }
630}
631
632impl std::ops::BitXorAssign for Modifiers {
633 fn bitxor_assign(&mut self, other: Self) {
634 self.control ^= other.control;
635 self.alt ^= other.alt;
636 self.shift ^= other.shift;
637 self.platform ^= other.platform;
638 self.function ^= other.function;
639 }
640}
641
642impl std::ops::BitAnd for Modifiers {
643 type Output = Self;
644 fn bitand(mut self, rhs: Self) -> Self::Output {
645 self &= rhs;
646 self
647 }
648}
649
650impl std::ops::BitAndAssign for Modifiers {
651 fn bitand_assign(&mut self, other: Self) {
652 self.control &= other.control;
653 self.alt &= other.alt;
654 self.shift &= other.shift;
655 self.platform &= other.platform;
656 self.function &= other.function;
657 }
658}
659
660#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
662pub struct Capslock {
663 #[serde(default)]
665 pub on: bool,
666}
667
668impl AsKeystroke for Keystroke {
669 fn as_keystroke(&self) -> &Keystroke {
670 self
671 }
672}
673
674impl AsKeystroke for KeybindingKeystroke {
675 fn as_keystroke(&self) -> &Keystroke {
676 &self.inner
677 }
678}
679
680impl<T: AsKeystroke + ?Sized> AsKeystroke for &T {
681 fn as_keystroke(&self) -> &Keystroke {
682 (**self).as_keystroke()
683 }
684}
685
686fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
687 if modifiers.control {
688 #[cfg(target_os = "macos")]
689 f.write_char('^')?;
690
691 #[cfg(not(target_os = "macos"))]
692 write!(f, "ctrl-")?;
693 }
694 if modifiers.alt {
695 #[cfg(target_os = "macos")]
696 f.write_char('⌥')?;
697
698 #[cfg(not(target_os = "macos"))]
699 write!(f, "alt-")?;
700 }
701 if modifiers.platform {
702 #[cfg(target_os = "macos")]
703 f.write_char('⌘')?;
704
705 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
706 f.write_char('❖')?;
707
708 #[cfg(target_os = "windows")]
709 f.write_char('⊞')?;
710 }
711 if modifiers.shift {
712 #[cfg(target_os = "macos")]
713 f.write_char('⇧')?;
714
715 #[cfg(not(target_os = "macos"))]
716 write!(f, "shift-")?;
717 }
718 Ok(())
719}
720
721fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
722 let key = match key {
723 #[cfg(target_os = "macos")]
724 "backspace" => '⌫',
725 #[cfg(target_os = "macos")]
726 "up" => '↑',
727 #[cfg(target_os = "macos")]
728 "down" => '↓',
729 #[cfg(target_os = "macos")]
730 "left" => '←',
731 #[cfg(target_os = "macos")]
732 "right" => '→',
733 #[cfg(target_os = "macos")]
734 "tab" => '⇥',
735 #[cfg(target_os = "macos")]
736 "escape" => '⎋',
737 #[cfg(target_os = "macos")]
738 "shift" => '⇧',
739 #[cfg(target_os = "macos")]
740 "control" => '⌃',
741 #[cfg(target_os = "macos")]
742 "alt" => '⌥',
743 #[cfg(target_os = "macos")]
744 "platform" => '⌘',
745
746 key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
747 key => return f.write_str(key),
748 };
749 f.write_char(key)
750}
751
752#[inline]
753fn unparse(modifiers: &Modifiers, key: &str) -> String {
754 let mut result = String::new();
755 if modifiers.function {
756 result.push_str("fn-");
757 }
758 if modifiers.control {
759 result.push_str("ctrl-");
760 }
761 if modifiers.alt {
762 result.push_str("alt-");
763 }
764 if modifiers.platform {
765 #[cfg(target_os = "macos")]
766 result.push_str("cmd-");
767
768 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
769 result.push_str("super-");
770
771 #[cfg(target_os = "windows")]
772 result.push_str("win-");
773 }
774 if modifiers.shift {
775 result.push_str("shift-");
776 }
777 result.push_str(key);
778 result
779}