1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use crossterm::event::KeyEvent;
5
6use crate::tui::keys::match_key_id;
7
8pub const ACTION_EDITOR_CURSOR_LEFT: &str = "tui.editor.cursorLeft";
14pub const ACTION_EDITOR_CURSOR_RIGHT: &str = "tui.editor.cursorRight";
15pub const ACTION_EDITOR_CURSOR_UP: &str = "tui.editor.cursorUp";
16pub const ACTION_EDITOR_CURSOR_DOWN: &str = "tui.editor.cursorDown";
17pub const ACTION_EDITOR_CURSOR_LINE_START: &str = "tui.editor.cursorLineStart";
18pub const ACTION_EDITOR_CURSOR_LINE_END: &str = "tui.editor.cursorLineEnd";
19pub const ACTION_EDITOR_CURSOR_WORD_LEFT: &str = "tui.editor.cursorWordLeft";
20pub const ACTION_EDITOR_CURSOR_WORD_RIGHT: &str = "tui.editor.cursorWordRight";
21pub const ACTION_EDITOR_DELETE_CHAR_BACKWARD: &str = "tui.editor.deleteCharBackward";
22pub const ACTION_EDITOR_DELETE_CHAR_FORWARD: &str = "tui.editor.deleteCharForward";
23pub const ACTION_EDITOR_DELETE_WORD_BACKWARD: &str = "tui.editor.deleteWordBackward";
24pub const ACTION_EDITOR_DELETE_WORD_FORWARD: &str = "tui.editor.deleteWordForward";
25pub const ACTION_EDITOR_DELETE_TO_LINE_START: &str = "tui.editor.deleteToLineStart";
26pub const ACTION_EDITOR_DELETE_TO_LINE_END: &str = "tui.editor.deleteToLineEnd";
27pub const ACTION_EDITOR_YANK: &str = "tui.editor.yank";
28pub const ACTION_EDITOR_YANK_POP: &str = "tui.editor.yankPop";
29pub const ACTION_EDITOR_UNDO: &str = "tui.editor.undo";
30pub const ACTION_EDITOR_PAGE_UP: &str = "tui.editor.pageUp";
31pub const ACTION_EDITOR_PAGE_DOWN: &str = "tui.editor.pageDown";
32pub const ACTION_EDITOR_JUMP_FORWARD: &str = "tui.editor.jumpForward";
33pub const ACTION_EDITOR_JUMP_BACKWARD: &str = "tui.editor.jumpBackward";
34
35pub const ACTION_INPUT_SUBMIT: &str = "tui.input.submit";
37pub const ACTION_INPUT_TAB: &str = "tui.input.tab";
38pub const ACTION_INPUT_NEW_LINE: &str = "tui.input.newLine";
39pub const ACTION_INPUT_COPY: &str = "tui.input.copy";
40
41pub const ACTION_SELECT_UP: &str = "tui.select.up";
43pub const ACTION_SELECT_DOWN: &str = "tui.select.down";
44pub const ACTION_SELECT_CONFIRM: &str = "tui.select.confirm";
45pub const ACTION_SELECT_CANCEL: &str = "tui.select.cancel";
46
47pub const ACTION_APP_ESCAPE: &str = "app.escape";
49pub const ACTION_APP_CLEAR: &str = "app.clear";
50pub const ACTION_APP_INTERRUPT: &str = "app.interrupt";
51pub const ACTION_APP_EXIT: &str = "app.exit";
52pub const ACTION_APP_SUSPEND: &str = "app.suspend";
53pub const ACTION_APP_THINKING_CYCLE: &str = "app.thinking.cycle";
54pub const ACTION_APP_MODEL_SELECTOR: &str = "app.model.select";
55pub const ACTION_APP_MODEL_CYCLE_FORWARD: &str = "app.model.cycleForward";
56pub const ACTION_APP_MODEL_CYCLE_BACKWARD: &str = "app.model.cycleBackward";
57pub const ACTION_APP_TOGGLE_THINKING: &str = "app.thinking.toggle";
58pub const ACTION_APP_TOOLS_EXPAND: &str = "app.tools.expand";
59pub const ACTION_APP_EDITOR_EXTERNAL: &str = "app.editor.external";
60pub const ACTION_APP_HELP: &str = "app.help";
61pub const ACTION_APP_HISTORY_UP: &str = "app.historyUp";
62pub const ACTION_APP_HISTORY_DOWN: &str = "app.historyDown";
63pub const ACTION_APP_MESSAGE_FOLLOW_UP: &str = "app.message.followUp";
64pub const ACTION_APP_MESSAGE_DEQUEUE: &str = "app.message.dequeue";
65pub const ACTION_APP_COMPACT_TOGGLE: &str = "app.compact.toggle";
66pub const ACTION_APP_SESSION_NEW: &str = "app.session.new";
67pub const ACTION_APP_SESSION_TREE: &str = "app.session.tree";
68pub const ACTION_APP_SESSION_FORK: &str = "app.session.fork";
69pub const ACTION_APP_SESSION_RESUME: &str = "app.session.resume";
70
71#[derive(Debug, Clone)]
77pub struct Keybindings {
78 bindings: HashMap<String, Vec<String>>,
79}
80
81impl Keybindings {
82 pub fn new() -> Self {
83 Self {
84 bindings: HashMap::new(),
85 }
86 }
87
88 pub fn with_defaults() -> Self {
90 let mut kb = Self::new();
91 kb.set_defaults();
92 kb
93 }
94
95 fn set_defaults(&mut self) {
96 self.set(
97 ACTION_EDITOR_CURSOR_LEFT,
98 vec!["left".into(), "ctrl+b".into()],
99 );
100 self.set(
101 ACTION_EDITOR_CURSOR_RIGHT,
102 vec!["right".into(), "ctrl+f".into()],
103 );
104 self.set(ACTION_EDITOR_CURSOR_UP, vec!["up".into()]);
105 self.set(ACTION_EDITOR_CURSOR_DOWN, vec!["down".into()]);
106 self.set(
107 ACTION_EDITOR_CURSOR_LINE_START,
108 vec!["home".into(), "ctrl+a".into()],
109 );
110 self.set(
111 ACTION_EDITOR_CURSOR_LINE_END,
112 vec!["end".into(), "ctrl+e".into()],
113 );
114 self.set(
115 ACTION_EDITOR_CURSOR_WORD_LEFT,
116 vec!["ctrl+left".into(), "alt+b".into()],
117 );
118 self.set(
119 ACTION_EDITOR_CURSOR_WORD_RIGHT,
120 vec!["ctrl+right".into(), "alt+f".into()],
121 );
122 self.set(
123 ACTION_EDITOR_DELETE_CHAR_BACKWARD,
124 vec!["backspace".into(), "ctrl+h".into()],
125 );
126 self.set(
127 ACTION_EDITOR_DELETE_CHAR_FORWARD,
128 vec!["delete".into(), "ctrl+d".into()],
129 );
130 self.set(ACTION_EDITOR_DELETE_WORD_BACKWARD, vec!["ctrl+w".into()]);
131 self.set(ACTION_EDITOR_DELETE_WORD_FORWARD, vec!["alt+d".into()]);
132 self.set(ACTION_EDITOR_DELETE_TO_LINE_START, vec!["ctrl+u".into()]);
133 self.set(ACTION_EDITOR_DELETE_TO_LINE_END, vec!["ctrl+k".into()]);
134 self.set(ACTION_EDITOR_YANK, vec!["ctrl+y".into()]);
135 self.set(ACTION_EDITOR_YANK_POP, vec!["alt+y".into()]);
136 self.set(ACTION_EDITOR_UNDO, vec!["ctrl+z".into()]);
137 self.set(ACTION_EDITOR_PAGE_UP, vec!["pageUp".into()]);
138 self.set(ACTION_EDITOR_PAGE_DOWN, vec!["pageDown".into()]);
139 self.set(ACTION_EDITOR_JUMP_FORWARD, vec!["alt+f".into()]);
140 self.set(ACTION_EDITOR_JUMP_BACKWARD, vec!["alt+b".into()]);
141
142 self.set(ACTION_INPUT_SUBMIT, vec!["enter".into()]);
143 self.set(ACTION_INPUT_TAB, vec!["tab".into()]);
144 self.set(ACTION_INPUT_NEW_LINE, vec!["ctrl+j".into()]);
145 self.set(ACTION_INPUT_COPY, vec!["ctrl+c".into()]);
146
147 self.set(ACTION_SELECT_UP, vec!["up".into()]);
148 self.set(ACTION_SELECT_DOWN, vec!["down".into()]);
149 self.set(ACTION_SELECT_CONFIRM, vec!["enter".into()]);
150 self.set(ACTION_SELECT_CANCEL, vec!["escape".into()]);
151
152 self.set(ACTION_APP_ESCAPE, vec!["escape".into()]);
153 self.set(ACTION_APP_CLEAR, vec!["ctrl+c".into()]);
154 self.set(ACTION_APP_INTERRUPT, vec!["escape".into()]);
155 self.set(ACTION_APP_EXIT, vec!["ctrl+d".into()]);
156 self.set(ACTION_APP_SUSPEND, vec!["ctrl+z".into()]);
157 self.set(ACTION_APP_THINKING_CYCLE, vec!["shift+tab".into()]);
158 self.set(ACTION_APP_MODEL_SELECTOR, vec!["ctrl+l".into()]);
159 self.set(ACTION_APP_MODEL_CYCLE_FORWARD, vec!["ctrl+p".into()]);
160 self.set(ACTION_APP_MODEL_CYCLE_BACKWARD, vec!["ctrl+shift+p".into()]);
161 self.set(ACTION_APP_TOGGLE_THINKING, vec!["ctrl+t".into()]);
162 self.set(ACTION_APP_TOOLS_EXPAND, vec!["ctrl+o".into()]);
163 self.set(ACTION_APP_EDITOR_EXTERNAL, vec!["ctrl+g".into()]);
164 self.set(ACTION_APP_HELP, vec!["f1".into()]);
165 self.set(ACTION_APP_HISTORY_UP, vec!["up".into()]);
166 self.set(ACTION_APP_HISTORY_DOWN, vec!["down".into()]);
167 self.set(ACTION_APP_MESSAGE_FOLLOW_UP, vec!["alt+enter".into()]);
168 self.set(ACTION_APP_MESSAGE_DEQUEUE, vec!["alt+up".into()]);
169 self.set(ACTION_APP_COMPACT_TOGGLE, vec!["ctrl+shift+c".into()]);
170 self.set(ACTION_APP_SESSION_NEW, vec![]);
172 self.set(ACTION_APP_SESSION_TREE, vec![]);
173 self.set(ACTION_APP_SESSION_FORK, vec![]);
174 self.set(ACTION_APP_SESSION_RESUME, vec![]);
175 }
176
177 pub fn set(&mut self, action: &str, keys: Vec<String>) {
179 self.bindings.insert(action.to_string(), keys);
180 }
181
182 pub fn merge(&mut self, other: Keybindings) {
184 for (action, keys) in other.bindings {
185 self.bindings.insert(action, keys);
186 }
187 }
188
189 pub fn matches(&self, event: &KeyEvent, action_id: &str) -> bool {
191 if let Some(keys) = self.bindings.get(action_id) {
192 for key_id in keys {
193 if match_key_id(event, key_id) {
194 return true;
195 }
196 }
197 }
198 false
199 }
200
201 pub fn get_keys(&self, action_id: &str) -> &[String] {
203 self.bindings
204 .get(action_id)
205 .map(|v| v.as_slice())
206 .unwrap_or(&[])
207 }
208
209 pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
211 let content = std::fs::read_to_string(path)?;
212 let bindings: HashMap<String, Vec<String>> = serde_json::from_str(&content)
213 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
214 Ok(Self { bindings })
215 }
216
217 pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
219 let content = serde_json::to_string_pretty(&self.bindings)
220 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
221 std::fs::write(path, content)
222 }
223}
224
225impl Default for Keybindings {
226 fn default() -> Self {
227 Self::with_defaults()
228 }
229}
230
231static GLOBAL_KEYBINDINGS: OnceLock<Keybindings> = OnceLock::new();
236
237pub fn get_keybindings() -> &'static Keybindings {
239 GLOBAL_KEYBINDINGS.get_or_init(Keybindings::with_defaults)
240}
241
242pub fn init_keybindings(kb: Keybindings) {
244 let _ = GLOBAL_KEYBINDINGS.set(kb);
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
251
252 #[test]
253 fn test_defaults_loaded() {
254 let kb = get_keybindings();
255 assert!(kb.matches(
256 &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
257 ACTION_INPUT_COPY,
258 ));
259 assert!(!kb.matches(
260 &KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
261 ACTION_INPUT_COPY,
262 ));
263 }
264
265 #[test]
266 fn test_editor_undo() {
267 let kb = get_keybindings();
268 assert!(kb.matches(
269 &KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL),
270 ACTION_EDITOR_UNDO,
271 ));
272 }
273
274 #[test]
275 fn test_select_up_down() {
276 let kb = get_keybindings();
277 assert!(kb.matches(
278 &KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
279 ACTION_SELECT_UP
280 ));
281 assert!(kb.matches(
282 &KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
283 ACTION_SELECT_DOWN
284 ));
285 assert!(kb.matches(
286 &KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
287 ACTION_SELECT_CONFIRM
288 ));
289 assert!(kb.matches(
290 &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
291 ACTION_SELECT_CANCEL
292 ));
293 }
294
295 #[test]
296 fn test_delete_word_backward() {
297 let kb = get_keybindings();
298 assert!(kb.matches(
299 &KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL),
300 ACTION_EDITOR_DELETE_WORD_BACKWARD,
301 ));
302 }
303
304 #[test]
305 fn test_app_clear() {
306 let kb = get_keybindings();
307 assert!(kb.matches(
308 &KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
309 ACTION_APP_CLEAR,
310 ));
311 assert!(!kb.matches(
312 &KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
313 ACTION_APP_CLEAR,
314 ));
315 }
316
317 #[test]
318 fn test_app_suspend() {
319 let kb = get_keybindings();
320 assert!(kb.matches(
321 &KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL),
322 ACTION_APP_SUSPEND,
323 ));
324 }
325
326 #[test]
327 fn test_app_thinking_cycle() {
328 let kb = get_keybindings();
329 assert!(kb.matches(
330 &KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE),
331 ACTION_APP_THINKING_CYCLE,
332 ));
333 assert!(kb.matches(
334 &KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT),
335 ACTION_APP_THINKING_CYCLE,
336 ));
337 }
338
339 #[test]
340 fn test_app_model_cycle() {
341 let kb = get_keybindings();
342 assert!(kb.matches(
343 &KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL),
344 ACTION_APP_MODEL_CYCLE_FORWARD,
345 ));
346 assert!(kb.matches(
347 &KeyEvent::new(
348 KeyCode::Char('p'),
349 KeyModifiers::CONTROL | KeyModifiers::SHIFT
350 ),
351 ACTION_APP_MODEL_CYCLE_BACKWARD,
352 ));
353 }
354
355 #[test]
356 fn test_app_tools_expand() {
357 let kb = get_keybindings();
358 assert!(kb.matches(
359 &KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL),
360 ACTION_APP_TOOLS_EXPAND,
361 ));
362 }
363
364 #[test]
365 fn test_app_editor_external() {
366 let kb = get_keybindings();
367 assert!(kb.matches(
368 &KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL),
369 ACTION_APP_EDITOR_EXTERNAL,
370 ));
371 }
372
373 #[test]
374 fn test_app_follow_up_dequeue() {
375 let kb = get_keybindings();
376 assert!(kb.matches(
377 &KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT),
378 ACTION_APP_MESSAGE_FOLLOW_UP,
379 ));
380 assert!(kb.matches(
381 &KeyEvent::new(KeyCode::Up, KeyModifiers::ALT),
382 ACTION_APP_MESSAGE_DEQUEUE,
383 ));
384 }
385
386 #[test]
387 fn test_cursor_word_left() {
388 let kb = get_keybindings();
389 assert!(kb.matches(
390 &KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL),
391 ACTION_EDITOR_CURSOR_WORD_LEFT,
392 ));
393 assert!(kb.matches(
394 &KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT),
395 ACTION_EDITOR_CURSOR_WORD_LEFT,
396 ));
397 }
398}