1use hashbrown::HashMap;
2use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum Action {
12 Interrupt,
13 Exit,
14 BackgroundOperation,
15 OpenModelPicker,
16 ClearScreen,
17 ToggleMode,
18 ScrollPageUp,
19 ScrollPageDown,
20 EditQueue,
21 HistoryPrevious,
22 HistoryNext,
23 ToggleLogs,
24 GeneratePromptSuggestion,
25}
26
27impl Action {
28 pub fn name(self) -> &'static str {
30 match self {
31 Action::Interrupt => "interrupt",
32 Action::Exit => "exit",
33 Action::BackgroundOperation => "background_operation",
34 Action::OpenModelPicker => "open_model_picker",
35 Action::ClearScreen => "clear_screen",
36 Action::ToggleMode => "toggle_mode",
37 Action::ScrollPageUp => "scroll_page_up",
38 Action::ScrollPageDown => "scroll_page_down",
39 Action::EditQueue => "edit_queue",
40 Action::HistoryPrevious => "history_previous",
41 Action::HistoryNext => "history_next",
42 Action::ToggleLogs => "toggle_logs",
43 Action::GeneratePromptSuggestion => "generate_prompt_suggestion",
44 }
45 }
46
47 pub fn all() -> &'static [Action] {
49 &[
50 Action::Interrupt,
51 Action::Exit,
52 Action::BackgroundOperation,
53 Action::OpenModelPicker,
54 Action::ClearScreen,
55 Action::ToggleMode,
56 Action::ScrollPageUp,
57 Action::ScrollPageDown,
58 Action::EditQueue,
59 Action::HistoryPrevious,
60 Action::HistoryNext,
61 Action::ToggleLogs,
62 Action::GeneratePromptSuggestion,
63 ]
64 }
65
66 pub fn from_name(name: &str) -> Option<Self> {
68 Self::all().iter().find(|a| a.name() == name).copied()
69 }
70}
71
72pub fn parse_key_binding(s: &str) -> Option<(KeyCode, KeyModifiers)> {
79 let s = s.trim();
80 if s.is_empty() {
81 return None;
82 }
83
84 let parts: Vec<&str> = s.split('+').collect();
85 let (modifiers, key_part) = if parts.len() == 1 {
86 (KeyModifiers::empty(), parts[0])
87 } else {
88 let mut mods = KeyModifiers::empty();
89 for part in &parts[..parts.len() - 1] {
90 match *part {
91 "ctrl" | "control" => mods.insert(KeyModifiers::CONTROL),
92 "shift" => mods.insert(KeyModifiers::SHIFT),
93 "alt" | "option" => mods.insert(KeyModifiers::ALT),
94 "meta" => mods.insert(KeyModifiers::META),
95 "cmd" | "command" | "super" | "gui" | "win" => {
96 mods.insert(KeyModifiers::SUPER);
97 }
98 _ => return None,
99 }
100 }
101 (mods, parts[parts.len() - 1])
102 };
103
104 let code = match key_part {
105 "enter" => KeyCode::Enter,
106 "tab" => KeyCode::Tab,
107 "backtab" => KeyCode::BackTab,
108 "esc" | "escape" => KeyCode::Esc,
109 "backspace" => KeyCode::Backspace,
110 "delete" => KeyCode::Delete,
111 "space" => KeyCode::Char(' '),
112 "up" => KeyCode::Up,
113 "down" => KeyCode::Down,
114 "left" => KeyCode::Left,
115 "right" => KeyCode::Right,
116 "pageup" => KeyCode::PageUp,
117 "pagedown" => KeyCode::PageDown,
118 "home" => KeyCode::Home,
119 "end" => KeyCode::End,
120 "insert" => KeyCode::Insert,
121 "null" => KeyCode::Null,
122 "capslock" => KeyCode::CapsLock,
123 "scrolllock" => KeyCode::ScrollLock,
124 "numlock" => KeyCode::NumLock,
125 "printscreen" => KeyCode::PrintScreen,
126 "pause" => KeyCode::Pause,
127 "menu" => KeyCode::Menu,
128 name if name.starts_with('f') && name.len() > 1 => {
129 let n: u8 = name[1..].parse().ok()?;
130 match n {
131 1 => KeyCode::F(1),
132 2 => KeyCode::F(2),
133 3 => KeyCode::F(3),
134 4 => KeyCode::F(4),
135 5 => KeyCode::F(5),
136 6 => KeyCode::F(6),
137 7 => KeyCode::F(7),
138 8 => KeyCode::F(8),
139 9 => KeyCode::F(9),
140 10 => KeyCode::F(10),
141 11 => KeyCode::F(11),
142 12 => KeyCode::F(12),
143 _ => return None,
144 }
145 }
146 ch => {
147 let chars: Vec<char> = ch.chars().collect();
148 if chars.len() == 1 {
149 KeyCode::Char(chars[0])
150 } else {
151 return None;
152 }
153 }
154 };
155
156 Some((code, modifiers))
157}
158
159fn default_bindings() -> HashMap<Action, Vec<(KeyCode, KeyModifiers)>> {
161 use Action::*;
162 let mut m = HashMap::new();
163
164 m.insert(
165 Interrupt,
166 vec![
167 (KeyCode::Char('c'), KeyModifiers::CONTROL),
168 (KeyCode::Char('C'), KeyModifiers::CONTROL),
169 (KeyCode::Char('\u{3}'), KeyModifiers::empty()),
170 ],
171 );
172 m.insert(
173 Exit,
174 vec![
175 (KeyCode::Char('d'), KeyModifiers::CONTROL),
176 (KeyCode::Char('D'), KeyModifiers::CONTROL),
177 ],
178 );
179 m.insert(
180 BackgroundOperation,
181 vec![
182 (KeyCode::Char('b'), KeyModifiers::CONTROL),
183 (KeyCode::Char('B'), KeyModifiers::CONTROL),
184 ],
185 );
186 m.insert(
187 OpenModelPicker,
188 vec![
189 (KeyCode::Char('m'), KeyModifiers::CONTROL),
190 (KeyCode::Char('M'), KeyModifiers::CONTROL),
191 ],
192 );
193 m.insert(
194 ClearScreen,
195 vec![
196 (KeyCode::Char('l'), KeyModifiers::CONTROL),
197 (KeyCode::Char('L'), KeyModifiers::CONTROL),
198 ],
199 );
200 m.insert(ToggleMode, vec![(KeyCode::BackTab, KeyModifiers::empty())]);
201 m.insert(ScrollPageUp, vec![(KeyCode::PageUp, KeyModifiers::empty())]);
202 m.insert(
203 ScrollPageDown,
204 vec![(KeyCode::PageDown, KeyModifiers::empty())],
205 );
206
207 m.insert(
208 EditQueue,
209 vec![
210 (KeyCode::Up, KeyModifiers::ALT),
211 (KeyCode::Up, KeyModifiers::META),
212 ],
213 );
214
215 m.insert(HistoryPrevious, vec![(KeyCode::Up, KeyModifiers::empty())]);
216 m.insert(HistoryNext, vec![(KeyCode::Down, KeyModifiers::empty())]);
217
218 m.insert(
219 ToggleLogs,
220 vec![
221 (KeyCode::Char('t'), KeyModifiers::CONTROL),
222 (KeyCode::Char('T'), KeyModifiers::CONTROL),
223 ],
224 );
225 m.insert(
226 GeneratePromptSuggestion,
227 vec![
228 (KeyCode::Char('p'), KeyModifiers::ALT),
229 (KeyCode::Char('P'), KeyModifiers::ALT),
230 ],
231 );
232
233 m
234}
235
236#[derive(Debug, Clone)]
241pub struct BindingStore {
242 entries: Vec<(KeyCode, KeyModifiers, Action)>,
244}
245
246impl Default for BindingStore {
247 fn default() -> Self {
248 Self::defaults()
249 }
250}
251
252impl BindingStore {
253 pub fn new(overlay: HashMap<String, Vec<String>>) -> Self {
259 let mut merged: HashMap<Action, Vec<(KeyCode, KeyModifiers)>> = default_bindings();
260
261 for (action_name, key_specs) in overlay {
262 let Some(action) = Action::from_name(&action_name) else {
263 tracing::debug!(%action_name, "unknown action in keybinding overlay, skipping");
264 continue;
265 };
266
267 let parsed: Vec<(KeyCode, KeyModifiers)> = key_specs
268 .iter()
269 .filter_map(|s| parse_key_binding(s))
270 .collect();
271
272 if parsed.is_empty() {
273 merged.remove(&action);
275 } else {
276 merged.insert(action, parsed);
277 }
278 }
279
280 let mut entries = Vec::new();
281 for (action, keys) in &merged {
282 for &(code, mods) in keys {
283 entries.push((code, mods, *action));
284 }
285 }
286
287 Self { entries }
288 }
289
290 pub fn defaults() -> Self {
292 Self::new(HashMap::new())
293 }
294
295 pub fn resolve(&self, key: &KeyEvent) -> Option<Action> {
300 let mut best: Option<(usize, Action)> = None;
301
302 for (i, &(code, mods, action)) in self.entries.iter().enumerate() {
305 let code_match = match (code, key.code) {
306 (KeyCode::Char(bc), KeyCode::Char(kc)) if bc.eq_ignore_ascii_case(&kc) => true,
307 _ => code == key.code,
308 };
309
310 if !code_match {
311 continue;
312 }
313
314 if !key.modifiers.contains(mods) {
316 continue;
317 }
318
319 let char_shift_grace = if let KeyCode::Char(_) = key.code {
322 KeyModifiers::SHIFT
323 } else {
324 KeyModifiers::empty()
325 };
326 let extra = key.modifiers.difference(mods);
327 if extra.intersection(!char_shift_grace) != KeyModifiers::empty() {
328 continue;
329 }
330
331 best = match best {
334 None => Some((i, action)),
335 Some((bi, _)) if i < bi => Some((i, action)),
336 Some(other) => Some(other),
337 };
338 }
339
340 best.map(|(_, action)| action)
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use ratatui::crossterm::event::KeyEvent;
348
349 #[test]
350 fn test_parse_key_binding_simple() {
351 let (code, mods) = parse_key_binding("ctrl+c").unwrap();
352 assert_eq!(code, KeyCode::Char('c'));
353 assert!(mods.contains(KeyModifiers::CONTROL));
354 assert!(!mods.contains(KeyModifiers::SHIFT));
355 }
356
357 #[test]
358 fn test_parse_key_binding_modifier_combos() {
359 let (code, mods) = parse_key_binding("ctrl+shift+enter").unwrap();
360 assert_eq!(code, KeyCode::Enter);
361 assert!(mods.contains(KeyModifiers::CONTROL));
362 assert!(mods.contains(KeyModifiers::SHIFT));
363 }
364
365 #[test]
366 fn test_parse_key_binding_func_keys() {
367 let (code, _) = parse_key_binding("f5").unwrap();
368 assert_eq!(code, KeyCode::F(5));
369 }
370
371 #[test]
372 fn test_parse_key_binding_special() {
373 let (code, _) = parse_key_binding("pageup").unwrap();
374 assert_eq!(code, KeyCode::PageUp);
375 let (code, _) = parse_key_binding("backtab").unwrap();
376 assert_eq!(code, KeyCode::BackTab);
377 }
378
379 #[test]
380 fn test_default_bindings_resolve() {
381 let store = BindingStore::defaults();
382
383 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
385 assert_eq!(store.resolve(&key), Some(Action::Interrupt));
386
387 let key = KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty());
389 assert_eq!(store.resolve(&key), Some(Action::ScrollPageUp));
390
391 let key = KeyEvent::new(KeyCode::Up, KeyModifiers::ALT);
393 assert_eq!(store.resolve(&key), Some(Action::EditQueue));
394
395 let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL);
397 assert_eq!(store.resolve(&key), None);
398 }
399
400 #[test]
401 fn test_default_bindings_case_insensitive() {
402 let store = BindingStore::defaults();
403
404 let key = KeyEvent::new(KeyCode::Char('C'), KeyModifiers::CONTROL);
406 assert_eq!(store.resolve(&key), Some(Action::Interrupt));
407 }
408
409 #[test]
410 fn test_user_overlay_overrides_default() {
411 let mut overlay = HashMap::new();
412 overlay.insert("interrupt".to_string(), vec!["ctrl+x".to_string()]);
413 let store = BindingStore::new(overlay);
414
415 let key_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
417 assert_eq!(store.resolve(&key_c), None);
418
419 let key_x = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL);
421 assert_eq!(store.resolve(&key_x), Some(Action::Interrupt));
422 }
423
424 #[test]
425 fn test_user_overlay_unbind() {
426 let mut overlay = HashMap::new();
427 overlay.insert("interrupt".to_string(), Vec::new());
428 let store = BindingStore::new(overlay);
429
430 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
431 assert_eq!(store.resolve(&key), None);
432 }
433
434 #[test]
435 fn test_parse_invalid_key() {
436 assert!(parse_key_binding("").is_none());
437 assert!(parse_key_binding("invalid_key_name").is_none());
438 assert!(parse_key_binding("ctrl+invalid").is_none());
439 assert!(parse_key_binding("+ctrl+c").is_none());
440 }
441
442 #[test]
443 fn test_action_name_roundtrip() {
444 for action in Action::all() {
445 let name = action.name();
446 let parsed = Action::from_name(name);
447 assert_eq!(parsed, Some(*action));
448 }
449 }
450
451 #[test]
452 fn test_action_from_name_unknown() {
453 assert_eq!(Action::from_name("nonexistent"), None);
454 }
455}