1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::fmt;
9use std::path::Path;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum KeyAction {
14 CursorUp,
16 CursorDown,
17 CursorLeft,
18 CursorRight,
19 CursorWordLeft,
20 CursorWordRight,
21 CursorLineStart,
22 CursorLineEnd,
23 JumpForward,
24 JumpBackward,
25 PageUp,
26 PageDown,
27 DeleteCharBackward,
28 DeleteCharForward,
29 DeleteWordBackward,
30 DeleteWordForward,
31 DeleteToLineStart,
32 DeleteToLineEnd,
33 Yank,
34 YankPop,
35 Undo,
36 NewLine,
37
38 Submit,
40 Tab,
41 Copy,
42 SelectUp,
43 SelectDown,
44 SelectPageUp,
45 SelectPageDown,
46 SelectConfirm,
47 SelectCancel,
48 Interrupt,
49
50 Clear,
52 Exit,
53 Suspend,
54 CycleThinkingLevel,
55 CycleModelForward,
56 CycleModelBackward,
57 SelectModel,
58 ExpandTools,
59 ToggleThinking,
60 ToggleSessionNamedFilter,
61 ExternalEditor,
62 FollowUp,
63 Dequeue,
64 PasteImage,
65 NewSession,
66 Tree,
67 Fork,
68 Resume,
69 TreeFoldOrUp,
70 TreeUnfoldOrDown,
71 TreeEditLabel,
72 TreeToggleLabelTimestamp,
73 ToggleSessionPath,
74 ToggleSessionSort,
75 RenameSession,
76 DeleteSession,
77 DeleteSessionNoninvasive,
78 SaveModelSelection,
79 EnableAllModels,
80 ClearAllModels,
81 ToggleProvider,
82 ReorderUp,
83 ReorderDown,
84 TreeFilterDefault,
85 TreeFilterNoTools,
86 TreeFilterUserOnly,
87 TreeFilterLabeledOnly,
88 TreeFilterAll,
89 TreeFilterCycleForward,
90 TreeFilterCycleBackward,
91 ToggleRawMode,
92
93 Custom(String),
95}
96
97impl fmt::Display for KeyAction {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 KeyAction::Custom(name) => write!(f, "{}", name),
101 _ => write!(f, "{:?}", self),
102 }
103 }
104}
105
106impl From<&str> for KeyAction {
107 fn from(s: &str) -> Self {
108 match s {
109 "tui.editor.cursorUp" => KeyAction::CursorUp,
111 "tui.editor.cursorDown" => KeyAction::CursorDown,
112 "tui.editor.cursorLeft" => KeyAction::CursorLeft,
113 "tui.editor.cursorRight" => KeyAction::CursorRight,
114 "tui.editor.cursorWordLeft" => KeyAction::CursorWordLeft,
115 "tui.editor.cursorWordRight" => KeyAction::CursorWordRight,
116 "tui.editor.cursorLineStart" => KeyAction::CursorLineStart,
117 "tui.editor.cursorLineEnd" => KeyAction::CursorLineEnd,
118 "tui.editor.jumpForward" => KeyAction::JumpForward,
119 "tui.editor.jumpBackward" => KeyAction::JumpBackward,
120 "tui.editor.pageUp" => KeyAction::PageUp,
121 "tui.editor.pageDown" => KeyAction::PageDown,
122 "tui.editor.deleteCharBackward" => KeyAction::DeleteCharBackward,
123 "tui.editor.deleteCharForward" => KeyAction::DeleteCharForward,
124 "tui.editor.deleteWordBackward" => KeyAction::DeleteWordBackward,
125 "tui.editor.deleteWordForward" => KeyAction::DeleteWordForward,
126 "tui.editor.deleteToLineStart" => KeyAction::DeleteToLineStart,
127 "tui.editor.deleteToLineEnd" => KeyAction::DeleteToLineEnd,
128 "tui.editor.yank" => KeyAction::Yank,
129 "tui.editor.yankPop" => KeyAction::YankPop,
130 "tui.editor.undo" => KeyAction::Undo,
131 "tui.input.newLine" => KeyAction::NewLine,
132 "tui.input.submit" => KeyAction::Submit,
133 "tui.input.tab" => KeyAction::Tab,
134 "tui.input.copy" => KeyAction::Copy,
135 "tui.select.up" => KeyAction::SelectUp,
136 "tui.select.down" => KeyAction::SelectDown,
137 "tui.select.pageUp" => KeyAction::SelectPageUp,
138 "tui.select.pageDown" => KeyAction::SelectPageDown,
139 "tui.select.confirm" => KeyAction::SelectConfirm,
140 "tui.select.cancel" => KeyAction::SelectCancel,
141
142 "app.interrupt" => KeyAction::Interrupt,
144 "app.clear" => KeyAction::Clear,
145 "app.exit" => KeyAction::Exit,
146 "app.suspend" => KeyAction::Suspend,
147 "app.thinking.cycle" => KeyAction::CycleThinkingLevel,
148 "app.model.cycleForward" => KeyAction::CycleModelForward,
149 "app.model.cycleBackward" => KeyAction::CycleModelBackward,
150 "app.model.select" => KeyAction::SelectModel,
151 "app.tools.expand" => KeyAction::ExpandTools,
152 "app.thinking.toggle" => KeyAction::ToggleThinking,
153 "app.session.toggleNamedFilter" => KeyAction::ToggleSessionNamedFilter,
154 "app.editor.external" => KeyAction::ExternalEditor,
155 "app.message.followUp" => KeyAction::FollowUp,
156 "app.message.dequeue" => KeyAction::Dequeue,
157 "app.clipboard.pasteImage" => KeyAction::PasteImage,
158 "app.session.new" => KeyAction::NewSession,
159 "app.session.tree" => KeyAction::Tree,
160 "app.session.fork" => KeyAction::Fork,
161 "app.session.resume" => KeyAction::Resume,
162 "app.tree.foldOrUp" => KeyAction::TreeFoldOrUp,
163 "app.tree.unfoldOrDown" => KeyAction::TreeUnfoldOrDown,
164 "app.tree.editLabel" => KeyAction::TreeEditLabel,
165 "app.tree.toggleLabelTimestamp" => KeyAction::TreeToggleLabelTimestamp,
166 "app.session.togglePath" => KeyAction::ToggleSessionPath,
167 "app.session.toggleSort" => KeyAction::ToggleSessionSort,
168 "app.session.rename" => KeyAction::RenameSession,
169 "app.session.delete" => KeyAction::DeleteSession,
170 "app.session.deleteNoninvasive" => KeyAction::DeleteSessionNoninvasive,
171 "app.models.save" => KeyAction::SaveModelSelection,
172 "app.models.enableAll" => KeyAction::EnableAllModels,
173 "app.models.clearAll" => KeyAction::ClearAllModels,
174 "app.models.toggleProvider" => KeyAction::ToggleProvider,
175 "app.models.reorderUp" => KeyAction::ReorderUp,
176 "app.models.reorderDown" => KeyAction::ReorderDown,
177 "app.tree.filter.default" => KeyAction::TreeFilterDefault,
178 "app.tree.filter.noTools" => KeyAction::TreeFilterNoTools,
179 "app.tree.filter.userOnly" => KeyAction::TreeFilterUserOnly,
180 "app.tree.filter.labeledOnly" => KeyAction::TreeFilterLabeledOnly,
181 "app.tree.filter.all" => KeyAction::TreeFilterAll,
182 "app.tree.filter.cycleForward" => KeyAction::TreeFilterCycleForward,
183 "app.tree.filter.cycleBackward" => KeyAction::TreeFilterCycleBackward,
184
185 "submit" => KeyAction::Submit,
187 "cancel" => KeyAction::SelectCancel,
188 "historyUp" | "history_up" => KeyAction::SelectUp,
189 "historyDown" | "history_down" => KeyAction::SelectDown,
190
191 _ => KeyAction::Custom(s.to_string()),
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct KeyBinding {
200 pub action: String,
202 pub default_keys: Vec<String>,
204 pub description: String,
206}
207
208impl KeyBinding {
209 pub fn new(action: &str, default_keys: Vec<&str>, description: &str) -> Self {
210 Self {
211 action: action.to_string(),
212 default_keys: default_keys.into_iter().map(String::from).collect(),
213 description: description.to_string(),
214 }
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct UserKeybindings {
221 #[serde(default)]
223 pub bindings: HashMap<String, Vec<String>>,
224}
225
226pub fn load_user_keybindings(path: &Path) -> Option<UserKeybindings> {
228 let content = std::fs::read_to_string(path).ok()?;
229 serde_json::from_str(&content).ok()
230}
231
232pub fn save_user_keybindings(path: &Path, bindings: &UserKeybindings) -> std::io::Result<()> {
234 let content = serde_json::to_string_pretty(bindings).map_err(|e| {
235 std::io::Error::new(std::io::ErrorKind::InvalidData, e)
236 })?;
237 std::fs::write(path, content)
238}
239
240pub fn default_keybindings_path() -> Option<PathBuf> {
242 dirs::config_dir().map(|p| {
243 p.join("oxi")
244 .join("keybindings.json")
245 })
246}
247
248pub fn vim_keybindings() -> HashMap<String, KeyBinding> {
250 let mut bindings = HashMap::new();
251
252 bindings.insert(
253 "tui.editor.cursorUp".to_string(),
254 KeyBinding::new("tui.editor.cursorUp", vec!["k", "Up"], "Move cursor up"),
255 );
256 bindings.insert(
257 "tui.editor.cursorDown".to_string(),
258 KeyBinding::new("tui.editor.cursorDown", vec!["j", "Down"], "Move cursor down"),
259 );
260 bindings.insert(
261 "tui.editor.cursorLeft".to_string(),
262 KeyBinding::new("tui.editor.cursorLeft", vec!["h", "Left"], "Move cursor left"),
263 );
264 bindings.insert(
265 "tui.editor.cursorRight".to_string(),
266 KeyBinding::new("tui.editor.cursorRight", vec!["l", "Right"], "Move cursor right"),
267 );
268 bindings.insert(
269 "tui.input.submit".to_string(),
270 KeyBinding::new("tui.input.submit", vec!["Enter"], "Submit input"),
271 );
272 bindings.insert(
273 "app.interrupt".to_string(),
274 KeyBinding::new("app.interrupt", vec!["Escape"], "Cancel or abort"),
275 );
276 bindings.insert(
277 "app.clear".to_string(),
278 KeyBinding::new("app.clear", vec!["ctrl+c"], "Clear editor"),
279 );
280
281 bindings
282}
283
284pub fn emacs_keybindings() -> HashMap<String, KeyBinding> {
286 let mut bindings = HashMap::new();
287
288 bindings.insert(
289 "tui.editor.cursorUp".to_string(),
290 KeyBinding::new("tui.editor.cursorUp", vec!["ctrl+p", "Up"], "Move cursor up"),
291 );
292 bindings.insert(
293 "tui.editor.cursorDown".to_string(),
294 KeyBinding::new("tui.editor.cursorDown", vec!["ctrl+n", "Down"], "Move cursor down"),
295 );
296 bindings.insert(
297 "tui.editor.cursorLeft".to_string(),
298 KeyBinding::new("tui.editor.cursorLeft", vec!["ctrl+b", "Left"], "Move cursor left"),
299 );
300 bindings.insert(
301 "tui.editor.cursorRight".to_string(),
302 KeyBinding::new("tui.editor.cursorRight", vec!["ctrl+f", "Right"], "Move cursor right"),
303 );
304 bindings.insert(
305 "tui.input.newLine".to_string(),
306 KeyBinding::new("tui.input.newLine", vec!["ctrl+j"], "New line"),
307 );
308 bindings.insert(
309 "tui.input.submit".to_string(),
310 KeyBinding::new("tui.input.submit", vec!["ctrl+m", "Enter"], "Submit input"),
311 );
312 bindings.insert(
313 "app.interrupt".to_string(),
314 KeyBinding::new("app.interrupt", vec!["ctrl+g"], "Cancel or abort"),
315 );
316
317 bindings
318}
319
320pub fn get_default_keybindings() -> HashMap<String, KeyBinding> {
322 #[cfg(unix)]
324 {
325 vim_keybindings()
326 }
327 #[cfg(not(unix))]
328 {
329 emacs_keybindings()
330 }
331}
332
333pub struct KeybindingsManager {
335 bindings: HashMap<String, KeyBinding>,
337 user_overrides: HashMap<String, Vec<String>>,
339}
340
341impl KeybindingsManager {
342 pub fn new() -> Self {
344 Self {
345 bindings: get_default_keybindings(),
346 user_overrides: HashMap::new(),
347 }
348 }
349
350 pub fn from_file(path: &Path) -> Self {
352 let mut manager = Self::new();
353 if let Some(user) = load_user_keybindings(path) {
354 manager.user_overrides = user.bindings;
355 }
356 manager
357 }
358
359 pub fn from_settings(_settings: &crate::settings::Settings) -> Self {
361 let manager = Self::new();
362 manager
364 }
365
366 pub fn register(&mut self, binding: KeyBinding) {
368 self.bindings.insert(binding.action.clone(), binding);
369 }
370
371 pub fn set_override(&mut self, action: &str, keys: Vec<String>) {
373 self.user_overrides.insert(action.to_string(), keys);
374 }
375
376 pub fn get_keys(&self, action: &str) -> Vec<String> {
378 if let Some(user_keys) = self.user_overrides.get(action) {
379 user_keys.clone()
380 } else if let Some(binding) = self.bindings.get(action) {
381 binding.default_keys.clone()
382 } else {
383 Vec::new()
384 }
385 }
386
387 pub fn get_binding(&self, action: &str) -> Option<&KeyBinding> {
389 self.bindings.get(action)
390 }
391
392 pub fn all_bindings(&self) -> &HashMap<String, KeyBinding> {
394 &self.bindings
395 }
396
397 pub fn user_overrides(&self) -> &HashMap<String, Vec<String>> {
399 &self.user_overrides
400 }
401
402 pub fn reload(&mut self, path: &Path) {
404 if let Some(user) = load_user_keybindings(path) {
405 self.user_overrides = user.bindings;
406 }
407 }
408
409 pub fn export_to_file(&self, path: &Path) -> std::io::Result<()> {
411 let user_bindings = UserKeybindings {
412 bindings: self.user_overrides.clone(),
413 };
414 save_user_keybindings(path, &user_bindings)
415 }
416}
417
418impl Default for KeybindingsManager {
419 fn default() -> Self {
420 Self::new()
421 }
422}
423
424pub fn parse_key_sequence(sequence: &str) -> Vec<(bool, bool, char)> {
426 let mut result = Vec::new();
428 let parts: Vec<&str> = sequence.split('+').collect();
429
430 let mut ctrl = false;
431 let mut alt = false;
432
433 for part in parts {
434 let part_lower = part.to_lowercase();
435
436 if part_lower == "ctrl" || part_lower == "control" {
437 ctrl = true;
438 continue;
439 }
440 if part_lower == "alt" || part_lower == "meta" {
441 alt = true;
442 continue;
443 }
444
445 let key_char = match part_lower.as_str() {
446 "space" => " ",
447 "tab" => "\t",
448 "enter" | "return" => "\n",
449 "escape" | "esc" => "\x1b",
450 "backspace" => "\x7f",
451 "delete" => "\x1b[3~",
452 "up" => "\x1b[A",
453 "down" => "\x1b[B",
454 "right" => "\x1b[C",
455 "left" => "\x1b[D",
456 "home" => "\x1b[H",
457 "end" => "\x1b[F",
458 "pageup" => "\x1b[5~",
459 "pagedown" => "\x1b[6~",
460 _ => part,
461 };
462
463 let first_char = key_char.chars().next().unwrap_or(' ');
464 result.push((ctrl, alt, first_char));
465 ctrl = false;
467 alt = false;
468 }
469
470 result
471}
472
473pub fn format_key_sequence(keys: &[String]) -> String {
475 keys.iter()
476 .map(|k| {
477 let parts: Vec<&str> = k.split('+').collect();
478 let mut formatted_parts = Vec::new();
479 for (i, part) in parts.iter().enumerate() {
480 let lower = part.to_lowercase();
481 if lower == "ctrl" {
482 formatted_parts.push("Ctrl".to_string());
483 } else if lower == "alt" || lower == "meta" {
484 formatted_parts.push("Alt".to_string());
485 } else if lower == "shift" {
486 formatted_parts.push("Shift".to_string());
487 } else if i == parts.len() - 1 {
488 let c = part.chars().next().unwrap_or(' ');
490 formatted_parts.push(c.to_uppercase().collect::<String>());
491 } else {
492 formatted_parts.push(part.to_string());
493 }
494 }
495 formatted_parts.join("+")
496 })
497 .collect::<Vec<_>>()
498 .join(", ")
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn test_key_action_from_str() {
507 assert_eq!(KeyAction::from("app.interrupt"), KeyAction::Interrupt);
508 assert_eq!(KeyAction::from("app.clear"), KeyAction::Clear);
509 assert_eq!(KeyAction::from("submit"), KeyAction::Submit);
510 }
511
512 #[test]
513 fn test_key_action_custom() {
514 let action = KeyAction::from("my-extension.action");
515 assert!(matches!(action, KeyAction::Custom(s) if s == "my-extension.action"));
516 }
517
518 #[test]
519 fn test_keybindings_manager_new() {
520 let manager = KeybindingsManager::new();
521 assert!(!manager.all_bindings().is_empty());
522 }
523
524 #[test]
525 fn test_get_keys_default() {
526 let manager = KeybindingsManager::new();
527 let keys = manager.get_keys("app.interrupt");
528 assert!(!keys.is_empty());
529 }
530
531 #[test]
532 fn test_set_override() {
533 let mut manager = KeybindingsManager::new();
534 manager.set_override("app.interrupt", vec!["ctrl+c".to_string()]);
535 let keys = manager.get_keys("app.interrupt");
536 assert_eq!(keys, vec!["ctrl+c"]);
537 }
538
539 #[test]
540 fn test_vim_keybindings() {
541 let bindings = vim_keybindings();
542 assert!(bindings.contains_key("tui.editor.cursorUp"));
543 assert!(bindings.contains_key("tui.input.submit"));
544 }
545
546 #[test]
547 fn test_emacs_keybindings() {
548 let bindings = emacs_keybindings();
549 assert!(bindings.contains_key("tui.editor.cursorUp"));
550 assert!(bindings.contains_key("tui.input.submit"));
551 }
552
553 #[test]
554 fn test_parse_key_sequence() {
555 let result = parse_key_sequence("ctrl+c");
556 assert!(result.iter().any(|(ctrl, _, _)| *ctrl));
557
558 let result = parse_key_sequence("alt+x");
559 assert!(result.iter().any(|(_, alt, _)| *alt));
560 }
561
562 #[test]
563 fn test_format_key_sequence() {
564 let keys = vec!["ctrl+c".to_string(), "ctrl+x".to_string()];
565 let formatted = format_key_sequence(&keys);
566 assert!(formatted.contains("Ctrl+C"));
567 assert!(formatted.contains("Ctrl+X"));
568 }
569
570 #[test]
571 fn test_user_keybindings_serde() {
572 let user = UserKeybindings {
573 bindings: HashMap::from([
574 ("app.interrupt".to_string(), vec!["ctrl+c".to_string()]),
575 ]),
576 };
577 let json = serde_json::to_string(&user).unwrap();
578 let parsed: UserKeybindings = serde_json::from_str(&json).unwrap();
579 assert_eq!(parsed.bindings.get("app.interrupt"), Some(&vec!["ctrl+c".to_string()]));
580 }
581
582 #[test]
583 fn test_keybinding_struct() {
584 let binding = KeyBinding::new("test.action", vec!["a", "b"], "Test action");
585 assert_eq!(binding.action, "test.action");
586 assert_eq!(binding.default_keys, vec!["a", "b"]);
587 assert_eq!(binding.description, "Test action");
588 }
589}