gravel_core/hotkeys/
parsing.rs

1use 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
19/// Parse an emacs-like keybinding. Does not support cords.
20pub 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}