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