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