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() {
81 return Err(bad(expr)); }
83 let modifiers = MODIFIERS
85 .iter()
86 .filter(|m| mods.iter().any(|x| x == *m))
87 .map(|m| m.to_string())
88 .collect();
89 Ok(KeySpec { modifiers, keys })
90}
91
92fn bad(expr: &str) -> CtlError {
93 CtlError::new(
94 ErrorCode::Protocol,
95 format!("invalid key expression {expr:?}: use e.g. ctrl+s, ctrl+shift+t, enter, f5"),
96 )
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn parses_common_combos() {
105 let spec = parse_key_expr("ctrl+s").unwrap();
106 assert_eq!(spec.modifiers, vec!["ctrl"]);
107 assert_eq!(spec.keys, vec![Key::Char('s')]);
108
109 let spec = parse_key_expr("ctrl+shift+t").unwrap();
110 assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
111
112 let spec = parse_key_expr("alt+F4").unwrap();
113 assert_eq!(spec.keys, vec![Key::Named("f4".into())]);
114
115 assert_eq!(
116 parse_key_expr("pgdn").unwrap().keys,
117 vec![Key::Named("pgdn".into())]
118 );
119 assert_eq!(parse_key_expr("a").unwrap().keys, vec![Key::Char('a')]);
120 }
121
122 #[test]
123 fn modifier_order_is_normalized_and_deduped() {
124 let spec = parse_key_expr("shift+ctrl+a").unwrap();
125 assert_eq!(spec.modifiers, vec!["ctrl", "shift"]);
126 let spec = parse_key_expr("ctrl+ctrl+a").unwrap();
127 assert_eq!(spec.modifiers, vec!["ctrl"]);
128 }
129
130 #[test]
131 fn aliases_normalize() {
132 assert_eq!(
133 parse_key_expr("delete").unwrap().keys,
134 vec![Key::Named("del".into())]
135 );
136 assert_eq!(
137 parse_key_expr("back").unwrap().keys,
138 vec![Key::Named("backspace".into())]
139 );
140 }
141
142 #[test]
143 fn rejects_garbage() {
144 for bad in [
146 "", "ctrl", "foo+bar", "ctrl+", "+a", "notakey", "f0", "f13", "f99",
147 ] {
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}