1use std::collections::HashMap;
8
9use ftui_core::event::{KeyCode, Modifiers, MouseEventKind};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum InputEvent {
17 Key(KeyCode, Modifiers),
19 Mouse(MouseEventKind, u16, u16),
21 Resize(u16, u16),
23 Action(KeyAction),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum KeyAction {
37 Quit,
40 TogglePalette,
42 NextScreen,
44 PrevScreen,
46 ToggleHelp,
48 CycleTheme,
50 Dismiss,
52
53 Up,
56 Down,
58 Left,
60 Right,
62 PageUp,
64 PageDown,
66 Home,
68 End,
70
71 Confirm,
74 Delete,
76 Copy,
78
79 Custom(String),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
88pub struct KeyBinding {
89 pub key: String,
91 pub modifiers: Vec<String>,
93 pub action: KeyAction,
95}
96
97pub struct Keymap {
101 bindings: HashMap<(KeyCode, Modifiers), KeyAction>,
102}
103
104impl Keymap {
105 #[must_use]
107 pub fn default_bindings() -> Self {
108 let mut bindings = HashMap::new();
109
110 bindings.insert((KeyCode::Char('q'), Modifiers::NONE), KeyAction::Quit);
112 bindings.insert((KeyCode::Char('c'), Modifiers::CTRL), KeyAction::Quit);
113
114 bindings.insert(
116 (KeyCode::Char('p'), Modifiers::CTRL),
117 KeyAction::TogglePalette,
118 );
119 bindings.insert(
120 (KeyCode::Char(':'), Modifiers::NONE),
121 KeyAction::TogglePalette,
122 );
123
124 bindings.insert((KeyCode::Tab, Modifiers::NONE), KeyAction::NextScreen);
126 bindings.insert((KeyCode::BackTab, Modifiers::SHIFT), KeyAction::PrevScreen);
127
128 bindings.insert((KeyCode::Char('?'), Modifiers::NONE), KeyAction::ToggleHelp);
130 bindings.insert((KeyCode::F(1), Modifiers::NONE), KeyAction::ToggleHelp);
131
132 bindings.insert((KeyCode::Escape, Modifiers::NONE), KeyAction::Dismiss);
134
135 bindings.insert((KeyCode::Up, Modifiers::NONE), KeyAction::Up);
137 bindings.insert((KeyCode::Down, Modifiers::NONE), KeyAction::Down);
138 bindings.insert((KeyCode::Left, Modifiers::NONE), KeyAction::Left);
139 bindings.insert((KeyCode::Right, Modifiers::NONE), KeyAction::Right);
140 bindings.insert((KeyCode::Char('k'), Modifiers::NONE), KeyAction::Up);
141 bindings.insert((KeyCode::Char('j'), Modifiers::NONE), KeyAction::Down);
142 bindings.insert((KeyCode::Char('h'), Modifiers::NONE), KeyAction::Left);
143 bindings.insert((KeyCode::Char('l'), Modifiers::NONE), KeyAction::Right);
144
145 bindings.insert((KeyCode::PageUp, Modifiers::NONE), KeyAction::PageUp);
147 bindings.insert((KeyCode::PageDown, Modifiers::NONE), KeyAction::PageDown);
148 bindings.insert((KeyCode::Home, Modifiers::NONE), KeyAction::Home);
149 bindings.insert((KeyCode::End, Modifiers::NONE), KeyAction::End);
150
151 bindings.insert((KeyCode::Char('t'), Modifiers::CTRL), KeyAction::CycleTheme);
153
154 bindings.insert((KeyCode::Enter, Modifiers::NONE), KeyAction::Confirm);
156 bindings.insert((KeyCode::Backspace, Modifiers::NONE), KeyAction::Delete);
157 bindings.insert((KeyCode::Char('y'), Modifiers::CTRL), KeyAction::Copy);
158
159 Self { bindings }
160 }
161
162 #[must_use]
164 pub fn resolve(&self, key: KeyCode, modifiers: Modifiers) -> Option<&KeyAction> {
165 self.bindings.get(&(key, modifiers))
166 }
167
168 pub fn bind(&mut self, key: KeyCode, modifiers: Modifiers, action: KeyAction) {
170 self.bindings.insert((key, modifiers), action);
171 }
172
173 pub fn unbind(&mut self, key: KeyCode, modifiers: Modifiers) {
175 self.bindings.remove(&(key, modifiers));
176 }
177
178 #[must_use]
180 pub fn len(&self) -> usize {
181 self.bindings.len()
182 }
183
184 #[must_use]
186 pub fn is_empty(&self) -> bool {
187 self.bindings.is_empty()
188 }
189}
190
191impl Default for Keymap {
192 fn default() -> Self {
193 Self::default_bindings()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use ftui_core::event::{KeyCode, Modifiers};
200
201 use super::*;
202
203 #[test]
204 fn default_keymap_has_bindings() {
205 let keymap = Keymap::default_bindings();
206 assert!(!keymap.is_empty());
207 assert!(keymap.len() > 15);
208 }
209
210 #[test]
211 fn resolve_quit_q() {
212 let keymap = Keymap::default_bindings();
213 let action = keymap.resolve(KeyCode::Char('q'), Modifiers::NONE);
214 assert_eq!(action, Some(&KeyAction::Quit));
215 }
216
217 #[test]
218 fn resolve_quit_ctrl_c() {
219 let keymap = Keymap::default_bindings();
220 let action = keymap.resolve(KeyCode::Char('c'), Modifiers::CTRL);
221 assert_eq!(action, Some(&KeyAction::Quit));
222 }
223
224 #[test]
225 fn resolve_palette_ctrl_p() {
226 let keymap = Keymap::default_bindings();
227 let action = keymap.resolve(KeyCode::Char('p'), Modifiers::CTRL);
228 assert_eq!(action, Some(&KeyAction::TogglePalette));
229 }
230
231 #[test]
232 fn resolve_vim_movement() {
233 let keymap = Keymap::default_bindings();
234 assert_eq!(
235 keymap.resolve(KeyCode::Char('j'), Modifiers::NONE),
236 Some(&KeyAction::Down)
237 );
238 assert_eq!(
239 keymap.resolve(KeyCode::Char('k'), Modifiers::NONE),
240 Some(&KeyAction::Up)
241 );
242 }
243
244 #[test]
245 fn resolve_unknown_returns_none() {
246 let keymap = Keymap::default_bindings();
247 assert!(
248 keymap
249 .resolve(KeyCode::Char('z'), Modifiers::NONE)
250 .is_none()
251 );
252 }
253
254 #[test]
255 fn custom_binding() {
256 let mut keymap = Keymap::default_bindings();
257 keymap.bind(
258 KeyCode::Char('s'),
259 Modifiers::CTRL,
260 KeyAction::Custom("save".to_string()),
261 );
262 let action = keymap.resolve(KeyCode::Char('s'), Modifiers::CTRL);
263 assert_eq!(action, Some(&KeyAction::Custom("save".to_string())));
264 }
265
266 #[test]
267 fn unbind_removes_binding() {
268 let mut keymap = Keymap::default_bindings();
269 assert!(
270 keymap
271 .resolve(KeyCode::Char('q'), Modifiers::NONE)
272 .is_some()
273 );
274 keymap.unbind(KeyCode::Char('q'), Modifiers::NONE);
275 assert!(
276 keymap
277 .resolve(KeyCode::Char('q'), Modifiers::NONE)
278 .is_none()
279 );
280 }
281
282 #[test]
283 fn key_action_serde_roundtrip() {
284 for action in [
285 KeyAction::Quit,
286 KeyAction::TogglePalette,
287 KeyAction::CycleTheme,
288 KeyAction::Up,
289 KeyAction::Custom("test".to_string()),
290 ] {
291 let json = serde_json::to_string(&action).unwrap();
292 let decoded: KeyAction = serde_json::from_str(&json).unwrap();
293 assert_eq!(decoded, action);
294 }
295 }
296}