1use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
2
3fn f_key_name(n: u8) -> String {
9 format!("f{}", n)
10}
11
12fn parse_f_key(key_name: &str) -> Option<u8> {
14 if let Some(rest) = key_name.strip_prefix('f') {
15 rest.parse().ok().filter(|&n: &u8| (1..=24).contains(&n))
16 } else if let Some(rest) = key_name.strip_prefix('F') {
17 rest.parse().ok().filter(|&n: &u8| (1..=24).contains(&n))
18 } else {
19 None
20 }
21}
22
23fn format_key_id(key_name: &str, mods: KeyModifiers) -> String {
26 let mut parts: Vec<&str> = Vec::new();
27 if mods.contains(KeyModifiers::CONTROL) {
28 parts.push("ctrl");
29 }
30 if mods.contains(KeyModifiers::SHIFT) {
31 parts.push("shift");
32 }
33 if mods.contains(KeyModifiers::ALT) {
34 parts.push("alt");
35 }
36 if mods.contains(KeyModifiers::SUPER) {
37 parts.push("super");
38 }
39 if parts.is_empty() {
40 key_name.to_string()
41 } else {
42 parts.push(key_name);
43 parts.join("+")
44 }
45}
46
47pub fn key_event_to_id(event: &KeyEvent) -> Option<String> {
52 let mods = event.modifiers;
53
54 match event.code {
55 KeyCode::Enter => Some(format_key_id("enter", mods)),
56 KeyCode::Esc => Some("escape".to_string()),
57 KeyCode::Tab => {
58 if mods.contains(KeyModifiers::SHIFT) {
59 Some("shift+tab".to_string())
60 } else {
61 Some(format_key_id("tab", mods))
62 }
63 }
64 KeyCode::Backspace => Some(format_key_id("backspace", mods)),
65 KeyCode::Delete => Some(format_key_id("delete", mods)),
66 KeyCode::Home => Some(format_key_id("home", mods)),
67 KeyCode::End => Some(format_key_id("end", mods)),
68 KeyCode::PageUp => Some(format_key_id("pageUp", mods)),
69 KeyCode::PageDown => Some(format_key_id("pageDown", mods)),
70 KeyCode::Up => Some(format_key_id("up", mods)),
71 KeyCode::Down => Some(format_key_id("down", mods)),
72 KeyCode::Left => Some(format_key_id("left", mods)),
73 KeyCode::Right => Some(format_key_id("right", mods)),
74 KeyCode::BackTab => Some("shift+tab".to_string()),
75 KeyCode::Insert => Some(format_key_id("insert", mods)),
76 KeyCode::F(n) => Some(format_key_id(&f_key_name(n), mods)),
77 KeyCode::Char(c) => {
78 if mods.is_empty() || mods == KeyModifiers::SHIFT {
79 Some(c.to_string())
81 } else if mods.contains(KeyModifiers::CONTROL) && !mods.contains(KeyModifiers::ALT) {
82 let mut parts: Vec<String> = Vec::new();
84 parts.push("ctrl".into());
85 if mods.contains(KeyModifiers::SHIFT) {
86 parts.push("shift".into());
87 }
88 if mods.contains(KeyModifiers::SUPER) {
89 parts.push("super".into());
90 }
91 let lower = c.to_ascii_lowercase();
92 Some(format!("{}+{}", parts.join("+"), lower))
93 } else if mods.contains(KeyModifiers::ALT) {
94 let mut parts: Vec<String> = Vec::new();
95 if mods.contains(KeyModifiers::CONTROL) {
96 parts.push("ctrl".into());
97 }
98 parts.push("alt".into());
99 if mods.contains(KeyModifiers::SHIFT) {
100 parts.push("shift".into());
101 }
102 if mods.contains(KeyModifiers::SUPER) {
103 parts.push("super".into());
104 }
105 Some(format!("{}+{}", parts.join("+"), c))
106 } else {
107 Some(c.to_string())
108 }
109 }
110
111 KeyCode::Null
112 | KeyCode::CapsLock
113 | KeyCode::ScrollLock
114 | KeyCode::NumLock
115 | KeyCode::PrintScreen
116 | KeyCode::Pause
117 | KeyCode::Menu
118 | KeyCode::KeypadBegin
119 | KeyCode::Media(_)
120 | KeyCode::Modifier(_) => None,
121 }
122}
123
124fn parse_key_id(key_id: &str) -> Option<(&str, bool, bool, bool, bool)> {
127 if key_id.is_empty() {
128 return None;
129 }
130 let parts: Vec<&str> = key_id.split('+').collect();
131 if parts.is_empty() {
132 return None;
133 }
134 let key = parts[parts.len() - 1];
135 let mut ctrl = false;
136 let mut shift = false;
137 let mut alt = false;
138 let mut super_mod = false;
139
140 for p in &parts[..parts.len() - 1] {
141 match *p {
142 "ctrl" => ctrl = true,
143 "shift" => shift = true,
144 "alt" => alt = true,
145 "super" => super_mod = true,
146 _ => return None, }
148 }
149
150 Some((key, ctrl, shift, alt, super_mod))
151}
152
153pub fn match_key_id(event: &KeyEvent, key_id: &str) -> bool {
157 let Some((key, wants_ctrl, wants_shift, wants_alt, wants_super)) = parse_key_id(key_id) else {
158 return false;
159 };
160
161 let mods = event.modifiers;
162 let has_ctrl = mods.contains(KeyModifiers::CONTROL);
163 let has_shift = mods.contains(KeyModifiers::SHIFT);
164 let has_alt = mods.contains(KeyModifiers::ALT);
165 let has_super = mods.contains(KeyModifiers::SUPER);
166
167 let is_backtab = event.code == KeyCode::BackTab;
170 let wants_tab = key == "tab";
171
172 let wanted_shift = has_shift || (is_backtab && wants_tab);
175
176 if wants_ctrl && !has_ctrl {
179 return false;
180 }
181 if wants_shift && !wanted_shift {
182 return false;
183 }
184 if wants_alt && !has_alt {
185 return false;
186 }
187 if wants_super && !has_super {
188 return false;
189 }
190
191 if !wants_ctrl && has_ctrl {
196 return false;
197 }
198 if !wants_alt && has_alt {
199 return false;
200 }
201 if !wants_super && has_super {
202 return false;
203 }
204 if !wants_shift && has_shift && !is_backtab {
209 let shiftable = key.len() == 1 && {
212 let c = key.chars().next().unwrap();
213 c.is_ascii_uppercase()
214 || c.is_ascii_digit()
215 || matches!(
216 c,
217 '!' | '@'
218 | '#'
219 | '$'
220 | '%'
221 | '^'
222 | '&'
223 | '*'
224 | '('
225 | ')'
226 | '_'
227 | '+'
228 | '|'
229 | '~'
230 | '{'
231 | '}'
232 | ':'
233 | '"'
234 | '<'
235 | '>'
236 | '?'
237 )
238 };
239 if !shiftable {
240 return false;
241 }
242 }
243
244 if event.code == KeyCode::BackTab && !wants_shift {
246 return false;
247 }
248
249 matches_key_name(&event.code, key)
251}
252
253fn matches_key_name(code: &KeyCode, key_name: &str) -> bool {
255 match code {
256 KeyCode::Enter => key_name == "enter" || key_name == "return",
257 KeyCode::Esc => key_name == "escape" || key_name == "esc",
258 KeyCode::Tab | KeyCode::BackTab => key_name == "tab",
259 KeyCode::Backspace => key_name == "backspace",
260 KeyCode::Delete => key_name == "delete",
261 KeyCode::Home => key_name == "home",
262 KeyCode::End => key_name == "end",
263 KeyCode::PageUp => key_name == "pageUp" || key_name == "pageup",
264 KeyCode::PageDown => key_name == "pageDown" || key_name == "pagedown",
265 KeyCode::Up => key_name == "up",
266 KeyCode::Down => key_name == "down",
267 KeyCode::Left => key_name == "left",
268 KeyCode::Right => key_name == "right",
269 KeyCode::Insert => key_name == "insert",
270 KeyCode::F(n) => Some(*n) == parse_f_key(key_name),
271 KeyCode::Char(c) if key_name.len() == 1 => {
272 let key_char = key_name.chars().next().unwrap();
274 c.eq_ignore_ascii_case(&key_char)
275 }
276 _ => false,
277 }
278}
279
280pub fn is_key_release(event: &KeyEvent) -> bool {
282 event.kind == KeyEventKind::Release
283}
284
285pub fn is_key_repeat(event: &KeyEvent) -> bool {
287 event.kind == KeyEventKind::Repeat
288}
289
290pub fn decode_kitty_printable(event: &KeyEvent) -> Option<String> {
294 match event.code {
295 KeyCode::Char(c)
296 if !event.modifiers.contains(KeyModifiers::CONTROL)
297 && !event.modifiers.contains(KeyModifiers::ALT) =>
298 {
299 Some(c.to_string())
300 }
301 _ => None,
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
312pub enum Key {
313 Enter,
314 Escape,
315 Tab,
316 Backspace,
317 Delete,
318 Home,
319 End,
320 PageUp,
321 PageDown,
322 Up,
323 Down,
324 Left,
325 Right,
326 Space,
327 Char(char),
329 Ctrl(char),
331 Alt(char),
333 ShiftTab,
335 CtrlShift(char),
337 AltLeft,
339 AltRight,
340 CtrlLeft,
342 CtrlRight,
343}
344
345impl Key {
346 pub fn enter() -> Self {
347 Key::Enter
348 }
349 pub fn escape() -> Self {
350 Key::Escape
351 }
352 pub fn tab() -> Self {
353 Key::Tab
354 }
355 pub fn space() -> Self {
356 Key::Space
357 }
358 pub fn backspace() -> Self {
359 Key::Backspace
360 }
361 pub fn delete() -> Self {
362 Key::Delete
363 }
364 pub fn home() -> Self {
365 Key::Home
366 }
367 pub fn end() -> Self {
368 Key::End
369 }
370 pub fn up() -> Self {
371 Key::Up
372 }
373 pub fn down() -> Self {
374 Key::Down
375 }
376 pub fn left() -> Self {
377 Key::Left
378 }
379 pub fn right() -> Self {
380 Key::Right
381 }
382 pub fn page_up() -> Self {
383 Key::PageUp
384 }
385 pub fn page_down() -> Self {
386 Key::PageDown
387 }
388 pub fn ctrl(c: char) -> Self {
389 Key::Ctrl(c.to_ascii_lowercase())
390 }
391 pub fn alt(c: char) -> Self {
392 Key::Alt(c)
393 }
394 pub fn shift_tab() -> Self {
395 Key::ShiftTab
396 }
397 pub fn ctrl_shift(c: char) -> Self {
398 Key::CtrlShift(c.to_ascii_lowercase())
399 }
400 pub fn alt_left() -> Self {
401 Key::AltLeft
402 }
403 pub fn alt_right() -> Self {
404 Key::AltRight
405 }
406 pub fn ctrl_left() -> Self {
407 Key::CtrlLeft
408 }
409 pub fn ctrl_right() -> Self {
410 Key::CtrlRight
411 }
412}
413
414pub fn matches_key(event: &KeyEvent, key: &Key) -> bool {
416 match key {
417 Key::Enter => event.code == KeyCode::Enter,
418 Key::Escape => event.code == KeyCode::Esc,
419 Key::Tab => event.code == KeyCode::Tab,
420 Key::Backspace => event.code == KeyCode::Backspace,
421 Key::Delete => event.code == KeyCode::Delete,
422 Key::Home => event.code == KeyCode::Home,
423 Key::End => event.code == KeyCode::End,
424 Key::PageUp => event.code == KeyCode::PageUp,
425 Key::PageDown => event.code == KeyCode::PageDown,
426 Key::Up => event.code == KeyCode::Up,
427 Key::Down => event.code == KeyCode::Down,
428 Key::Left => event.code == KeyCode::Left,
429 Key::Right => event.code == KeyCode::Right,
430 Key::Space => event.code == KeyCode::Char(' '),
431 Key::Char(c) => {
432 event.code == KeyCode::Char(*c)
433 && !event.modifiers.contains(KeyModifiers::CONTROL)
434 && !event.modifiers.contains(KeyModifiers::ALT)
435 }
436 Key::Ctrl(c) => {
437 event.code == KeyCode::Char(c.to_ascii_lowercase())
438 && event.modifiers.contains(KeyModifiers::CONTROL)
439 && !event.modifiers.contains(KeyModifiers::ALT)
440 }
441 Key::Alt(c) => {
442 event.code == KeyCode::Char(*c)
443 && event.modifiers.contains(KeyModifiers::ALT)
444 && !event.modifiers.contains(KeyModifiers::CONTROL)
445 }
446 Key::ShiftTab => {
447 event.code == KeyCode::BackTab
448 || (event.code == KeyCode::Tab && event.modifiers.contains(KeyModifiers::SHIFT))
449 }
450 Key::CtrlShift(c) => {
451 event.code == KeyCode::Char(c.to_ascii_lowercase())
452 && event.modifiers.contains(KeyModifiers::CONTROL)
453 && event.modifiers.contains(KeyModifiers::SHIFT)
454 }
455 Key::AltLeft => event.code == KeyCode::Left && event.modifiers.contains(KeyModifiers::ALT),
456 Key::AltRight => {
457 event.code == KeyCode::Right && event.modifiers.contains(KeyModifiers::ALT)
458 }
459 Key::CtrlLeft => {
460 event.code == KeyCode::Left && event.modifiers.contains(KeyModifiers::CONTROL)
461 }
462 Key::CtrlRight => {
463 event.code == KeyCode::Right && event.modifiers.contains(KeyModifiers::CONTROL)
464 }
465 }
466}
467
468pub fn key_event_to_string(event: &KeyEvent) -> Option<String> {
470 match event.code {
471 KeyCode::Char(c) => {
472 if event.modifiers.is_empty() || event.modifiers == KeyModifiers::SHIFT {
473 Some(c.to_string())
474 } else {
475 None
476 }
477 }
478 KeyCode::Enter => Some("\n".to_string()),
479 KeyCode::Tab => Some("\t".to_string()),
480 _ => None,
481 }
482}
483
484pub fn is_printable(event: &KeyEvent) -> bool {
486 matches!(event.code, KeyCode::Char(_))
487 && !event.modifiers.contains(KeyModifiers::CONTROL)
488 && !event.modifiers.contains(KeyModifiers::ALT)
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn test_key_event_to_id_enter() {
497 let event = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
498 assert_eq!(key_event_to_id(&event), Some("enter".into()));
499 }
500
501 #[test]
502 fn test_key_event_to_id_escape() {
503 let event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
504 assert_eq!(key_event_to_id(&event), Some("escape".into()));
505 }
506
507 #[test]
508 fn test_key_event_to_id_ctrl_c() {
509 let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
510 assert_eq!(key_event_to_id(&event), Some("ctrl+c".into()));
511 }
512
513 #[test]
514 fn test_key_event_to_id_char() {
515 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
516 assert_eq!(key_event_to_id(&event), Some("a".into()));
517 }
518
519 #[test]
520 fn test_key_event_to_id_shift_tab() {
521 let event = KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE);
522 assert_eq!(key_event_to_id(&event), Some("shift+tab".into()));
523 }
524
525 #[test]
526 fn test_key_event_to_id_ctrl_shift() {
527 let event = KeyEvent::new(
528 KeyCode::Char('p'),
529 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
530 );
531 assert_eq!(key_event_to_id(&event), Some("ctrl+shift+p".into()));
532 }
533
534 #[test]
535 fn test_key_event_to_id_alt_left() {
536 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::ALT);
537 assert_eq!(key_event_to_id(&event), Some("alt+left".into()));
538 }
539
540 #[test]
541 fn test_key_event_to_id_ctrl_left() {
542 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL);
543 assert_eq!(key_event_to_id(&event), Some("ctrl+left".into()));
544 }
545
546 #[test]
547 fn test_match_key_id_exact() {
548 let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
549 assert!(match_key_id(&event, "ctrl+c"));
550 assert!(!match_key_id(&event, "ctrl+x"));
551 }
552
553 #[test]
554 fn test_match_key_id_no_extra_modifiers() {
555 let event = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL);
557 assert!(!match_key_id(&event, "enter"));
558 }
559
560 #[test]
563 fn test_matches_enter() {
564 let event = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
565 assert!(matches_key(&event, &Key::Enter));
566 }
567
568 #[test]
569 fn test_matches_escape() {
570 let event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
571 assert!(matches_key(&event, &Key::Escape));
572 }
573
574 #[test]
575 fn test_matches_ctrl_c() {
576 let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
577 assert!(matches_key(&event, &Key::Ctrl('c')));
578 assert!(!matches_key(&event, &Key::Char('c')));
579 }
580
581 #[test]
582 fn test_matches_char() {
583 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
584 assert!(matches_key(&event, &Key::Char('a')));
585 assert!(!matches_key(&event, &Key::Ctrl('a')));
586 }
587
588 #[test]
589 fn test_matches_arrow_keys() {
590 assert!(matches_key(
591 &KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
592 &Key::Up
593 ));
594 assert!(matches_key(
595 &KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
596 &Key::Down
597 ));
598 assert!(matches_key(
599 &KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
600 &Key::Left
601 ));
602 assert!(matches_key(
603 &KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
604 &Key::Right
605 ));
606 }
607
608 #[test]
609 fn test_shift_tab() {
610 assert!(matches_key(
611 &KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE),
612 &Key::ShiftTab
613 ));
614 }
615
616 #[test]
617 fn test_ctrl_shift() {
618 let event = KeyEvent::new(
619 KeyCode::Char('p'),
620 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
621 );
622 assert!(matches_key(&event, &Key::CtrlShift('p')));
623 }
624
625 #[test]
626 fn test_is_printable() {
627 assert!(is_printable(&KeyEvent::new(
628 KeyCode::Char('a'),
629 KeyModifiers::NONE
630 )));
631 assert!(!is_printable(&KeyEvent::new(
632 KeyCode::Char('c'),
633 KeyModifiers::CONTROL
634 )));
635 assert!(!is_printable(&KeyEvent::new(
636 KeyCode::Enter,
637 KeyModifiers::NONE
638 )));
639 }
640
641 #[test]
642 fn test_key_event_to_id_up() {
643 let event = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
644 assert_eq!(key_event_to_id(&event), Some("up".into()));
645 }
646
647 #[test]
648 fn test_key_event_to_id_backspace() {
649 let event = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
650 assert_eq!(key_event_to_id(&event), Some("backspace".into()));
651 }
652}