Skip to main content

wisp/
keybindings.rs

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}