1use std::fmt;
13use std::str::FromStr;
14
15use crate::error::Error;
16
17bitflags::bitflags! {
18 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
20 pub struct Modifiers: u8 {
21 const SHIFT = 1 << 0;
23 const CONTROL = 1 << 1;
25 const ALT = 1 << 2;
27 const META = 1 << 3;
29 const FUNCTION = 1 << 4;
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum KeypadKey {
38 Digit(u8),
40 Enter,
42 Equal,
44 Multiply,
46 Add,
48 Subtract,
50 Decimal,
52 Divide,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58#[non_exhaustive]
59pub enum Key {
60 Char(char),
62 Return,
64 Tab,
66 Space,
68 Escape,
70 Backspace,
72 Delete,
74 Insert,
76 Up,
78 Down,
80 Left,
82 Right,
84 Home,
86 End,
88 PageUp,
90 PageDown,
92 CapsLock,
94 Menu,
96 F(u8),
98 Keypad(KeypadKey),
100 Modifier(Modifiers),
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub struct KeyChord {
107 pub modifiers: Modifiers,
109 pub key: Key,
111}
112
113impl KeyChord {
114 #[must_use]
116 pub const fn new(key: Key) -> Self {
117 Self { modifiers: Modifiers::empty(), key }
118 }
119
120 #[must_use]
122 pub const fn with(mut self, modifiers: Modifiers) -> Self {
123 self.modifiers = self.modifiers.union(modifiers);
124 self
125 }
126}
127
128impl FromStr for KeyChord {
129 type Err = Error;
130
131 fn from_str(spec: &str) -> Result<Self, Self::Err> {
132 parse_chord(spec)
133 }
134}
135
136impl fmt::Display for KeyChord {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 for (flag, name) in [
139 (Modifiers::CONTROL, "ctrl"),
140 (Modifiers::ALT, "alt"),
141 (Modifiers::SHIFT, "shift"),
142 (Modifiers::META, "super"),
143 (Modifiers::FUNCTION, "fn"),
144 ] {
145 if self.modifiers.contains(flag) {
146 write!(f, "{name}+")?;
147 }
148 }
149 match self.key {
150 Key::Char(c) => write!(f, "{c}"),
151 Key::F(n) => write!(f, "F{n}"),
152 Key::Keypad(KeypadKey::Digit(d)) => write!(f, "KP_{d}"),
153 Key::Keypad(k) => write!(f, "KP_{k:?}"),
154 Key::Modifier(m) => write!(f, "{m:?}"),
155 other => write!(f, "{other:?}"),
156 }
157 }
158}
159
160fn parse_chord(spec: &str) -> Result<KeyChord, Error> {
161 let spec = spec.trim();
162 if spec.is_empty() {
163 return Err(Error::invalid("key specification is empty"));
164 }
165 let (body, trailing_plus) = match spec.strip_suffix("++") {
166 Some(rest) => (rest, true),
167 None if spec == "+" => ("", true),
168 None => (spec, false),
169 };
170 let mut tokens: Vec<&str> = body.split('+').map(str::trim).filter(|t| !t.is_empty()).collect();
171 let key_token = if trailing_plus {
172 "+"
173 } else {
174 tokens.pop().ok_or_else(|| Error::invalid(format!("unsupported key '{spec}'")))?
175 };
176
177 let mut modifiers = Modifiers::empty();
178 for token in tokens {
179 modifiers |= parse_modifier(token)
180 .ok_or_else(|| Error::invalid(format!("unsupported modifier '{token}' in '{spec}'")))?;
181 }
182 let key = parse_key(key_token).ok_or_else(|| Error::invalid(format!("unsupported key '{spec}'")))?;
183 if let Key::Char(c) = key
184 && key_token.chars().next().is_some_and(char::is_uppercase)
185 && key_token.chars().count() == 1
186 {
187 return Ok(KeyChord { modifiers: modifiers | Modifiers::SHIFT, key: Key::Char(c) });
188 }
189 Ok(KeyChord { modifiers, key })
190}
191
192fn parse_modifier(token: &str) -> Option<Modifiers> {
193 Some(match token.to_ascii_lowercase().as_str() {
194 "shift" | "shift_l" | "shift_r" => Modifiers::SHIFT,
195 "ctrl" | "control" | "control_l" | "control_r" | "ctrl_l" | "ctrl_r" => Modifiers::CONTROL,
196 "alt" | "option" | "opt" | "alt_l" | "alt_r" | "mod1" => Modifiers::ALT,
197 "super" | "cmd" | "command" | "meta" | "win" | "windows" | "super_l" | "super_r" | "meta_l" | "meta_r"
198 | "mod4" => Modifiers::META,
199 "fn" | "function" => Modifiers::FUNCTION,
200 _ => return None,
201 })
202}
203
204fn parse_key(token: &str) -> Option<Key> {
205 let mut chars = token.chars();
206 if let (Some(c), None) = (chars.next(), chars.next()) {
207 return Some(match c {
208 ' ' => Key::Space,
209 '\n' | '\r' => Key::Return,
210 '\t' => Key::Tab,
211 c => Key::Char(c.to_lowercase().next().unwrap_or(c)),
212 });
213 }
214 if let Some(m) = parse_modifier(token) {
215 return Some(Key::Modifier(m));
216 }
217 let lower = token.to_ascii_lowercase().replace([' ', '-'], "_");
218 if let Some(n) = lower.strip_prefix('f').and_then(|n| n.parse::<u8>().ok())
219 && (1..=24).contains(&n)
220 {
221 return Some(Key::F(n));
222 }
223 if let Some(rest) = lower.strip_prefix("kp_") {
224 return parse_keypad(rest);
225 }
226 Some(match lower.as_str() {
227 "return" | "enter" => Key::Return,
228 "tab" | "iso_left_tab" => Key::Tab,
229 "space" | "spacebar" => Key::Space,
230 "escape" | "esc" => Key::Escape,
231 "backspace" => Key::Backspace,
232 "delete" | "del" | "forwarddelete" | "forward_delete" => Key::Delete,
233 "insert" | "ins" | "help" => Key::Insert,
234 "up" | "arrowup" | "arrow_up" | "uparrow" => Key::Up,
235 "down" | "arrowdown" | "arrow_down" | "downarrow" => Key::Down,
236 "left" | "arrowleft" | "arrow_left" | "leftarrow" => Key::Left,
237 "right" | "arrowright" | "arrow_right" | "rightarrow" => Key::Right,
238 "home" => Key::Home,
239 "end" => Key::End,
240 "page_up" | "pageup" | "prior" => Key::PageUp,
241 "page_down" | "pagedown" | "next" => Key::PageDown,
242 "caps_lock" | "capslock" => Key::CapsLock,
243 "menu" | "apps" | "contextmenu" => Key::Menu,
244 _ => Key::Char(keysym_char(&lower)?),
245 })
246}
247
248fn parse_keypad(rest: &str) -> Option<Key> {
249 if let Ok(d) = rest.parse::<u8>()
250 && d <= 9
251 {
252 return Some(Key::Keypad(KeypadKey::Digit(d)));
253 }
254 Some(match rest {
255 "enter" => Key::Keypad(KeypadKey::Enter),
256 "equal" => Key::Keypad(KeypadKey::Equal),
257 "multiply" => Key::Keypad(KeypadKey::Multiply),
258 "add" => Key::Keypad(KeypadKey::Add),
259 "subtract" => Key::Keypad(KeypadKey::Subtract),
260 "decimal" | "delete" => Key::Keypad(KeypadKey::Decimal),
261 "divide" => Key::Keypad(KeypadKey::Divide),
262 "home" => Key::Home,
263 "end" => Key::End,
264 "left" => Key::Left,
265 "right" => Key::Right,
266 "up" => Key::Up,
267 "down" => Key::Down,
268 "prior" | "page_up" => Key::PageUp,
269 "next" | "page_down" => Key::PageDown,
270 "insert" => Key::Insert,
271 _ => return None,
272 })
273}
274
275fn keysym_char(name: &str) -> Option<char> {
276 Some(match name {
277 "minus" => '-',
278 "equal" => '=',
279 "plus" => '+',
280 "bracketleft" => '[',
281 "bracketright" => ']',
282 "braceleft" => '{',
283 "braceright" => '}',
284 "backslash" => '\\',
285 "bar" => '|',
286 "semicolon" => ';',
287 "colon" => ':',
288 "apostrophe" | "quoteright" => '\'',
289 "quotedbl" => '"',
290 "grave" | "quoteleft" => '`',
291 "asciitilde" => '~',
292 "comma" => ',',
293 "period" => '.',
294 "slash" => '/',
295 "question" => '?',
296 "less" => '<',
297 "greater" => '>',
298 "exclam" => '!',
299 "at" => '@',
300 "numbersign" => '#',
301 "dollar" => '$',
302 "percent" => '%',
303 "asciicircum" => '^',
304 "ampersand" => '&',
305 "asterisk" => '*',
306 "parenleft" => '(',
307 "parenright" => ')',
308 "underscore" => '_',
309 _ => return None,
310 })
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 fn chord(spec: &str) -> KeyChord {
318 spec.parse().unwrap_or_else(|e| panic!("{spec}: {e}"))
319 }
320
321 #[test]
322 fn parses_xdotool_names() {
323 assert_eq!(chord("Return").key, Key::Return);
324 assert_eq!(chord("BackSpace").key, Key::Backspace);
325 assert_eq!(chord("KP_0").key, Key::Keypad(KeypadKey::Digit(0)));
326 assert_eq!(chord("Page_Up").key, Key::PageUp);
327 assert_eq!(chord("Next").key, Key::PageDown);
328 assert_eq!(chord("F12").key, Key::F(12));
329 assert_eq!(chord("bracketleft").key, Key::Char('['));
330 assert_eq!(chord("space").key, Key::Space);
331 }
332
333 #[test]
334 fn parses_modifiers() {
335 let c = chord("super+c");
336 assert_eq!(c.modifiers, Modifiers::META);
337 assert_eq!(c.key, Key::Char('c'));
338 let c = chord("ctrl + shift + Tab");
339 assert_eq!(c.modifiers, Modifiers::CONTROL | Modifiers::SHIFT);
340 assert_eq!(c.key, Key::Tab);
341 assert_eq!(chord("cmd+A").modifiers, Modifiers::META | Modifiers::SHIFT);
342 assert_eq!(chord("shift").key, Key::Modifier(Modifiers::SHIFT));
343 }
344
345 #[test]
346 fn parses_plus_key() {
347 let c = chord("ctrl++");
348 assert_eq!((c.modifiers, c.key), (Modifiers::CONTROL, Key::Char('+')));
349 assert_eq!(chord("+").key, Key::Char('+'));
350 assert_eq!(chord("ctrl+plus").key, Key::Char('+'));
351 }
352
353 #[test]
354 fn rejects_garbage() {
355 assert!("".parse::<KeyChord>().is_err());
356 assert!("hyper+x".parse::<KeyChord>().is_err());
357 assert!("NotAKey".parse::<KeyChord>().is_err());
358 assert!("F25".parse::<KeyChord>().is_err());
359 }
360
361 #[test]
362 fn display_round_trips() {
363 for spec in ["ctrl+shift+t", "super+F5", "alt+KP_3", "Return"] {
364 let c = chord(spec);
365 assert_eq!(chord(&c.to_string()), c, "{spec} -> {c}");
366 }
367 }
368}