1use tui::{KeyCode, KeyEvent, KeyModifiers};
2
3#[doc = include_str!("docs/key_binding.md")]
4#[derive(Clone, Debug)]
5pub struct KeyBinding {
6 pub code: KeyCode,
7 pub modifiers: KeyModifiers,
8}
9
10impl KeyBinding {
11 pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
12 Self { code, modifiers }
13 }
14
15 pub fn matches(&self, event: KeyEvent) -> bool {
16 self.code == event.code && event.modifiers.contains(self.modifiers)
17 }
18
19 pub fn char(&self) -> Option<char> {
20 match self.code {
21 KeyCode::Char(c) => Some(c),
22 _ => None,
23 }
24 }
25}
26
27#[doc = include_str!("docs/keybindings.md")]
28#[derive(Clone, Debug)]
29pub struct Keybindings {
30 pub exit: KeyBinding,
31 pub cancel: KeyBinding,
32 pub cycle_reasoning: KeyBinding,
33 pub cycle_mode: KeyBinding,
34 pub submit: KeyBinding,
35 pub open_command_picker: KeyBinding,
36 pub open_file_picker: KeyBinding,
37 pub toggle_git_diff: KeyBinding,
38}
39
40impl Default for Keybindings {
41 fn default() -> Self {
42 Self {
43 exit: KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
44 cancel: KeyBinding::new(KeyCode::Esc, KeyModifiers::NONE),
45 cycle_reasoning: KeyBinding::new(KeyCode::Tab, KeyModifiers::NONE),
46 cycle_mode: KeyBinding::new(KeyCode::BackTab, KeyModifiers::NONE),
47 submit: KeyBinding::new(KeyCode::Enter, KeyModifiers::NONE),
48 open_command_picker: KeyBinding::new(KeyCode::Char('/'), KeyModifiers::NONE),
49 open_file_picker: KeyBinding::new(KeyCode::Char('@'), KeyModifiers::NONE),
50 toggle_git_diff: KeyBinding::new(KeyCode::Char('g'), KeyModifiers::CONTROL),
51 }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 fn key(code: KeyCode) -> KeyEvent {
60 KeyEvent::new(code, KeyModifiers::NONE)
61 }
62
63 #[test]
64 fn matches_simple_key() {
65 let binding = KeyBinding::new(KeyCode::Tab, KeyModifiers::NONE);
66 assert!(binding.matches(key(KeyCode::Tab)));
67 assert!(!binding.matches(key(KeyCode::Enter)));
68 }
69
70 #[test]
71 fn matches_key_with_modifier() {
72 let binding = KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
73 assert!(binding.matches(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)));
74 assert!(!binding.matches(key(KeyCode::Char('c'))));
75 }
76
77 #[test]
78 fn matches_ignores_extra_modifiers() {
79 let binding = KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
80 let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL | KeyModifiers::SHIFT);
81 assert!(binding.matches(event));
82 }
83
84 #[test]
85 fn char_returns_char_for_char_key() {
86 let binding = KeyBinding::new(KeyCode::Char('/'), KeyModifiers::NONE);
87 assert_eq!(binding.char(), Some('/'));
88 }
89
90 #[test]
91 fn char_returns_none_for_non_char_key() {
92 let binding = KeyBinding::new(KeyCode::Enter, KeyModifiers::NONE);
93 assert_eq!(binding.char(), None);
94 }
95
96 #[test]
97 fn default_keybindings_have_expected_values() {
98 let kb = Keybindings::default();
99 assert_eq!(kb.exit.code, KeyCode::Char('c'));
100 assert_eq!(kb.exit.modifiers, KeyModifiers::CONTROL);
101 assert_eq!(kb.cancel.code, KeyCode::Esc);
102 assert_eq!(kb.cycle_reasoning.code, KeyCode::Tab);
103 assert_eq!(kb.cycle_mode.code, KeyCode::BackTab);
104 assert_eq!(kb.submit.code, KeyCode::Enter);
105 assert_eq!(kb.open_command_picker.code, KeyCode::Char('/'));
106 assert_eq!(kb.open_file_picker.code, KeyCode::Char('@'));
107 assert_eq!(kb.toggle_git_diff.code, KeyCode::Char('g'));
108 assert_eq!(kb.toggle_git_diff.modifiers, KeyModifiers::CONTROL);
109 }
110}