Skip to main content

mockforge_tui/
keybindings.rs

1//! Centralised keybinding definitions.
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5/// Recognised global actions.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Action {
8    Quit,
9    NextTab,
10    PrevTab,
11    JumpTab(usize),
12    Refresh,
13    ToggleHelp,
14    StartFilter,
15    ScrollUp,
16    ScrollDown,
17    ScrollTop,
18    ScrollBottom,
19    PageUp,
20    PageDown,
21    Select,
22    Back,
23    ToggleFollow,
24    Edit,
25    Toggle,
26    Delete,
27    Sort,
28}
29
30/// Map a key event to an [`Action`].
31pub fn resolve(key: KeyEvent) -> Option<Action> {
32    // Ctrl+C / Ctrl+Q always quit
33    if key.modifiers.contains(KeyModifiers::CONTROL) {
34        return match key.code {
35            KeyCode::Char('c' | 'q') => Some(Action::Quit),
36            _ => None,
37        };
38    }
39
40    match key.code {
41        KeyCode::Char('q') => Some(Action::Quit),
42        KeyCode::Tab => Some(Action::NextTab),
43        KeyCode::BackTab => Some(Action::PrevTab),
44        KeyCode::Char('1') => Some(Action::JumpTab(0)),
45        KeyCode::Char('2') => Some(Action::JumpTab(1)),
46        KeyCode::Char('3') => Some(Action::JumpTab(2)),
47        KeyCode::Char('4') => Some(Action::JumpTab(3)),
48        KeyCode::Char('5') => Some(Action::JumpTab(4)),
49        KeyCode::Char('6') => Some(Action::JumpTab(5)),
50        KeyCode::Char('7') => Some(Action::JumpTab(6)),
51        KeyCode::Char('8') => Some(Action::JumpTab(7)),
52        KeyCode::Char('9') => Some(Action::JumpTab(8)),
53        KeyCode::Char('0') => Some(Action::JumpTab(9)),
54        KeyCode::Char('r') => Some(Action::Refresh),
55        KeyCode::Char('?') => Some(Action::ToggleHelp),
56        KeyCode::Char('/') => Some(Action::StartFilter),
57        KeyCode::Char('j') | KeyCode::Down => Some(Action::ScrollDown),
58        KeyCode::Char('k') | KeyCode::Up => Some(Action::ScrollUp),
59        KeyCode::Char('g') => Some(Action::ScrollTop),
60        KeyCode::Char('G') => Some(Action::ScrollBottom),
61        KeyCode::PageUp => Some(Action::PageUp),
62        KeyCode::PageDown => Some(Action::PageDown),
63        KeyCode::Enter => Some(Action::Select),
64        KeyCode::Esc => Some(Action::Back),
65        KeyCode::Char('f') => Some(Action::ToggleFollow),
66        KeyCode::Char('e') => Some(Action::Edit),
67        KeyCode::Char('t') => Some(Action::Toggle),
68        KeyCode::Char('d') => Some(Action::Delete),
69        KeyCode::Char('s') => Some(Action::Sort),
70        _ => None,
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
78
79    /// Helper to build a plain key event with no modifiers.
80    fn key(code: KeyCode) -> KeyEvent {
81        KeyEvent {
82            code,
83            modifiers: KeyModifiers::NONE,
84            kind: KeyEventKind::Press,
85            state: KeyEventState::NONE,
86        }
87    }
88
89    /// Helper to build a key event with Ctrl modifier.
90    fn ctrl(code: KeyCode) -> KeyEvent {
91        KeyEvent {
92            code,
93            modifiers: KeyModifiers::CONTROL,
94            kind: KeyEventKind::Press,
95            state: KeyEventState::NONE,
96        }
97    }
98
99    #[test]
100    fn q_maps_to_quit() {
101        assert_eq!(resolve(key(KeyCode::Char('q'))), Some(Action::Quit));
102    }
103
104    #[test]
105    fn ctrl_c_maps_to_quit() {
106        assert_eq!(resolve(ctrl(KeyCode::Char('c'))), Some(Action::Quit));
107    }
108
109    #[test]
110    fn ctrl_q_maps_to_quit() {
111        assert_eq!(resolve(ctrl(KeyCode::Char('q'))), Some(Action::Quit));
112    }
113
114    #[test]
115    fn tab_maps_to_next_tab() {
116        assert_eq!(resolve(key(KeyCode::Tab)), Some(Action::NextTab));
117    }
118
119    #[test]
120    fn backtab_maps_to_prev_tab() {
121        assert_eq!(resolve(key(KeyCode::BackTab)), Some(Action::PrevTab));
122    }
123
124    #[test]
125    fn digit_keys_map_to_jump_tab() {
126        assert_eq!(resolve(key(KeyCode::Char('1'))), Some(Action::JumpTab(0)));
127        assert_eq!(resolve(key(KeyCode::Char('2'))), Some(Action::JumpTab(1)));
128        assert_eq!(resolve(key(KeyCode::Char('3'))), Some(Action::JumpTab(2)));
129        assert_eq!(resolve(key(KeyCode::Char('4'))), Some(Action::JumpTab(3)));
130        assert_eq!(resolve(key(KeyCode::Char('5'))), Some(Action::JumpTab(4)));
131        assert_eq!(resolve(key(KeyCode::Char('6'))), Some(Action::JumpTab(5)));
132        assert_eq!(resolve(key(KeyCode::Char('7'))), Some(Action::JumpTab(6)));
133        assert_eq!(resolve(key(KeyCode::Char('8'))), Some(Action::JumpTab(7)));
134        assert_eq!(resolve(key(KeyCode::Char('9'))), Some(Action::JumpTab(8)));
135        assert_eq!(resolve(key(KeyCode::Char('0'))), Some(Action::JumpTab(9)));
136    }
137
138    #[test]
139    fn r_maps_to_refresh() {
140        assert_eq!(resolve(key(KeyCode::Char('r'))), Some(Action::Refresh));
141    }
142
143    #[test]
144    fn question_mark_maps_to_toggle_help() {
145        assert_eq!(resolve(key(KeyCode::Char('?'))), Some(Action::ToggleHelp));
146    }
147
148    #[test]
149    fn slash_maps_to_start_filter() {
150        assert_eq!(resolve(key(KeyCode::Char('/'))), Some(Action::StartFilter));
151    }
152
153    #[test]
154    fn navigation_keys() {
155        assert_eq!(resolve(key(KeyCode::Char('j'))), Some(Action::ScrollDown));
156        assert_eq!(resolve(key(KeyCode::Down)), Some(Action::ScrollDown));
157        assert_eq!(resolve(key(KeyCode::Char('k'))), Some(Action::ScrollUp));
158        assert_eq!(resolve(key(KeyCode::Up)), Some(Action::ScrollUp));
159        assert_eq!(resolve(key(KeyCode::Char('g'))), Some(Action::ScrollTop));
160        assert_eq!(resolve(key(KeyCode::Char('G'))), Some(Action::ScrollBottom));
161        assert_eq!(resolve(key(KeyCode::PageUp)), Some(Action::PageUp));
162        assert_eq!(resolve(key(KeyCode::PageDown)), Some(Action::PageDown));
163    }
164
165    #[test]
166    fn action_keys() {
167        assert_eq!(resolve(key(KeyCode::Enter)), Some(Action::Select));
168        assert_eq!(resolve(key(KeyCode::Esc)), Some(Action::Back));
169        assert_eq!(resolve(key(KeyCode::Char('f'))), Some(Action::ToggleFollow));
170        assert_eq!(resolve(key(KeyCode::Char('e'))), Some(Action::Edit));
171        assert_eq!(resolve(key(KeyCode::Char('t'))), Some(Action::Toggle));
172        assert_eq!(resolve(key(KeyCode::Char('d'))), Some(Action::Delete));
173        assert_eq!(resolve(key(KeyCode::Char('s'))), Some(Action::Sort));
174    }
175
176    #[test]
177    fn unrecognized_key_returns_none() {
178        assert_eq!(resolve(key(KeyCode::Char('z'))), None);
179        assert_eq!(resolve(key(KeyCode::F(1))), None);
180        assert_eq!(resolve(key(KeyCode::Insert)), None);
181    }
182
183    #[test]
184    fn ctrl_with_unrecognized_char_returns_none() {
185        assert_eq!(resolve(ctrl(KeyCode::Char('x'))), None);
186        assert_eq!(resolve(ctrl(KeyCode::Char('a'))), None);
187    }
188}