1use crate::{CtlError, ErrorCode};
8
9const MODIFIERS: &[&str] = &["ctrl", "shift", "alt", "win"];
10
11pub const NAMED_KEYS: &[&str] = &[
13 "enter",
14 "esc",
15 "tab",
16 "space",
17 "del",
18 "delete",
19 "backspace",
20 "back",
21 "up",
22 "down",
23 "left",
24 "right",
25 "home",
26 "end",
27 "pgup",
28 "pgdn",
29];
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Key {
33 Char(char),
34 Named(String),
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct KeySpec {
39 pub modifiers: Vec<String>,
41 pub keys: Vec<Key>,
42}
43
44pub fn parse_key_expr(expr: &str) -> Result<KeySpec, CtlError> {
46 let mut mods: Vec<String> = Vec::new();
47 let mut keys: Vec<Key> = Vec::new();
48 for part in expr.split('+') {
49 let p = part.trim().to_lowercase();
50 if p.is_empty() {
51 return Err(bad(expr));
52 }
53 if MODIFIERS.contains(&p.as_str()) {
54 if !mods.iter().any(|m| m == &p) {
55 mods.push(p);
56 }
57 continue;
58 }
59 let mut chars = p.chars();
60 if let (Some(c), None) = (chars.next(), chars.next()) {
61 keys.push(Key::Char(c));
62 continue;
63 }
64 let named = match p.as_str() {
65 "delete" => "del",
66 "back" => "backspace",
67 other => other,
68 };
69 let is_fkey = (2..=3).contains(&p.len())
71 && p.starts_with('f')
72 && p[1..].chars().all(|c| c.is_ascii_digit())
73 && (1..=12).contains(&p[1..].parse::<u8>().unwrap_or(0));
74 if NAMED_KEYS.contains(&named) || is_fkey {
75 keys.push(Key::Named(named.to_string()));
76 continue;
77 }
78 return Err(bad(expr)); }
80 if keys.is_empty() && mods.is_empty() {
83 return Err(bad(expr));
84 }
85 let modifiers = MODIFIERS
87 .iter()
88 .filter(|m| mods.iter().any(|x| x == *m))
89 .map(|m| m.to_string())
90 .collect();
91 Ok(KeySpec { modifiers, keys })
92}
93
94fn bad(expr: &str) -> CtlError {
95 CtlError::new(
96 ErrorCode::Protocol,
97 format!("invalid key expression {expr:?}: use e.g. ctrl+s, ctrl+shift+t, enter, f5"),
98 )
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn parses_common_combos() {
107 let spec = parse_key_expr("ctrl+s").unwrap();
108 assert_eq!(spec.modifiers, vec!["ctrl"]);
109 assert_eq!(spec.keys, vec![Key::Char('s')]);
110
111 let spec = parse_key_expr("ctrl+shift+t").unwrap();
112 assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
113
114 let spec = parse_key_expr("alt+F4").unwrap();
115 assert_eq!(spec.keys, vec![Key::Named("f4".into())]);
116
117 assert_eq!(
118 parse_key_expr("pgdn").unwrap().keys,
119 vec![Key::Named("pgdn".into())]
120 );
121 assert_eq!(parse_key_expr("a").unwrap().keys, vec![Key::Char('a')]);
122 }
123
124 #[test]
125 fn modifier_order_is_normalized_and_deduped() {
126 let spec = parse_key_expr("shift+ctrl+a").unwrap();
127 assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
128 let spec = parse_key_expr("ctrl+ctrl+a").unwrap();
129 assert_eq!(spec.modifiers, vec!["ctrl"]);
130 }
131
132 #[test]
133 fn aliases_normalize() {
134 assert_eq!(
135 parse_key_expr("delete").unwrap().keys,
136 vec![Key::Named("del".into())]
137 );
138 assert_eq!(
139 parse_key_expr("back").unwrap().keys,
140 vec![Key::Named("backspace".into())]
141 );
142 }
143
144 #[test]
145 fn rejects_garbage() {
146 for bad in ["", "foo+bar", "ctrl+", "+a", "notakey", "f0", "f13", "f99"] {
148 assert_eq!(
149 parse_key_expr(bad).unwrap_err().code,
150 ErrorCode::Protocol,
151 "{bad:?} should be PROTOCOL"
152 );
153 }
154 }
155
156 #[test]
157 fn fkeys_f1_through_f12_all_parse() {
158 for name in [
160 "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
161 ] {
162 let spec = parse_key_expr(name).unwrap_or_else(|e| panic!("{name}: {e:?}"));
163 assert_eq!(spec.keys, vec![Key::Named(name.into())], "{name}");
164 assert!(spec.modifiers.is_empty());
165 }
166 }
167}
168
169#[cfg(test)]
170mod bare_modifier_tests {
171 use super::*;
172
173 #[test]
174 fn bare_modifier_is_a_keyable_press() {
175 let spec = parse_key_expr("win").unwrap();
177 assert_eq!(spec.modifiers, vec!["win"]);
178 assert!(spec.keys.is_empty());
179 assert!(parse_key_expr("shift").is_ok());
180 assert!(parse_key_expr("").is_err());
181 }
182}