Skip to main content

a3s_tui/
keymap.rs

1//! Configurable key binding system for mapping keys to actions.
2
3use crate::event::KeyEvent;
4use crossterm::event::{KeyCode, KeyModifiers};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct KeyBinding {
9    pub code: KeyCode,
10    pub modifiers: KeyModifiers,
11}
12
13impl KeyBinding {
14    pub fn new(code: KeyCode) -> Self {
15        Self {
16            code,
17            modifiers: KeyModifiers::NONE,
18        }
19    }
20
21    pub fn ctrl(code: KeyCode) -> Self {
22        Self {
23            code,
24            modifiers: KeyModifiers::CONTROL,
25        }
26    }
27
28    pub fn alt(code: KeyCode) -> Self {
29        Self {
30            code,
31            modifiers: KeyModifiers::ALT,
32        }
33    }
34
35    pub fn shift(code: KeyCode) -> Self {
36        Self {
37            code,
38            modifiers: KeyModifiers::SHIFT,
39        }
40    }
41
42    pub fn with_modifiers(code: KeyCode, modifiers: KeyModifiers) -> Self {
43        Self { code, modifiers }
44    }
45
46    pub fn matches(&self, event: &KeyEvent) -> bool {
47        self.code == event.code && self.modifiers == event.modifiers
48    }
49
50    pub fn display(&self) -> String {
51        let mut parts = Vec::new();
52        if self.modifiers.contains(KeyModifiers::CONTROL) {
53            parts.push("Ctrl".to_string());
54        }
55        if self.modifiers.contains(KeyModifiers::ALT) {
56            parts.push("Alt".to_string());
57        }
58        if self.modifiers.contains(KeyModifiers::SHIFT) {
59            parts.push("Shift".to_string());
60        }
61        parts.push(key_name(self.code));
62        parts.join("+")
63    }
64}
65
66impl From<&KeyEvent> for KeyBinding {
67    fn from(event: &KeyEvent) -> Self {
68        Self {
69            code: event.code,
70            modifiers: event.modifiers,
71        }
72    }
73}
74
75pub struct Keymap<A: Clone> {
76    bindings: HashMap<KeyBinding, (A, String)>,
77}
78
79impl<A: Clone> Keymap<A> {
80    pub fn new() -> Self {
81        Self {
82            bindings: HashMap::new(),
83        }
84    }
85
86    pub fn bind(mut self, key: KeyBinding, action: A, description: &str) -> Self {
87        self.bindings.insert(key, (action, description.to_string()));
88        self
89    }
90
91    pub fn register(&mut self, key: KeyBinding, action: A, description: &str) {
92        self.bindings.insert(key, (action, description.to_string()));
93    }
94
95    pub fn unbind(&mut self, key: &KeyBinding) {
96        self.bindings.remove(key);
97    }
98
99    pub fn resolve(&self, event: &KeyEvent) -> Option<A> {
100        let binding = KeyBinding::from(event);
101        self.bindings
102            .get(&binding)
103            .map(|(action, _)| action.clone())
104    }
105
106    pub fn help(&self) -> Vec<(String, String)> {
107        self.bindings
108            .iter()
109            .map(|(key, (_, desc))| (key.display(), desc.clone()))
110            .collect()
111    }
112}
113
114impl<A: Clone> Default for Keymap<A> {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120fn key_name(code: KeyCode) -> String {
121    match code {
122        KeyCode::Char(c) => c.to_string(),
123        KeyCode::Enter => "Enter".to_string(),
124        KeyCode::Esc => "Esc".to_string(),
125        KeyCode::Backspace => "Backspace".to_string(),
126        KeyCode::Tab => "Tab".to_string(),
127        KeyCode::Delete => "Delete".to_string(),
128        KeyCode::Up => "Up".to_string(),
129        KeyCode::Down => "Down".to_string(),
130        KeyCode::Left => "Left".to_string(),
131        KeyCode::Right => "Right".to_string(),
132        KeyCode::Home => "Home".to_string(),
133        KeyCode::End => "End".to_string(),
134        KeyCode::PageUp => "PageUp".to_string(),
135        KeyCode::PageDown => "PageDown".to_string(),
136        KeyCode::F(n) => format!("F{}", n),
137        _ => "?".to_string(),
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[derive(Clone, Debug, PartialEq)]
146    enum Action {
147        Quit,
148        Save,
149        Help,
150    }
151
152    #[test]
153    fn keybinding_matches_event() {
154        let binding = KeyBinding::new(KeyCode::Char('q'));
155        let event = KeyEvent {
156            code: KeyCode::Char('q'),
157            modifiers: KeyModifiers::NONE,
158        };
159        assert!(binding.matches(&event));
160    }
161
162    #[test]
163    fn keybinding_ctrl_modifier() {
164        let binding = KeyBinding::ctrl(KeyCode::Char('c'));
165        let event = KeyEvent {
166            code: KeyCode::Char('c'),
167            modifiers: KeyModifiers::CONTROL,
168        };
169        assert!(binding.matches(&event));
170
171        let plain = KeyEvent {
172            code: KeyCode::Char('c'),
173            modifiers: KeyModifiers::NONE,
174        };
175        assert!(!binding.matches(&plain));
176    }
177
178    #[test]
179    fn keybinding_display() {
180        assert_eq!(KeyBinding::new(KeyCode::Char('q')).display(), "q");
181        assert_eq!(KeyBinding::ctrl(KeyCode::Char('c')).display(), "Ctrl+c");
182        assert_eq!(KeyBinding::alt(KeyCode::Char('x')).display(), "Alt+x");
183        assert_eq!(KeyBinding::new(KeyCode::F(1)).display(), "F1");
184    }
185
186    #[test]
187    fn keymap_resolve() {
188        let keymap = Keymap::new()
189            .bind(KeyBinding::new(KeyCode::Char('q')), Action::Quit, "Quit")
190            .bind(KeyBinding::ctrl(KeyCode::Char('s')), Action::Save, "Save");
191
192        let event = KeyEvent {
193            code: KeyCode::Char('q'),
194            modifiers: KeyModifiers::NONE,
195        };
196        assert_eq!(keymap.resolve(&event), Some(Action::Quit));
197
198        let event2 = KeyEvent {
199            code: KeyCode::Char('s'),
200            modifiers: KeyModifiers::CONTROL,
201        };
202        assert_eq!(keymap.resolve(&event2), Some(Action::Save));
203    }
204
205    #[test]
206    fn keymap_resolve_unbound_returns_none() {
207        let keymap: Keymap<Action> = Keymap::new();
208        let event = KeyEvent {
209            code: KeyCode::Char('z'),
210            modifiers: KeyModifiers::NONE,
211        };
212        assert_eq!(keymap.resolve(&event), None);
213    }
214
215    #[test]
216    fn keymap_unbind() {
217        let mut keymap =
218            Keymap::new().bind(KeyBinding::new(KeyCode::Char('q')), Action::Quit, "Quit");
219        keymap.unbind(&KeyBinding::new(KeyCode::Char('q')));
220        let event = KeyEvent {
221            code: KeyCode::Char('q'),
222            modifiers: KeyModifiers::NONE,
223        };
224        assert_eq!(keymap.resolve(&event), None);
225    }
226
227    #[test]
228    fn keymap_help_lists_bindings() {
229        let keymap = Keymap::new()
230            .bind(
231                KeyBinding::new(KeyCode::Char('q')),
232                Action::Quit,
233                "Quit app",
234            )
235            .bind(
236                KeyBinding::new(KeyCode::Char('?')),
237                Action::Help,
238                "Show help",
239            );
240        let help = keymap.help();
241        assert_eq!(help.len(), 2);
242    }
243}