1use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
8pub enum Key {
9 Char(char),
11 Enter,
13 Esc,
15 Tab,
17 Space,
19 Backspace,
21 Delete,
23 Insert,
25 Home,
27 End,
29 PageUp,
31 PageDown,
33 Up,
35 Down,
37 Left,
39 Right,
41 F(u8),
43 Menu,
45}
46
47const NAMED: [(&str, Key); 16] = [
48 ("enter", Key::Enter),
49 ("esc", Key::Esc),
50 ("tab", Key::Tab),
51 ("space", Key::Space),
52 ("backspace", Key::Backspace),
53 ("delete", Key::Delete),
54 ("insert", Key::Insert),
55 ("home", Key::Home),
56 ("end", Key::End),
57 ("pgup", Key::PageUp),
58 ("pgdn", Key::PageDown),
59 ("up", Key::Up),
60 ("down", Key::Down),
61 ("left", Key::Left),
62 ("right", Key::Right),
63 ("menu", Key::Menu),
64];
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
68pub struct Modifiers {
69 pub ctrl: bool,
71 pub alt: bool,
73 pub shift: bool,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
81pub struct KeyChord {
82 pub key: Key,
84 pub mods: Modifiers,
86}
87
88impl KeyChord {
89 #[must_use]
91 pub fn plain(key: Key) -> Self {
92 Self { key, mods: Modifiers::default() }
93 }
94
95 #[must_use]
97 pub fn label(&self) -> String {
98 self.parts().join(" ")
99 }
100
101 fn parts(&self) -> Vec<String> {
102 let mut parts = Vec::new();
103 if self.mods.ctrl {
104 parts.push("ctrl".to_owned());
105 }
106 if self.mods.alt {
107 parts.push("alt".to_owned());
108 }
109 if self.mods.shift {
110 parts.push("shift".to_owned());
111 }
112 parts.push(match self.key {
113 Key::Char(c) => c.to_string(),
114 Key::F(n) => format!("f{n}"),
115 other => {
116 NAMED.iter().find(|(_, key)| *key == other).map(|(name, _)| (*name).to_owned()).unwrap_or_default()
117 }
118 });
119 parts
120 }
121}
122
123impl FromStr for KeyChord {
124 type Err = String;
125
126 fn from_str(text: &str) -> Result<Self, Self::Err> {
130 let trimmed = text.trim();
131 if trimmed.is_empty() {
132 return Err("empty key binding".to_owned());
133 }
134 let (modifier_part, key_part) = if trimmed == "+" {
135 ("", "+")
136 } else if let Some(prefix) = trimmed.strip_suffix("++") {
137 (prefix, "+")
138 } else {
139 match trimmed.rsplit_once('+') {
140 Some((mods, key)) => (mods, key),
141 None => ("", trimmed),
142 }
143 };
144 let mut mods = Modifiers::default();
145 for modifier in modifier_part.split('+').filter(|m| !m.is_empty()) {
146 match modifier.to_lowercase().as_str() {
147 "ctrl" | "control" => mods.ctrl = true,
148 "alt" | "option" => mods.alt = true,
149 "shift" => mods.shift = true,
150 other => {
151 return Err(format!("unknown modifier `{other}` in `{text}`; use ctrl, alt or shift"));
152 }
153 }
154 }
155 let key = parse_key(key_part, &mut mods).ok_or_else(|| {
156 format!("unknown key `{key_part}` in `{text}`; use a character, f1–f24 or a key name such as enter, esc, tab, space, up")
157 })?;
158 Ok(Self { key, mods })
159 }
160}
161
162fn parse_key(text: &str, mods: &mut Modifiers) -> Option<Key> {
165 let mut chars = text.chars();
166 if let (Some(c), None) = (chars.next(), chars.next()) {
167 if c.is_whitespace() || c.is_control() {
168 return None;
169 }
170 if c.is_uppercase() {
171 mods.shift = true;
172 return Some(Key::Char(c.to_lowercase().next().unwrap_or(c)));
173 }
174 return Some(Key::Char(c));
175 }
176 let name = text.to_lowercase();
177 if let Some((_, key)) = NAMED.iter().find(|(n, _)| *n == name) {
178 return Some(*key);
179 }
180 let number = name.strip_prefix('f')?.parse::<u8>().ok()?;
181 (1..=24).contains(&number).then_some(Key::F(number))
182}
183
184impl fmt::Display for KeyChord {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 f.write_str(&self.parts().join("+"))
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn chord(text: &str) -> KeyChord {
196 text.parse().expect("valid chord")
197 }
198
199 #[test]
200 fn parses_modifiers_and_keys() {
201 let c = chord("Ctrl+Shift+P");
202 assert_eq!(c.key, Key::Char('p'));
203 assert!(c.mods.ctrl && c.mods.shift && !c.mods.alt);
204 assert_eq!(chord("?"), KeyChord::plain(Key::Char('?')));
205 assert_eq!(chord("f12"), KeyChord::plain(Key::F(12)));
206 assert_eq!(chord("shift+tab").key, Key::Tab);
207 assert_eq!(chord("+"), KeyChord::plain(Key::Char('+')));
208 assert_eq!(chord("ctrl++").key, Key::Char('+'));
209 }
210
211 #[test]
212 fn an_uppercase_letter_means_shift_plus_that_letter() {
213 assert_eq!(chord("S"), chord("shift+s"));
214 assert_eq!(chord("ctrl+S"), chord("ctrl+shift+s"));
215 assert_eq!(chord("shift+S"), chord("shift+s"));
216 assert_eq!(chord("s"), KeyChord::plain(Key::Char('s')));
217 assert_eq!(chord("Ş"), chord("shift+ş"));
218 assert_eq!(chord("F12"), KeyChord::plain(Key::F(12)), "key names stay case-insensitive");
219 assert_eq!(chord("Ctrl+Enter"), chord("ctrl+enter"));
220 assert_eq!(chord("?"), KeyChord::plain(Key::Char('?')), "symbols carry no shift");
221 assert_eq!(chord("S").to_string(), "shift+s");
222 }
223
224 #[test]
225 fn rejects_unknown_parts() {
226 assert!("hyper+x".parse::<KeyChord>().is_err());
227 assert!("ctrl+banana".parse::<KeyChord>().is_err());
228 assert!("f25".parse::<KeyChord>().is_err());
229 assert!("".parse::<KeyChord>().is_err());
230 }
231
232 #[test]
233 fn formats_for_files_and_hint_bars() {
234 let c = chord("shift+ctrl+pgup");
235 assert_eq!(c.to_string(), "ctrl+shift+pgup");
236 assert_eq!(c.label(), "ctrl shift pgup");
237 assert_eq!(chord("f12").label(), "f12");
238 }
239}