Krolyxon
Commit 4654f72

Branches

Tags

Refs →

remagesrcinputkeys.rs

437 lines · 12.8 KB · Rust

1//! Vim key notation: `n`, `G`, `<C-x>`, `<S-Tab>`, `<Space>w`, `gg`, mouse buttons and wheel.
2//!
3//! Keys are normalised so that the config and the keyboard agree: a character already carries
4//! its shift state (`G`, `?`), so shift is dropped from character keys and `<S-g>` means `G`.
5
6use std::fmt;
7use std::str::FromStr;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub struct Mods {
11 pub ctrl: bool,
12 pub alt: bool,
13 pub shift: bool,
14 /// The logo key, `<D-…>` as in vim.
15 pub sup: bool,
16}
17
18impl Mods {
19 pub const NONE: Mods = Mods {
20 ctrl: false,
21 alt: false,
22 shift: false,
23 sup: false,
24 };
25
26 pub fn is_empty(self) -> bool {
27 self == Mods::NONE
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub enum Named {
33 Space,
34 Enter,
35 Esc,
36 Tab,
37 Backspace,
38 Delete,
39 Insert,
40 Home,
41 End,
42 PageUp,
43 PageDown,
44 Up,
45 Down,
46 Left,
47 Right,
48 F(u8),
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub enum Button {
53 Left,
54 Right,
55 Middle,
56 Back,
57 Forward,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
61pub enum Scroll {
62 Up,
63 Down,
64 Left,
65 Right,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
69pub enum Code {
70 Char(char),
71 Named(Named),
72 Mouse(Button),
73 Scroll(Scroll),
74}
75
76/// One key press (or click, or wheel notch) with its modifiers.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
78pub struct Key {
79 pub mods: Mods,
80 pub code: Code,
81}
82
83impl Key {
84 pub fn new(mods: Mods, code: Code) -> Self {
85 Self { mods, code }.normalized()
86 }
87
88 pub fn plain(c: char) -> Self {
89 Self::new(Mods::NONE, Code::Char(c))
90 }
91
92 /// Characters carry their own shift state; a space is `<Space>`.
93 fn normalized(mut self) -> Self {
94 if let Code::Char(c) = self.code {
95 if c == ' ' {
96 self.code = Code::Named(Named::Space);
97 } else if self.mods.shift {
98 self.code = Code::Char(c.to_uppercase().next().unwrap_or(c));
99 self.mods.shift = false;
100 }
101 }
102 self
103 }
104
105 /// A digit typed without modifiers, which may start or extend a count.
106 pub fn digit(self) -> Option<u32> {
107 match self.code {
108 Code::Char(c) if self.mods.is_empty() => c.to_digit(10),
109 _ => None,
110 }
111 }
112}
113
114const NAMES: &[(&str, Code)] = &[
115 ("Space", Code::Named(Named::Space)),
116 ("CR", Code::Named(Named::Enter)),
117 ("Enter", Code::Named(Named::Enter)),
118 ("Return", Code::Named(Named::Enter)),
119 ("Esc", Code::Named(Named::Esc)),
120 ("Escape", Code::Named(Named::Esc)),
121 ("Tab", Code::Named(Named::Tab)),
122 ("BS", Code::Named(Named::Backspace)),
123 ("Backspace", Code::Named(Named::Backspace)),
124 ("Del", Code::Named(Named::Delete)),
125 ("Delete", Code::Named(Named::Delete)),
126 ("Insert", Code::Named(Named::Insert)),
127 ("Home", Code::Named(Named::Home)),
128 ("End", Code::Named(Named::End)),
129 ("PageUp", Code::Named(Named::PageUp)),
130 ("PageDown", Code::Named(Named::PageDown)),
131 ("Up", Code::Named(Named::Up)),
132 ("Down", Code::Named(Named::Down)),
133 ("Left", Code::Named(Named::Left)),
134 ("Right", Code::Named(Named::Right)),
135 ("lt", Code::Char('<')),
136 ("gt", Code::Char('>')),
137 ("Bar", Code::Char('|')),
138 ("Bslash", Code::Char('\\')),
139 ("LeftMouse", Code::Mouse(Button::Left)),
140 ("RightMouse", Code::Mouse(Button::Right)),
141 ("MiddleMouse", Code::Mouse(Button::Middle)),
142 ("X1Mouse", Code::Mouse(Button::Back)),
143 ("X2Mouse", Code::Mouse(Button::Forward)),
144 ("ScrollUp", Code::Scroll(Scroll::Up)),
145 ("ScrollDown", Code::Scroll(Scroll::Down)),
146 ("ScrollLeft", Code::Scroll(Scroll::Left)),
147 ("ScrollRight", Code::Scroll(Scroll::Right)),
148 ("ScrollWheelUp", Code::Scroll(Scroll::Up)),
149 ("ScrollWheelDown", Code::Scroll(Scroll::Down)),
150 ("ScrollWheelLeft", Code::Scroll(Scroll::Left)),
151 ("ScrollWheelRight", Code::Scroll(Scroll::Right)),
152];
153
154fn lookup(name: &str) -> Option<Code> {
155 if let Some(n) = name.strip_prefix(['F', 'f'])
156 && let Ok(n) = n.parse::<u8>()
157 && (1..=35).contains(&n)
158 {
159 return Some(Code::Named(Named::F(n)));
160 }
161 NAMES
162 .iter()
163 .find(|(n, _)| n.eq_ignore_ascii_case(name))
164 .map(|(_, c)| *c)
165}
166
167fn code_name(code: Code) -> Option<String> {
168 let s = match code {
169 Code::Char('<') => "lt",
170 Code::Char(_) => return None,
171 Code::Named(Named::F(n)) => return Some(format!("F{n}")),
172 Code::Named(Named::Space) => "Space",
173 Code::Named(Named::Enter) => "CR",
174 Code::Named(Named::Esc) => "Esc",
175 Code::Named(Named::Tab) => "Tab",
176 Code::Named(Named::Backspace) => "BS",
177 Code::Named(Named::Delete) => "Del",
178 Code::Named(Named::Insert) => "Insert",
179 Code::Named(Named::Home) => "Home",
180 Code::Named(Named::End) => "End",
181 Code::Named(Named::PageUp) => "PageUp",
182 Code::Named(Named::PageDown) => "PageDown",
183 Code::Named(Named::Up) => "Up",
184 Code::Named(Named::Down) => "Down",
185 Code::Named(Named::Left) => "Left",
186 Code::Named(Named::Right) => "Right",
187 Code::Mouse(Button::Left) => "LeftMouse",
188 Code::Mouse(Button::Right) => "RightMouse",
189 Code::Mouse(Button::Middle) => "MiddleMouse",
190 Code::Mouse(Button::Back) => "X1Mouse",
191 Code::Mouse(Button::Forward) => "X2Mouse",
192 Code::Scroll(Scroll::Up) => "ScrollUp",
193 Code::Scroll(Scroll::Down) => "ScrollDown",
194 Code::Scroll(Scroll::Left) => "ScrollLeft",
195 Code::Scroll(Scroll::Right) => "ScrollRight",
196 };
197 Some(s.into())
198}
199
200impl fmt::Display for Key {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 let m = self.mods;
203 let name = code_name(self.code);
204 if m.is_empty()
205 && let Code::Char(c) = self.code
206 && name.is_none()
207 {
208 return write!(f, "{c}");
209 }
210 f.write_str("<")?;
211 for (on, p) in [
212 (m.ctrl, "C-"),
213 (m.alt, "A-"),
214 (m.shift, "S-"),
215 (m.sup, "D-"),
216 ] {
217 if on {
218 f.write_str(p)?;
219 }
220 }
221 match (name, self.code) {
222 (Some(n), _) => f.write_str(&n)?,
223 (None, Code::Char(c)) => write!(f, "{c}")?,
224 (None, _) => unreachable!("every non-char code has a name"),
225 }
226 f.write_str(">")
227 }
228}
229
230/// Parse a `<…>` token at the start of `s`. Returns the key and the bytes consumed, or `None`
231/// when `s` doesn't start with a well-formed token (a lone `<` is then a literal).
232fn parse_bracket(s: &str) -> Result<Option<(Key, usize)>, String> {
233 let Some(inner) = s.strip_prefix('<') else {
234 return Ok(None);
235 };
236 let mut mods = Mods::NONE;
237 let mut r = inner;
238 loop {
239 let mut it = r.chars();
240 let (Some(m), Some('-'), Some(_)) = (it.next(), it.next(), it.next()) else {
241 break;
242 };
243 match m.to_ascii_uppercase() {
244 'C' => mods.ctrl = true,
245 'A' | 'M' => mods.alt = true,
246 'S' => mods.shift = true,
247 'D' | 'T' => mods.sup = true,
248 _ => break,
249 }
250 r = &r[2..];
251 }
252 let mut it = r.chars();
253 let Some(first) = it.next() else {
254 return Ok(None);
255 };
256 // A single character: `<C-x>`, `<C-->`.
257 if it.next() == Some('>') {
258 if mods.shift && !first.is_alphabetic() {
259 return Err(format!(
260 "`<S-{first}>`: shift only combines with letters and named keys; \
261 write the shifted character instead"
262 ));
263 }
264 let used = 1 + (inner.len() - r.len()) + first.len_utf8() + 1;
265 return Ok(Some((Key::new(mods, Code::Char(first)), used)));
266 }
267 let Some(end) = r.find('>') else {
268 return Ok(None);
269 };
270 let name = &r[..end];
271 if !name.chars().all(|c| c.is_ascii_alphanumeric()) {
272 return Ok(None);
273 }
274 let used = 1 + (inner.len() - r.len()) + end + 1;
275 match lookup(name) {
276 Some(code) => {
277 if mods.shift && matches!(code, Code::Char(c) if !c.is_alphabetic()) {
278 return Err(format!(
279 "`<S-{name}>`: shift only combines with letters and named keys"
280 ));
281 }
282 Ok(Some((Key::new(mods, code), used)))
283 }
284 None if mods.is_empty() && !inner.starts_with(char::is_alphabetic) => Ok(None),
285 None => Err(format!("unknown key `<{}>`", &inner[..used - 2])),
286 }
287}
288
289/// A key sequence such as `gg` or `<Space>w`.
290#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
291pub struct KeySeq(pub Vec<Key>);
292
293impl FromStr for KeySeq {
294 type Err = String;
295
296 fn from_str(s: &str) -> Result<Self, String> {
297 let mut keys = Vec::new();
298 let mut rest = s;
299 while let Some(c) = rest.chars().next() {
300 if c == '<'
301 && let Some((key, used)) = parse_bracket(rest)?
302 {
303 keys.push(key);
304 rest = &rest[used..];
305 continue;
306 }
307 if c.is_control() {
308 return Err(format!("control character in key `{}`", s.escape_debug()));
309 }
310 keys.push(Key::plain(c));
311 rest = &rest[c.len_utf8()..];
312 }
313 if keys.is_empty() {
314 return Err("empty key".into());
315 }
316 Ok(KeySeq(keys))
317 }
318}
319
320impl fmt::Display for KeySeq {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 self.0.iter().try_for_each(|k| write!(f, "{k}"))
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn seq(s: &str) -> Vec<Key> {
331 s.parse::<KeySeq>().unwrap().0
332 }
333
334 fn ctrl() -> Mods {
335 Mods {
336 ctrl: true,
337 ..Mods::NONE
338 }
339 }
340
341 #[test]
342 fn plain_characters_and_sequences() {
343 assert_eq!(seq("n"), [Key::plain('n')]);
344 assert_eq!(seq("gg"), [Key::plain('g'), Key::plain('g')]);
345 assert_eq!(seq("G"), [Key::plain('G')]);
346 assert_eq!(seq("<"), [Key::plain('<')]);
347 assert_eq!(seq("<>"), [Key::plain('<'), Key::plain('>')]);
348 assert_eq!(seq("|"), [Key::plain('|')]);
349 assert_eq!(seq(" "), [Key::new(Mods::NONE, Code::Named(Named::Space))]);
350 }
351
352 #[test]
353 fn bracket_notation() {
354 assert_eq!(seq("<C-x>"), [Key::new(ctrl(), Code::Char('x'))]);
355 assert_eq!(seq("<c-X>"), [Key::new(ctrl(), Code::Char('X'))]);
356 assert_eq!(seq("<C-->"), [Key::new(ctrl(), Code::Char('-'))]);
357 let shift = Mods {
358 shift: true,
359 ..Mods::NONE
360 };
361 assert_eq!(seq("<S-Tab>"), [Key::new(shift, Code::Named(Named::Tab))]);
362 assert_eq!(seq("<S-g>"), [Key::plain('G')]);
363 assert_eq!(seq("<cr>"), seq("<Enter>"));
364 assert_eq!(seq("<lt>"), [Key::plain('<')]);
365 assert_eq!(
366 seq("<F5>"),
367 [Key::new(Mods::NONE, Code::Named(Named::F(5)))]
368 );
369 assert_eq!(
370 seq("<Space>w"),
371 [
372 Key::new(Mods::NONE, Code::Named(Named::Space)),
373 Key::plain('w')
374 ]
375 );
376 assert_eq!(
377 seq("<C-ScrollUp>"),
378 [Key::new(ctrl(), Code::Scroll(Scroll::Up))]
379 );
380 assert_eq!(
381 seq("<LeftMouse>"),
382 [Key::new(Mods::NONE, Code::Mouse(Button::Left))]
383 );
384 assert_eq!(
385 seq("<C-S-A-Up>"),
386 [Key::new(
387 Mods {
388 ctrl: true,
389 alt: true,
390 shift: true,
391 sup: false
392 },
393 Code::Named(Named::Up)
394 )]
395 );
396 }
397
398 #[test]
399 fn errors() {
400 assert!("".parse::<KeySeq>().is_err());
401 assert!("<C-Bogus>".parse::<KeySeq>().is_err());
402 assert!("<Bogus>".parse::<KeySeq>().is_err());
403 assert!("<S-1>".parse::<KeySeq>().is_err());
404 }
405
406 #[test]
407 fn display_round_trips() {
408 for s in [
409 "n",
410 "gg",
411 "<C-x>",
412 "<S-Tab>",
413 "<Space>w",
414 "<lt>",
415 "<C-Space>",
416 "<C-ScrollUp>",
417 "<F12>",
418 "<CR>",
419 "|",
420 "<C-->",
421 "<A-D-x>",
422 ] {
423 let k: KeySeq = s.parse().unwrap();
424 assert_eq!(k.to_string(), s);
425 assert_eq!(k.to_string().parse::<KeySeq>().unwrap(), k);
426 }
427 assert_eq!("<S-a>".parse::<KeySeq>().unwrap().to_string(), "A");
428 assert_eq!("<escape>".parse::<KeySeq>().unwrap().to_string(), "<Esc>");
429 }
430
431 #[test]
432 fn digits() {
433 assert_eq!(Key::plain('5').digit(), Some(5));
434 assert_eq!(Key::new(ctrl(), Code::Char('5')).digit(), None);
435 assert_eq!(Key::plain('x').digit(), None);
436 }
437}