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