1use core::fmt;
20use core::str::FromStr;
21
22use super::{Key, KeyInput, Modifiers};
23
24impl fmt::Display for KeyInput {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 let mods = self.modifiers();
27 if mods.contains(Modifiers::CTRL) {
28 f.write_str("ctrl+")?;
29 }
30 if mods.contains(Modifiers::ALT) {
31 f.write_str("alt+")?;
32 }
33 if mods.contains(Modifiers::SHIFT) {
34 f.write_str("shift+")?;
35 }
36 if mods.contains(Modifiers::SUPER) {
37 f.write_str("super+")?;
38 }
39
40 match self.key() {
43 Key::Char(c) => write!(f, "{c}"),
44 Key::F(n) => write!(f, "f{n}"),
45 Key::Enter => f.write_str("enter"),
46 Key::Esc => f.write_str("esc"),
47 Key::Tab => f.write_str("tab"),
48 Key::Backspace => f.write_str("backspace"),
49 Key::Delete => f.write_str("delete"),
50 Key::Insert => f.write_str("insert"),
51 Key::Home => f.write_str("home"),
52 Key::End => f.write_str("end"),
53 Key::PageUp => f.write_str("pageup"),
54 Key::PageDown => f.write_str("pagedown"),
55 Key::Up => f.write_str("up"),
56 Key::Down => f.write_str("down"),
57 Key::Left => f.write_str("left"),
58 Key::Right => f.write_str("right"),
59 }
60 }
61}
62
63impl FromStr for KeyInput {
64 type Err = ParseKeyInputError;
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 if s.is_empty() {
68 return Err(ParseKeyInputError::new(s, ErrorKind::Empty));
69 }
70
71 let mut rest = s;
74 let mut mods = Modifiers::NONE;
75 while let Some(idx) = rest.find('+') {
76 let token = &rest[..idx];
77 if let Some(m) = parse_modifier(token) {
78 mods |= m;
79 rest = &rest[idx + 1..];
80 } else {
81 break;
82 }
83 }
84
85 let key =
86 parse_key(rest).ok_or_else(|| ParseKeyInputError::new(s, ErrorKind::InvalidKey))?;
87 Ok(KeyInput::normalized(key, mods))
88 }
89}
90
91fn parse_modifier(token: &str) -> Option<Modifiers> {
92 match token.to_ascii_lowercase().as_str() {
93 "ctrl" | "control" => Some(Modifiers::CTRL),
94 "alt" | "opt" | "option" => Some(Modifiers::ALT),
95 "shift" => Some(Modifiers::SHIFT),
96 "cmd" | "super" | "win" | "meta" => Some(Modifiers::SUPER),
97 _ => None,
98 }
99}
100
101fn parse_key(token: &str) -> Option<Key> {
102 if token.is_empty() {
103 return None;
104 }
105 let lower = token.to_ascii_lowercase();
106 let named = match lower.as_str() {
107 "tab" => Some(Key::Tab),
108 "enter" | "return" => Some(Key::Enter),
109 "esc" | "escape" => Some(Key::Esc),
110 "space" => Some(Key::Char(' ')),
111 "backspace" => Some(Key::Backspace),
112 "delete" | "del" => Some(Key::Delete),
113 "insert" | "ins" => Some(Key::Insert),
114 "home" => Some(Key::Home),
115 "end" => Some(Key::End),
116 "pageup" | "pgup" => Some(Key::PageUp),
117 "pagedown" | "pgdn" => Some(Key::PageDown),
118 "up" => Some(Key::Up),
119 "down" => Some(Key::Down),
120 "left" => Some(Key::Left),
121 "right" => Some(Key::Right),
122 _ => None,
123 };
124 if let Some(key) = named {
125 return Some(key);
126 }
127
128 if let Some(rest) = lower.strip_prefix('f') {
129 if let Ok(n) = rest.parse::<u8>() {
130 if (1..=24).contains(&n) {
131 return Some(Key::F(n));
132 }
133 }
134 }
135
136 let mut chars = token.chars();
138 let c = chars.next()?;
139 if chars.next().is_some() {
140 return None;
141 }
142 Some(Key::Char(c))
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ParseKeyInputError {
148 input: String,
149 kind: ErrorKind,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153enum ErrorKind {
154 Empty,
155 InvalidKey,
156}
157
158impl ParseKeyInputError {
159 fn new(input: &str, kind: ErrorKind) -> Self {
160 ParseKeyInputError {
161 input: input.to_string(),
162 kind,
163 }
164 }
165
166 #[must_use]
168 pub fn input(&self) -> &str {
169 &self.input
170 }
171}
172
173impl fmt::Display for ParseKeyInputError {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self.kind {
176 ErrorKind::Empty => f.write_str("empty key string"),
177 ErrorKind::InvalidKey => write!(f, "invalid key string: {:?}", self.input),
178 }
179 }
180}
181
182impl std::error::Error for ParseKeyInputError {}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn display_round_trips_through_from_str() {
190 let cases = [
195 KeyInput::new(Key::Char('a'), Modifiers::NONE),
196 KeyInput::new(Key::Char('A'), Modifiers::NONE),
197 KeyInput::new(Key::Char('1'), Modifiers::SUPER),
198 KeyInput::new(Key::Char('s'), Modifiers::CTRL | Modifiers::SHIFT),
199 KeyInput::new(Key::Char('あ'), Modifiers::CTRL),
200 KeyInput::new(Key::Char(' '), Modifiers::NONE),
201 KeyInput::new(Key::Char('+'), Modifiers::CTRL),
202 KeyInput::new(Key::F(1), Modifiers::NONE),
203 KeyInput::new(Key::F(12), Modifiers::SHIFT),
204 KeyInput::new(Key::Tab, Modifiers::SHIFT),
205 KeyInput::new(Key::Esc, Modifiers::NONE),
206 KeyInput::new(Key::Up, Modifiers::CTRL | Modifiers::ALT),
207 KeyInput::new(
208 Key::Enter,
209 Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT | Modifiers::SUPER,
210 ),
211 ];
212 for k in cases {
213 let rendered = k.to_string();
214 assert_eq!(
215 rendered.parse::<KeyInput>(),
216 Ok(k),
217 "round trip via {rendered:?}"
218 );
219 }
220 }
221
222 #[test]
223 fn display_uses_canonical_modifier_order_and_names() {
224 let k = KeyInput::new(Key::Char('a'), Modifiers::SUPER | Modifiers::CTRL);
225 assert_eq!(k.to_string(), "ctrl+super+a");
226 assert_eq!(KeyInput::new(Key::F(1), Modifiers::NONE).to_string(), "f1");
227 assert_eq!(
228 KeyInput::new(Key::Tab, Modifiers::SHIFT).to_string(),
229 "shift+tab"
230 );
231 }
232
233 #[test]
234 fn from_str_accepts_aliases_case_insensitively() {
235 let ctrl_a = KeyInput::new(Key::Char('a'), Modifiers::CTRL);
236 assert_eq!("ctrl+a".parse::<KeyInput>().unwrap(), ctrl_a);
237 assert_eq!("CTRL+a".parse::<KeyInput>().unwrap(), ctrl_a);
238 assert_eq!("control+a".parse::<KeyInput>().unwrap(), ctrl_a);
239 assert_eq!(
240 "cmd+1".parse::<KeyInput>().unwrap(),
241 "super+1".parse::<KeyInput>().unwrap()
242 );
243 }
244
245 #[test]
246 fn from_str_shares_shift_normalization() {
247 let a = KeyInput::new(Key::Char('a'), Modifiers::NONE);
249 assert_eq!("shift+a".parse::<KeyInput>().unwrap(), a);
250 assert_eq!("a".parse::<KeyInput>().unwrap(), a);
251 }
252
253 #[test]
254 fn from_str_keeps_shift_with_other_modifier() {
255 let cmd_shift_s = "cmd+shift+s".parse::<KeyInput>().unwrap();
258 assert_eq!(
259 cmd_shift_s,
260 KeyInput::new(Key::Char('s'), Modifiers::SUPER | Modifiers::SHIFT)
261 );
262 assert_ne!(cmd_shift_s, "cmd+s".parse::<KeyInput>().unwrap());
263 assert_ne!(
264 "ctrl+shift+s".parse::<KeyInput>().unwrap(),
265 "ctrl+s".parse::<KeyInput>().unwrap()
266 );
267 }
268
269 #[test]
270 fn from_str_handles_literal_plus_as_key() {
271 assert_eq!(
272 "ctrl++".parse::<KeyInput>().unwrap(),
273 KeyInput::new(Key::Char('+'), Modifiers::CTRL)
274 );
275 assert_eq!(
276 "+".parse::<KeyInput>().unwrap(),
277 KeyInput::new(Key::Char('+'), Modifiers::NONE)
278 );
279 }
280
281 #[test]
282 fn from_str_parses_named_and_function_keys() {
283 assert_eq!("f1".parse::<KeyInput>().unwrap().key(), Key::F(1));
284 assert_eq!("up".parse::<KeyInput>().unwrap().key(), Key::Up);
285 assert_eq!("esc".parse::<KeyInput>().unwrap().key(), Key::Esc);
286 assert_eq!("escape".parse::<KeyInput>().unwrap().key(), Key::Esc);
287 }
288
289 #[test]
290 fn from_str_rejects_invalid_input() {
291 assert!("".parse::<KeyInput>().is_err());
292 assert!("ctrl".parse::<KeyInput>().is_err());
293 assert!("hyper+a".parse::<KeyInput>().is_err());
294 assert!("ctrl+xyz".parse::<KeyInput>().is_err());
295 }
296}