Skip to main content

oxi/
keybindings.rs

1//! Keybinding configuration and management
2//!
3//! Provides keybinding definitions, defaults, and custom keybinding loading.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::fmt;
9use std::path::Path;
10
11/// Keybinding action identifiers
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum KeyAction {
14    // Editor actions
15    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    // Input actions
39    Submit,
40    Tab,
41    Copy,
42    SelectUp,
43    SelectDown,
44    SelectPageUp,
45    SelectPageDown,
46    SelectConfirm,
47    SelectCancel,
48    Interrupt,
49
50    // App actions
51    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 action (for extensions)
94    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            // Editor actions
110            "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 actions
143            "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            // Default actions
186            "submit" => KeyAction::Submit,
187            "cancel" => KeyAction::SelectCancel,
188            "historyUp" | "history_up" => KeyAction::SelectUp,
189            "historyDown" | "history_down" => KeyAction::SelectDown,
190
191            // Fallback to custom
192            _ => KeyAction::Custom(s.to_string()),
193        }
194    }
195}
196
197/// A key binding
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct KeyBinding {
200    /// The action associated with this binding
201    pub action: String,
202    /// Default key sequences
203    pub default_keys: Vec<String>,
204    /// Description of the action
205    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/// User-configurable keybindings
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct UserKeybindings {
221    /// Map of action name to custom key sequence(s)
222    #[serde(default)]
223    pub bindings: HashMap<String, Vec<String>>,
224}
225
226/// Load user keybindings from a file
227pub 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
232/// Save user keybindings to a file
233pub 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
240/// Get the default keybindings configuration file path
241pub fn default_keybindings_path() -> Option<PathBuf> {
242    dirs::config_dir().map(|p| {
243        p.join("oxi")
244            .join("keybindings.json")
245    })
246}
247
248/// Vim-style keybindings
249pub 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
284/// Emacs-style keybindings
285pub 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
320/// Get the default keybindings based on settings
321pub fn get_default_keybindings() -> HashMap<String, KeyBinding> {
322    // Default to vim-style on Unix, emacs-style elsewhere
323    #[cfg(unix)]
324    {
325        vim_keybindings()
326    }
327    #[cfg(not(unix))]
328    {
329        emacs_keybindings()
330    }
331}
332
333/// Keybindings manager for loading and resolving keybindings
334pub struct KeybindingsManager {
335    /// All defined keybindings
336    bindings: HashMap<String, KeyBinding>,
337    /// User overrides
338    user_overrides: HashMap<String, Vec<String>>,
339}
340
341impl KeybindingsManager {
342    /// Create a new keybindings manager
343    pub fn new() -> Self {
344        Self {
345            bindings: get_default_keybindings(),
346            user_overrides: HashMap::new(),
347        }
348    }
349
350    /// Create from a config file
351    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    /// Create from settings
360    pub fn from_settings(_settings: &crate::settings::Settings) -> Self {
361        let manager = Self::new();
362        // Settings-based keybindings would be applied here if available
363        manager
364    }
365
366    /// Register a new keybinding
367    pub fn register(&mut self, binding: KeyBinding) {
368        self.bindings.insert(binding.action.clone(), binding);
369    }
370
371    /// Set user override for an action
372    pub fn set_override(&mut self, action: &str, keys: Vec<String>) {
373        self.user_overrides.insert(action.to_string(), keys);
374    }
375
376    /// Get the resolved key sequence for an action
377    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    /// Get a binding by action name
388    pub fn get_binding(&self, action: &str) -> Option<&KeyBinding> {
389        self.bindings.get(action)
390    }
391
392    /// Get all registered bindings
393    pub fn all_bindings(&self) -> &HashMap<String, KeyBinding> {
394        &self.bindings
395    }
396
397    /// Get all user overrides
398    pub fn user_overrides(&self) -> &HashMap<String, Vec<String>> {
399        &self.user_overrides
400    }
401
402    /// Reload from file
403    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    /// Export user overrides to file
410    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
424/// Parse a key sequence string into components
425pub fn parse_key_sequence(sequence: &str) -> Vec<(bool, bool, char)> {
426    // Returns (ctrl, alt, key)
427    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        // Reset modifiers after use
466        ctrl = false;
467        alt = false;
468    }
469
470    result
471}
472
473/// Format a key sequence for display
474pub 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                    // Last part (the key) — uppercase single chars
489                    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}