gravel_core/hotkeys/
parsing.rs1use super::ParsedBinding;
2use crate::hotkeys::{Key, Modifier};
3use enumflags2::BitFlags;
4use itertools::Itertools;
5use thiserror::Error;
6
7#[derive(Error, Debug, PartialEq, Eq)]
8pub enum ParseError {
9 #[error("'{0}' is not a valid modifier")]
10 InvalidModifier(String),
11 #[error("'{0}' is not a valid key")]
12 InvalidKey(String),
13 #[error("'{0}' is a valid modifier, but is used in place of a key")]
14 ModifierUsedAsKey(String),
15 #[error("binding is empty")]
16 Empty,
17}
18
19pub fn parse_binding(binding: &str) -> Result<ParsedBinding, ParseError> {
21 if binding.is_empty() {
22 return Err(ParseError::Empty);
23 }
24
25 let parts = binding.split('-').collect_vec();
26
27 let key = convert_key(parts.last().expect("vec always contains at least one item"))?;
28 let modifiers = parts
29 .iter()
30 .take(parts.len() - 1)
31 .try_fold(BitFlags::empty(), |r, v| convert_modifier(v).map(|m| r | m))?;
32
33 Ok(ParsedBinding { modifiers, key })
34}
35
36fn convert_modifier(value: &str) -> Result<Modifier, ParseError> {
37 let modifier = match value {
38 "A" => Modifier::Alt,
39 "C" => Modifier::Control,
40 "S" => Modifier::Shift,
41 "M" => Modifier::Super,
42 _ => return Err(ParseError::InvalidModifier(value.to_owned())),
43 };
44
45 Ok(modifier)
46}
47
48fn convert_key(value: &str) -> Result<Key, ParseError> {
49 if convert_modifier(value).is_ok() {
50 return Err(ParseError::ModifierUsedAsKey(value.to_owned()));
51 }
52
53 let key = match value {
54 "a" => Key::A,
55 "b" => Key::B,
56 "c" => Key::C,
57 "d" => Key::D,
58 "e" => Key::E,
59 "f" => Key::F,
60 "g" => Key::G,
61 "h" => Key::H,
62 "i" => Key::I,
63 "j" => Key::J,
64 "k" => Key::K,
65 "l" => Key::L,
66 "m" => Key::M,
67 "n" => Key::N,
68 "o" => Key::O,
69 "p" => Key::P,
70 "q" => Key::Q,
71 "r" => Key::R,
72 "s" => Key::S,
73 "t" => Key::T,
74 "u" => Key::U,
75 "v" => Key::V,
76 "w" => Key::W,
77 "x" => Key::X,
78 "y" => Key::Y,
79 "z" => Key::Z,
80 _ => match value.to_lowercase().as_str() {
81 "<backspace>" => Key::Backspace,
82 "<tab>" => Key::Tab,
83 "<enter>" => Key::Enter,
84 "<caps_lock>" => Key::CapsLock,
85 "<escape>" => Key::Escape,
86 "<space>" => Key::Space,
87 "<page_up>" => Key::PageUp,
88 "<page_down>" => Key::PageDown,
89 "<end>" => Key::End,
90 "<home>" => Key::Home,
91 "<left>" => Key::Left,
92 "<right>" => Key::Right,
93 "<up>" => Key::Up,
94 "<down>" => Key::Down,
95 "<print_screen>" => Key::PrintScreen,
96 "<insert>" => Key::Insert,
97 "<delete>" => Key::Delete,
98 _ => return Err(ParseError::InvalidKey(value.to_owned())),
99 },
100 };
101
102 Ok(key)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::hotkeys::{Key, Modifier};
109 use enumflags2::BitFlags;
110 use rstest::rstest;
111
112 #[rstest]
113 #[case("q", ParsedBinding {modifiers: BitFlags::empty(), key: Key::Q})]
114 #[case("C-a", ParsedBinding {modifiers: Modifier::Control.into(), key: Key::A})]
115 #[case("C-A-S-s", ParsedBinding {modifiers: Modifier::Control | Modifier::Alt | Modifier::Shift, key: Key::S})]
116 #[case("M-S-<PRINT_screen>", ParsedBinding {modifiers: Modifier::Super | Modifier::Shift, key: Key::PrintScreen})]
117 fn should_parse(#[case] binding: &str, #[case] expected: ParsedBinding) {
118 let actual = parse_binding(binding);
119 assert_eq!(actual, Ok(expected));
120 }
121
122 #[rstest]
123 #[case("not- working", ParseError::InvalidKey(String::from(" working")))]
124 #[case("Z", ParseError::InvalidKey(String::from("Z")))]
125 #[case("c-d", ParseError::InvalidModifier(String::from("c")))]
126 #[case("C-S", ParseError::ModifierUsedAsKey(String::from("S")))]
127 #[case("", ParseError::Empty)]
128 fn should_err(#[case] binding: &str, #[case] expected: ParseError) {
129 let actual = parse_binding(binding);
130 assert_eq!(actual, Err(expected));
131 }
132}