1use std::fmt::Write;
2
3use anyhow::anyhow;
4use serde::Deserialize;
5use smallvec::SmallVec;
6
7#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
8pub struct Keystroke {
9 pub ctrl: bool,
10 pub alt: bool,
11 pub shift: bool,
12 pub cmd: bool,
13 pub function: bool,
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 /// ime_key is the character inserted by the IME engine when that key was pressed.
18 /// e.g. for option-s, ime_key is "ร"
19 pub ime_key: Option<String>,
20}
21
22impl Keystroke {
23 // When matching a key we cannot know whether the user intended to type
24 // the ime_key or the key. On some non-US keyboards keys we use in our
25 // bindings are behind option (for example `$` is typed `alt-รง` on a Czech keyboard),
26 // and on some keyboards the IME handler converts a sequence of keys into a
27 // specific character (for example `"` is typed as `" space` on a brazillian keyboard).
28 pub fn match_possibilities(&self) -> SmallVec<[Keystroke; 2]> {
29 let mut possibilities = SmallVec::new();
30 match self.ime_key.as_ref() {
31 None => possibilities.push(self.clone()),
32 Some(ime_key) => {
33 possibilities.push(Keystroke {
34 ctrl: self.ctrl,
35 alt: false,
36 shift: false,
37 cmd: false,
38 function: false,
39 key: ime_key.to_string(),
40 ime_key: None,
41 });
42 possibilities.push(Keystroke {
43 ime_key: None,
44 ..self.clone()
45 });
46 }
47 }
48 possibilities
49 }
50
51 /// key syntax is:
52 /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
53 /// ime_key is only used for generating test events,
54 /// when matching a key with an ime_key set will be matched without it.
55 pub fn parse(source: &str) -> anyhow::Result<Self> {
56 let mut ctrl = false;
57 let mut alt = false;
58 let mut shift = false;
59 let mut cmd = false;
60 let mut function = false;
61 let mut key = None;
62 let mut ime_key = None;
63
64 let mut components = source.split('-').peekable();
65 while let Some(component) = components.next() {
66 match component {
67 "ctrl" => ctrl = true,
68 "alt" => alt = true,
69 "shift" => shift = true,
70 "cmd" => cmd = true,
71 "fn" => function = true,
72 _ => {
73 if let Some(next) = components.peek() {
74 if next.is_empty() && source.ends_with('-') {
75 key = Some(String::from("-"));
76 break;
77 } else if next.len() > 1 && next.starts_with('>') {
78 key = Some(String::from(component));
79 ime_key = Some(String::from(&next[1..]));
80 components.next();
81 } else {
82 return Err(anyhow!("Invalid keystroke `{}`", source));
83 }
84 } else {
85 key = Some(String::from(component));
86 }
87 }
88 }
89 }
90
91 let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
92
93 Ok(Keystroke {
94 ctrl,
95 alt,
96 shift,
97 cmd,
98 function,
99 key,
100 ime_key,
101 })
102 }
103
104 pub fn modified(&self) -> bool {
105 self.ctrl || self.alt || self.shift || self.cmd
106 }
107}
108
109impl std::fmt::Display for Keystroke {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 if self.ctrl {
112 f.write_char('^')?;
113 }
114 if self.alt {
115 f.write_char('โ')?;
116 }
117 if self.cmd {
118 f.write_char('โ')?;
119 }
120 if self.shift {
121 f.write_char('โง')?;
122 }
123 let key = match self.key.as_str() {
124 "backspace" => 'โซ',
125 "up" => 'โ',
126 "down" => 'โ',
127 "left" => 'โ',
128 "right" => 'โ',
129 "tab" => 'โฅ',
130 "escape" => 'โ',
131 key => {
132 if key.len() == 1 {
133 key.chars().next().unwrap().to_ascii_uppercase()
134 } else {
135 return f.write_str(key);
136 }
137 }
138 };
139 f.write_char(key)
140 }
141}