1use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
10pub enum Key {
11 Char(char),
12 Enter,
13 Esc,
14 Tab,
15 BackTab,
16 Backspace,
17 Delete,
18 Up,
19 Down,
20 Left,
21 Right,
22 Home,
23 End,
24 PageUp,
25 PageDown,
26 F(u8),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
30pub struct Mods {
31 pub ctrl: bool,
32 pub alt: bool,
33 pub shift: bool,
34}
35
36impl Mods {
37 pub const NONE: Self = Self {
38 ctrl: false,
39 alt: false,
40 shift: false,
41 };
42 pub const CTRL: Self = Self {
43 ctrl: true,
44 alt: false,
45 shift: false,
46 };
47
48 pub fn is_none(self) -> bool {
49 self == Self::NONE
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
54pub struct Chord {
55 pub key: Key,
56 pub mods: Mods,
57}
58
59impl Chord {
60 pub const fn plain(key: Key) -> Self {
61 Self {
62 key,
63 mods: Mods::NONE,
64 }
65 }
66 pub const fn ch(c: char) -> Self {
67 Self::plain(Key::Char(c))
68 }
69 pub const fn ctrl(c: char) -> Self {
70 Self {
71 key: Key::Char(c),
72 mods: Mods::CTRL,
73 }
74 }
75
76 pub fn as_insertable(self) -> Option<char> {
78 match self.key {
79 Key::Char(c) if self.mods.is_none() => Some(c),
80 _ => None,
81 }
82 }
83}
84
85#[derive(Debug, thiserror::Error, PartialEq, Eq)]
86pub enum KeyParseError {
87 #[error("unterminated `<` in key sequence `{0}`")]
88 Unterminated(String),
89 #[error("unknown key name `<{0}>`")]
90 UnknownKey(String),
91 #[error("empty key sequence")]
92 Empty,
93}
94
95fn key_name(k: Key) -> String {
96 match k {
97 Key::Enter => "CR".into(),
98 Key::Esc => "Esc".into(),
99 Key::Tab => "Tab".into(),
100 Key::BackTab => "S-Tab".into(),
101 Key::Backspace => "BS".into(),
102 Key::Delete => "Del".into(),
103 Key::Up => "Up".into(),
104 Key::Down => "Down".into(),
105 Key::Left => "Left".into(),
106 Key::Right => "Right".into(),
107 Key::Home => "Home".into(),
108 Key::End => "End".into(),
109 Key::PageUp => "PageUp".into(),
110 Key::PageDown => "PageDown".into(),
111 Key::F(n) => format!("F{n}"),
112 Key::Char(' ') => "Space".into(),
113 Key::Char(c) => c.to_string(),
114 }
115}
116
117fn named_key(name: &str) -> Option<Key> {
118 Some(match name {
119 "cr" | "enter" | "return" => Key::Enter,
120 "esc" | "escape" => Key::Esc,
121 "tab" => Key::Tab,
122 "s-tab" | "btab" => Key::BackTab,
123 "bs" | "backspace" => Key::Backspace,
124 "del" | "delete" => Key::Delete,
125 "space" => Key::Char(' '),
126 "up" => Key::Up,
127 "down" => Key::Down,
128 "left" => Key::Left,
129 "right" => Key::Right,
130 "home" => Key::Home,
131 "end" => Key::End,
132 "pageup" => Key::PageUp,
133 "pagedown" => Key::PageDown,
134 _ => {
135 let n = name.strip_prefix('f')?.parse::<u8>().ok()?;
136 if (1..=12).contains(&n) {
137 Key::F(n)
138 } else {
139 return None;
140 }
141 }
142 })
143}
144
145impl fmt::Display for Chord {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 let bare = matches!(self.key, Key::Char(c) if c != ' ');
148 if self.mods.is_none() && bare {
149 return write!(f, "{}", key_name(self.key));
150 }
151 let mut prefix = String::new();
152 if self.mods.ctrl {
153 prefix.push_str("C-");
154 }
155 if self.mods.alt {
156 prefix.push_str("A-");
157 }
158 if self.mods.shift && self.key != Key::BackTab {
160 prefix.push_str("S-");
161 }
162 write!(f, "<{prefix}{}>", key_name(self.key))
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
168pub struct ChordSeq(pub Vec<Chord>);
169
170impl ChordSeq {
171 pub fn parse(s: &str, leader: Chord) -> Result<Self, KeyParseError> {
174 let mut out = Vec::new();
175 let chars: Vec<char> = s.chars().collect();
176 let mut i = 0;
177
178 while i < chars.len() {
179 if chars[i] != '<' {
180 out.push(Chord::ch(chars[i]));
181 i += 1;
182 continue;
183 }
184 let close = chars[i..]
185 .iter()
186 .position(|&c| c == '>')
187 .ok_or_else(|| KeyParseError::Unterminated(s.to_string()))?
188 + i;
189 let token: String = chars[i + 1..close].iter().collect();
190 out.push(parse_token(&token, leader)?);
191 i = close + 1;
192 }
193
194 if out.is_empty() {
195 return Err(KeyParseError::Empty);
196 }
197 Ok(Self(out))
198 }
199
200 pub fn len(&self) -> usize {
201 self.0.len()
202 }
203 pub fn is_empty(&self) -> bool {
204 self.0.is_empty()
205 }
206 pub fn starts_with(&self, prefix: &[Chord]) -> bool {
207 self.0.starts_with(prefix)
208 }
209}
210
211fn parse_token(token: &str, leader: Chord) -> Result<Chord, KeyParseError> {
212 let lower = token.to_ascii_lowercase();
213 if lower == "leader" {
214 return Ok(leader);
215 }
216 if let Some(k) = named_key(&lower) {
217 return Ok(Chord::plain(k));
218 }
219
220 let mut mods = Mods::NONE;
222 let mut rest = token;
223 loop {
224 let head = rest.get(..2).map(str::to_ascii_lowercase);
225 match head.as_deref() {
226 Some("c-") => mods.ctrl = true,
227 Some("a-") | Some("m-") => mods.alt = true,
228 Some("s-") => mods.shift = true,
229 _ => break,
230 }
231 rest = &rest[2..];
232 }
233 if mods.is_none() {
234 return Err(KeyParseError::UnknownKey(token.to_string()));
235 }
236
237 let lower_rest = rest.to_ascii_lowercase();
238 if mods.shift && lower_rest == "tab" {
240 return Ok(Chord::plain(Key::BackTab));
241 }
242 if let Some(k) = named_key(&lower_rest) {
243 return Ok(Chord { key: k, mods });
244 }
245 let mut it = rest.chars();
246 match (it.next(), it.next()) {
247 (Some(c), None) => Ok(Chord {
248 key: Key::Char(c),
249 mods,
250 }),
251 _ => Err(KeyParseError::UnknownKey(token.to_string())),
252 }
253}
254
255impl fmt::Display for ChordSeq {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 for c in &self.0 {
258 write!(f, "{c}")?;
259 }
260 Ok(())
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 const LEADER: Chord = Chord::ch(' ');
269
270 fn seq(s: &str) -> ChordSeq {
271 ChordSeq::parse(s, LEADER).expect(s)
272 }
273
274 #[test]
275 fn parses_plain_characters() {
276 assert_eq!(seq("j").0, vec![Chord::ch('j')]);
277 assert_eq!(seq("gg").0, vec![Chord::ch('g'), Chord::ch('g')]);
278 }
279
280 #[test]
281 fn parses_control_chords() {
282 assert_eq!(seq("<C-w>").0, vec![Chord::ctrl('w')]);
283 assert_eq!(seq("<c-d>").0, vec![Chord::ctrl('d')]);
284 }
285
286 #[test]
287 fn leader_expands_to_the_configured_chord() {
288 assert_eq!(
289 seq("<leader>ff").0,
290 vec![Chord::ch(' '), Chord::ch('f'), Chord::ch('f')]
291 );
292 let comma = ChordSeq::parse("<leader>x", Chord::ch(',')).unwrap();
294 assert_eq!(comma.0[0], Chord::ch(','));
295 }
296
297 #[test]
298 fn parses_named_keys() {
299 assert_eq!(seq("<Esc>").0, vec![Chord::plain(Key::Esc)]);
300 assert_eq!(seq("<CR>").0, vec![Chord::plain(Key::Enter)]);
301 assert_eq!(seq("<Space>").0, vec![Chord::ch(' ')]);
302 assert_eq!(seq("<F5>").0, vec![Chord::plain(Key::F(5))]);
303 assert_eq!(seq("<S-Tab>").0, vec![Chord::plain(Key::BackTab)]);
304 }
305
306 #[test]
307 fn parses_mixed_sequences() {
308 assert_eq!(
309 seq("<leader>s<C-w>").0,
310 vec![Chord::ch(' '), Chord::ch('s'), Chord::ctrl('w')]
311 );
312 }
313
314 #[test]
315 fn rejects_malformed_input() {
316 assert_eq!(
317 ChordSeq::parse("<C-w", LEADER),
318 Err(KeyParseError::Unterminated("<C-w".into()))
319 );
320 assert_eq!(
321 ChordSeq::parse("<nope>", LEADER),
322 Err(KeyParseError::UnknownKey("nope".into()))
323 );
324 assert_eq!(ChordSeq::parse("", LEADER), Err(KeyParseError::Empty));
325 }
326
327 #[test]
328 fn round_trips_through_display() {
329 for s in ["j", "gg", "<C-w>", "<Esc>", "<Space>", "<F5>", "<S-Tab>"] {
330 assert_eq!(seq(s).to_string(), s, "round-trip failed for {s}");
331 }
332 }
333
334 #[test]
335 fn only_unmodified_characters_are_insertable() {
336 assert_eq!(Chord::ch('j').as_insertable(), Some('j'));
337 assert_eq!(Chord::ctrl('j').as_insertable(), None);
338 assert_eq!(Chord::plain(Key::Esc).as_insertable(), None);
339 }
340}