1pub use lattice_host::chord::*;
28
29use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
30
31pub fn from_event(event: &KeyEvent) -> Option<KeyChord> {
56 let mut mods = KeyMods::NONE;
57 if event.modifiers.contains(KeyModifiers::CONTROL) {
58 mods = mods | KeyMods::CTRL;
59 }
60 if event.modifiers.contains(KeyModifiers::SHIFT) {
61 mods = mods | KeyMods::SHIFT;
62 }
63 if event.modifiers.contains(KeyModifiers::ALT) {
64 mods = mods | KeyMods::ALT;
65 }
66 if event.modifiers.contains(KeyModifiers::SUPER) {
67 mods = mods | KeyMods::SUPER;
68 }
69
70 let key = match event.code {
71 KeyCode::Esc => KeyKind::Special(SpecialKey::Esc),
72 KeyCode::Enter => KeyKind::Special(SpecialKey::Enter),
73 KeyCode::Tab => KeyKind::Special(SpecialKey::Tab),
74 KeyCode::BackTab => {
75 mods = mods | KeyMods::SHIFT;
77 KeyKind::Special(SpecialKey::Tab)
78 }
79 KeyCode::Backspace => KeyKind::Special(SpecialKey::Backspace),
80 KeyCode::Up => KeyKind::Special(SpecialKey::Up),
81 KeyCode::Down => KeyKind::Special(SpecialKey::Down),
82 KeyCode::Left => KeyKind::Special(SpecialKey::Left),
83 KeyCode::Right => KeyKind::Special(SpecialKey::Right),
84 KeyCode::Home => KeyKind::Special(SpecialKey::Home),
85 KeyCode::End => KeyKind::Special(SpecialKey::End),
86 KeyCode::PageUp => KeyKind::Special(SpecialKey::PageUp),
87 KeyCode::PageDown => KeyKind::Special(SpecialKey::PageDown),
88 KeyCode::Insert => KeyKind::Special(SpecialKey::Insert),
89 KeyCode::Delete => KeyKind::Special(SpecialKey::Delete),
90 KeyCode::F(n) if (1..=24).contains(&n) => KeyKind::Special(SpecialKey::F(n)),
91 KeyCode::Char(' ') if mods == KeyMods::NONE => KeyKind::Char(' '),
106 KeyCode::Char(' ') => KeyKind::Special(SpecialKey::Space),
107 KeyCode::Char(c) => {
108 let ctrl_or_alt = mods.ctrl() || mods.alt();
109 if ctrl_or_alt && c.is_ascii_alphabetic() {
110 KeyKind::Char(c.to_ascii_lowercase())
114 } else if !ctrl_or_alt {
115 if mods.shift() {
120 mods = mods.without(KeyMods::SHIFT);
121 }
122 KeyKind::Char(c)
123 } else {
124 KeyKind::Char(c)
125 }
126 }
127 _ => return None,
128 };
129
130 Some(KeyChord::new(key, mods))
134}
135
136pub fn format_chord(event: &KeyEvent) -> Option<String> {
140 from_event(event).map(|c| c.to_string())
141}
142
143#[cfg(test)]
144mod tests {
145 #![allow(clippy::unwrap_used)]
146 use super::*;
147 use crossterm::event::{KeyEventKind, KeyEventState};
148
149 fn ev(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
150 KeyEvent {
151 code,
152 modifiers: mods,
153 kind: KeyEventKind::Press,
154 state: KeyEventState::NONE,
155 }
156 }
157
158 #[test]
164 fn disambiguated_events_decode_to_the_same_chords() {
165 for (code, mods) in [
166 (KeyCode::Esc, KeyModifiers::NONE),
167 (KeyCode::Char('c'), KeyModifiers::CONTROL),
168 (KeyCode::Enter, KeyModifiers::NONE),
169 (KeyCode::Tab, KeyModifiers::SHIFT),
170 ] {
171 let plain = KeyEvent::new(code, mods);
172 let disambiguated = KeyEvent {
173 kind: KeyEventKind::Press,
174 state: KeyEventState::NONE,
175 ..plain
176 };
177 assert_eq!(
178 from_event(&plain),
179 from_event(&disambiguated),
180 "{code:?}+{mods:?} must decode identically under disambiguation"
181 );
182 }
183 }
184
185 #[test]
190 fn shift_enter_is_a_distinct_chord_from_enter() {
191 let enter = from_event(&ev(KeyCode::Enter, KeyModifiers::NONE));
192 let s_enter = from_event(&ev(KeyCode::Enter, KeyModifiers::SHIFT));
193 assert_ne!(enter, s_enter);
194 assert_eq!(s_enter.unwrap().to_string(), "<S-CR>");
195 }
196
197 #[test]
211 fn ctrl_space_promotes_to_the_special_key_the_parser_produces() {
212 let chord = from_event(&ev(KeyCode::Char(' '), KeyModifiers::CONTROL)).unwrap();
213 assert_eq!(chord.key, KeyKind::Special(SpecialKey::Space));
214 assert!(chord.mods.ctrl());
215 }
216
217 #[test]
221 fn a_real_ctrl_space_matches_what_the_binding_string_parses_to() {
222 let parsed = lattice_protocol::parse_chord_sequence("<C-Space>").expect("parses");
223 let pressed = from_event(&ev(KeyCode::Char(' '), KeyModifiers::CONTROL)).unwrap();
224 assert_eq!(parsed.as_slice(), &[pressed]);
225 }
226
227 #[test]
238 fn a_real_bare_space_matches_what_the_leader_string_parses_to() {
239 let parsed = lattice_protocol::parse_chord_sequence("<Space>").expect("parses");
240 let pressed = from_event(&ev(KeyCode::Char(' '), KeyModifiers::NONE)).unwrap();
241 assert_eq!(parsed.as_slice(), &[pressed]);
242 }
243
244 #[test]
246 fn a_leader_chord_matches_the_keys_that_produce_it() {
247 let parsed = lattice_protocol::parse_chord_sequence("<Space>oa").expect("parses");
248 let typed: Vec<_> = [
249 ev(KeyCode::Char(' '), KeyModifiers::NONE),
250 ev(KeyCode::Char('o'), KeyModifiers::NONE),
251 ev(KeyCode::Char('a'), KeyModifiers::NONE),
252 ]
253 .iter()
254 .map(|e| from_event(e).unwrap())
255 .collect();
256 assert_eq!(parsed, typed);
257 }
258
259 #[test]
261 fn alt_space_promotes_as_well() {
262 let chord = from_event(&ev(KeyCode::Char(' '), KeyModifiers::ALT)).unwrap();
263 assert_eq!(chord.key, KeyKind::Special(SpecialKey::Space));
264 }
265
266 #[test]
270 fn a_bare_space_stays_a_literal_char() {
271 let chord = from_event(&ev(KeyCode::Char(' '), KeyModifiers::NONE)).unwrap();
272 assert_eq!(chord.key, KeyKind::Char(' '));
273 }
274
275 #[test]
276 fn plain_char_renders_unwrapped() {
277 assert_eq!(
278 format_chord(&ev(KeyCode::Char('a'), KeyModifiers::NONE)),
279 Some("a".into())
280 );
281 assert_eq!(
282 format_chord(&ev(KeyCode::Char('$'), KeyModifiers::NONE)),
283 Some("$".into())
284 );
285 }
286
287 #[test]
288 fn ctrl_letter_renders_with_c_prefix_lowercase() {
289 assert_eq!(
292 format_chord(&ev(KeyCode::Char('c'), KeyModifiers::CONTROL)),
293 Some("<C-c>".into())
294 );
295 assert_eq!(
296 format_chord(&ev(KeyCode::Char('C'), KeyModifiers::CONTROL)),
297 Some("<C-c>".into())
298 );
299 }
300
301 #[test]
302 fn alt_letter_renders_with_m_prefix() {
303 assert_eq!(
304 format_chord(&ev(KeyCode::Char('x'), KeyModifiers::ALT)),
305 Some("<M-x>".into())
306 );
307 }
308
309 #[test]
310 fn ctrl_shift_letter_canonical_order() {
311 assert_eq!(
312 format_chord(&ev(
313 KeyCode::Char('c'),
314 KeyModifiers::CONTROL | KeyModifiers::SHIFT
315 )),
316 Some("<C-S-c>".into())
317 );
318 }
319
320 #[test]
321 fn special_keys_render_with_canonical_names() {
322 assert_eq!(
323 format_chord(&ev(KeyCode::Esc, KeyModifiers::NONE)),
324 Some("<Esc>".into())
325 );
326 assert_eq!(
327 format_chord(&ev(KeyCode::Tab, KeyModifiers::NONE)),
328 Some("<Tab>".into())
329 );
330 assert_eq!(
331 format_chord(&ev(KeyCode::Enter, KeyModifiers::NONE)),
332 Some("<CR>".into())
333 );
334 assert_eq!(
335 format_chord(&ev(KeyCode::Backspace, KeyModifiers::NONE)),
336 Some("<BS>".into())
337 );
338 assert_eq!(
339 format_chord(&ev(KeyCode::Left, KeyModifiers::NONE)),
340 Some("<Left>".into())
341 );
342 }
343
344 #[test]
345 fn ctrl_special_key_carries_modifier() {
346 assert_eq!(
347 format_chord(&ev(KeyCode::Up, KeyModifiers::CONTROL)),
348 Some("<C-Up>".into())
349 );
350 }
351
352 #[test]
353 fn back_tab_encodes_shift_without_double_prefix() {
354 assert_eq!(
355 format_chord(&ev(KeyCode::BackTab, KeyModifiers::SHIFT)),
356 Some("<S-Tab>".into())
357 );
358 }
359
360 #[test]
361 fn function_keys_render_as_fn() {
362 assert_eq!(
363 format_chord(&ev(KeyCode::F(1), KeyModifiers::NONE)),
364 Some("<F1>".into())
365 );
366 assert_eq!(
367 format_chord(&ev(KeyCode::F(12), KeyModifiers::NONE)),
368 Some("<F12>".into())
369 );
370 }
371
372 #[test]
373 fn literal_lt_escapes_as_lt_token() {
374 assert_eq!(
375 format_chord(&ev(KeyCode::Char('<'), KeyModifiers::NONE)),
376 Some("<lt>".into())
377 );
378 }
379
380 #[test]
381 fn from_event_normalises_ctrl_letter_lowercase() {
382 let lower = from_event(&ev(KeyCode::Char('c'), KeyModifiers::CONTROL)).expect("ctrl-c");
383 let upper = from_event(&ev(KeyCode::Char('C'), KeyModifiers::CONTROL)).expect("ctrl-C");
384 assert_eq!(lower, upper);
385 assert_eq!(lower, KeyChord::ctrl('c'));
386 }
387
388 #[test]
389 fn from_event_strips_redundant_shift_on_bare_letter() {
390 let chord = from_event(&ev(KeyCode::Char('A'), KeyModifiers::SHIFT)).expect("shift-A");
393 assert_eq!(chord, KeyChord::char('A'));
394 assert!(!chord.mods.shift());
395 }
396
397 #[test]
398 fn from_event_keeps_shift_on_special_keys() {
399 let stab = from_event(&ev(KeyCode::Tab, KeyModifiers::SHIFT)).expect("shift-tab");
400 assert_eq!(stab.key, KeyKind::Special(SpecialKey::Tab));
401 assert!(stab.mods.shift());
402 let sf1 = from_event(&ev(KeyCode::F(1), KeyModifiers::SHIFT)).expect("shift-F1");
403 assert!(sf1.mods.shift());
404 }
405
406 #[test]
407 fn from_event_canonicalises_back_tab_to_tab_plus_shift() {
408 let chord = from_event(&ev(KeyCode::BackTab, KeyModifiers::NONE)).expect("back-tab");
409 assert_eq!(chord.key, KeyKind::Special(SpecialKey::Tab));
410 assert!(chord.mods.shift());
411 }
412
413 #[test]
414 fn keyevent_to_keychord_to_string_matches_format_chord() {
415 let cases = &[
419 ev(KeyCode::Char('a'), KeyModifiers::NONE),
420 ev(KeyCode::Char('A'), KeyModifiers::NONE),
421 ev(KeyCode::Char('c'), KeyModifiers::CONTROL),
422 ev(
423 KeyCode::Char('c'),
424 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
425 ),
426 ev(KeyCode::Char('x'), KeyModifiers::ALT),
427 ev(KeyCode::Esc, KeyModifiers::NONE),
428 ev(KeyCode::Tab, KeyModifiers::NONE),
429 ev(KeyCode::BackTab, KeyModifiers::NONE),
430 ev(KeyCode::Tab, KeyModifiers::SHIFT),
431 ev(KeyCode::F(7), KeyModifiers::NONE),
432 ev(KeyCode::Char('<'), KeyModifiers::NONE),
433 ];
434 for e in cases {
435 let via_shim = format_chord(e);
436 let via_typed = from_event(e).map(|c| c.to_string());
437 assert_eq!(via_shim, via_typed, "mismatch for {e:?}");
438 }
439 }
440}