revue/utils/
keymap.rs

1//! Extended keymap utilities
2//!
3//! Provides configurable key binding management for TUI applications.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use revue::utils::keymap::{KeymapConfig, Mode, bind};
9//!
10//! let mut keymap = KeymapConfig::new();
11//!
12//! // Add mode-specific bindings
13//! keymap.bind(Mode::Normal, "j", "move_down");
14//! keymap.bind(Mode::Normal, "k", "move_up");
15//! keymap.bind(Mode::Insert, "Escape", "exit_insert");
16//!
17//! // Parse and execute
18//! keymap.set_mode(Mode::Normal);
19//! let action = keymap.lookup("j");
20//! ```
21
22use crate::event::{Key, KeyBinding};
23use std::collections::HashMap;
24
25/// Input mode
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub enum Mode {
28    /// Normal mode (navigation)
29    #[default]
30    Normal,
31    /// Insert mode (text input)
32    Insert,
33    /// Visual mode (selection)
34    Visual,
35    /// Command mode (ex commands)
36    Command,
37    /// Search mode
38    Search,
39    /// Custom mode
40    Custom(u8),
41}
42
43impl Mode {
44    /// Get mode name
45    pub fn name(&self) -> &'static str {
46        match self {
47            Mode::Normal => "NORMAL",
48            Mode::Insert => "INSERT",
49            Mode::Visual => "VISUAL",
50            Mode::Command => "COMMAND",
51            Mode::Search => "SEARCH",
52            Mode::Custom(_) => "CUSTOM",
53        }
54    }
55}
56
57/// Key chord (multiple keys)
58#[derive(Clone, Debug, PartialEq, Eq, Hash)]
59pub struct KeyChord {
60    /// Keys in the chord
61    pub keys: Vec<KeyBinding>,
62}
63
64impl KeyChord {
65    /// Create single key chord
66    pub fn single(key: KeyBinding) -> Self {
67        Self { keys: vec![key] }
68    }
69
70    /// Create multi-key chord
71    pub fn multi(keys: Vec<KeyBinding>) -> Self {
72        Self { keys }
73    }
74
75    /// Parse from string (e.g., "Ctrl-x Ctrl-s")
76    pub fn parse(s: &str) -> Option<Self> {
77        let parts: Vec<&str> = s.split_whitespace().collect();
78        if parts.is_empty() {
79            return None;
80        }
81
82        let keys: Option<Vec<KeyBinding>> = parts.iter().map(|p| parse_key_binding(p)).collect();
83        keys.map(|k| Self { keys: k })
84    }
85}
86
87/// Parse a single key binding string
88pub fn parse_key_binding(s: &str) -> Option<KeyBinding> {
89    let s = s.trim();
90    if s.is_empty() {
91        return None;
92    }
93
94    let mut ctrl = false;
95    let mut alt = false;
96    let mut shift = false;
97    let mut key_part = s;
98
99    // Parse modifiers
100    loop {
101        let lower = key_part.to_lowercase();
102        if lower.starts_with("ctrl-") || lower.starts_with("c-") {
103            ctrl = true;
104            key_part = if lower.starts_with("ctrl-") {
105                &key_part[5..]
106            } else {
107                &key_part[2..]
108            };
109        } else if lower.starts_with("alt-") || lower.starts_with("m-") {
110            alt = true;
111            key_part = if lower.starts_with("alt-") {
112                &key_part[4..]
113            } else {
114                &key_part[2..]
115            };
116        } else if lower.starts_with("shift-") || lower.starts_with("s-") {
117            shift = true;
118            key_part = if lower.starts_with("shift-") {
119                &key_part[6..]
120            } else {
121                &key_part[2..]
122            };
123        } else {
124            break;
125        }
126    }
127
128    let key = parse_key(key_part)?;
129
130    Some(KeyBinding {
131        key,
132        ctrl,
133        alt,
134        shift,
135    })
136}
137
138/// Parse key name to Key enum
139fn parse_key(s: &str) -> Option<Key> {
140    let lower = s.to_lowercase();
141    match lower.as_str() {
142        "enter" | "return" | "cr" => Some(Key::Enter),
143        "escape" | "esc" => Some(Key::Escape),
144        "tab" => Some(Key::Tab),
145        "backtab" | "s-tab" => Some(Key::BackTab),
146        "backspace" | "bs" => Some(Key::Backspace),
147        "delete" | "del" => Some(Key::Delete),
148        "up" => Some(Key::Up),
149        "down" => Some(Key::Down),
150        "left" => Some(Key::Left),
151        "right" => Some(Key::Right),
152        "home" => Some(Key::Home),
153        "end" => Some(Key::End),
154        "pageup" | "pgup" => Some(Key::PageUp),
155        "pagedown" | "pgdn" => Some(Key::PageDown),
156        "insert" | "ins" => Some(Key::Insert),
157        "space" => Some(Key::Char(' ')),
158        "f1" => Some(Key::F(1)),
159        "f2" => Some(Key::F(2)),
160        "f3" => Some(Key::F(3)),
161        "f4" => Some(Key::F(4)),
162        "f5" => Some(Key::F(5)),
163        "f6" => Some(Key::F(6)),
164        "f7" => Some(Key::F(7)),
165        "f8" => Some(Key::F(8)),
166        "f9" => Some(Key::F(9)),
167        "f10" => Some(Key::F(10)),
168        "f11" => Some(Key::F(11)),
169        "f12" => Some(Key::F(12)),
170        _ => {
171            // Single character
172            let chars: Vec<char> = s.chars().collect();
173            if chars.len() == 1 {
174                Some(Key::Char(chars[0]))
175            } else {
176                None
177            }
178        }
179    }
180}
181
182/// Format a key binding for display
183pub fn format_key_binding(binding: &KeyBinding) -> String {
184    let mut parts = Vec::new();
185
186    if binding.ctrl {
187        parts.push("Ctrl");
188    }
189    if binding.alt {
190        parts.push("Alt");
191    }
192    if binding.shift {
193        parts.push("Shift");
194    }
195
196    let key_str = match binding.key {
197        Key::Char(' ') => "Space".to_string(),
198        Key::Char(c) => c.to_string(),
199        Key::Enter => "Enter".to_string(),
200        Key::Escape => "Esc".to_string(),
201        Key::Tab => "Tab".to_string(),
202        Key::BackTab => "BackTab".to_string(),
203        Key::Backspace => "Backspace".to_string(),
204        Key::Delete => "Del".to_string(),
205        Key::Up => "↑".to_string(),
206        Key::Down => "↓".to_string(),
207        Key::Left => "←".to_string(),
208        Key::Right => "→".to_string(),
209        Key::Home => "Home".to_string(),
210        Key::End => "End".to_string(),
211        Key::PageUp => "PgUp".to_string(),
212        Key::PageDown => "PgDn".to_string(),
213        Key::Insert => "Ins".to_string(),
214        Key::F(n) => format!("F{}", n),
215        Key::Null => "Null".to_string(),
216        Key::Unknown => "Unknown".to_string(),
217    };
218
219    parts.push(&key_str);
220    parts.join("-")
221}
222
223/// Keymap configuration
224#[derive(Clone, Debug)]
225pub struct KeymapConfig {
226    /// Mode-specific bindings
227    bindings: HashMap<Mode, HashMap<KeyChord, String>>,
228    /// Current mode
229    current_mode: Mode,
230    /// Pending keys for multi-key chords
231    pending: Vec<KeyBinding>,
232    /// Timeout for multi-key chords (ms)
233    chord_timeout: u64,
234    /// Global bindings (active in all modes)
235    global_bindings: HashMap<KeyChord, String>,
236}
237
238impl KeymapConfig {
239    /// Create new keymap config
240    pub fn new() -> Self {
241        Self {
242            bindings: HashMap::new(),
243            current_mode: Mode::Normal,
244            pending: Vec::new(),
245            chord_timeout: 1000,
246            global_bindings: HashMap::new(),
247        }
248    }
249
250    /// Set current mode
251    pub fn set_mode(&mut self, mode: Mode) {
252        self.current_mode = mode;
253        self.pending.clear();
254    }
255
256    /// Get current mode
257    pub fn mode(&self) -> Mode {
258        self.current_mode
259    }
260
261    /// Add a binding to a specific mode
262    pub fn bind(&mut self, mode: Mode, keys: &str, action: impl Into<String>) {
263        if let Some(chord) = KeyChord::parse(keys) {
264            self.bindings
265                .entry(mode)
266                .or_default()
267                .insert(chord, action.into());
268        }
269    }
270
271    /// Add a global binding (all modes)
272    pub fn bind_global(&mut self, keys: &str, action: impl Into<String>) {
273        if let Some(chord) = KeyChord::parse(keys) {
274            self.global_bindings.insert(chord, action.into());
275        }
276    }
277
278    /// Remove a binding
279    pub fn unbind(&mut self, mode: Mode, keys: &str) {
280        if let Some(chord) = KeyChord::parse(keys) {
281            if let Some(mode_bindings) = self.bindings.get_mut(&mode) {
282                mode_bindings.remove(&chord);
283            }
284        }
285    }
286
287    /// Look up action for a key
288    pub fn lookup(&mut self, key: KeyBinding) -> LookupResult {
289        self.pending.push(key);
290
291        let chord = KeyChord {
292            keys: self.pending.clone(),
293        };
294
295        // Check global bindings first
296        if let Some(action) = self.global_bindings.get(&chord) {
297            self.pending.clear();
298            return LookupResult::Action(action.clone());
299        }
300
301        // Check mode-specific bindings
302        if let Some(mode_bindings) = self.bindings.get(&self.current_mode) {
303            if let Some(action) = mode_bindings.get(&chord) {
304                self.pending.clear();
305                return LookupResult::Action(action.clone());
306            }
307
308            // Check if this could be a prefix of a longer chord
309            for existing_chord in mode_bindings.keys() {
310                if existing_chord.keys.len() > self.pending.len()
311                    && existing_chord.keys.starts_with(&self.pending)
312                {
313                    return LookupResult::Pending;
314                }
315            }
316        }
317
318        // No match and no prefix match
319        self.pending.clear();
320        LookupResult::None
321    }
322
323    /// Clear pending keys
324    pub fn clear_pending(&mut self) {
325        self.pending.clear();
326    }
327
328    /// Get pending keys
329    pub fn pending_keys(&self) -> &[KeyBinding] {
330        &self.pending
331    }
332
333    /// Check if there are pending keys
334    pub fn has_pending(&self) -> bool {
335        !self.pending.is_empty()
336    }
337
338    /// Set chord timeout
339    pub fn chord_timeout(&mut self, ms: u64) {
340        self.chord_timeout = ms;
341    }
342
343    /// Get all bindings for a mode
344    pub fn bindings_for_mode(&self, mode: Mode) -> Vec<(&KeyChord, &str)> {
345        self.bindings
346            .get(&mode)
347            .map(|m| m.iter().map(|(k, v)| (k, v.as_str())).collect())
348            .unwrap_or_default()
349    }
350
351    /// Get all global bindings
352    pub fn global_bindings(&self) -> Vec<(&KeyChord, &str)> {
353        self.global_bindings
354            .iter()
355            .map(|(k, v)| (k, v.as_str()))
356            .collect()
357    }
358}
359
360impl Default for KeymapConfig {
361    fn default() -> Self {
362        Self::new()
363    }
364}
365
366/// Result of key lookup
367#[derive(Clone, Debug, PartialEq, Eq)]
368pub enum LookupResult {
369    /// No matching binding
370    None,
371    /// Matched an action
372    Action(String),
373    /// Could be part of a longer chord, waiting for more keys
374    Pending,
375}
376
377/// Vim-style keymap preset
378pub fn vim_preset() -> KeymapConfig {
379    let mut config = KeymapConfig::new();
380
381    // Normal mode
382    config.bind(Mode::Normal, "h", "move_left");
383    config.bind(Mode::Normal, "j", "move_down");
384    config.bind(Mode::Normal, "k", "move_up");
385    config.bind(Mode::Normal, "l", "move_right");
386    config.bind(Mode::Normal, "i", "enter_insert");
387    config.bind(Mode::Normal, "a", "append");
388    config.bind(Mode::Normal, "A", "append_end");
389    config.bind(Mode::Normal, "o", "open_below");
390    config.bind(Mode::Normal, "O", "open_above");
391    config.bind(Mode::Normal, "v", "enter_visual");
392    config.bind(Mode::Normal, ":", "enter_command");
393    config.bind(Mode::Normal, "/", "search_forward");
394    config.bind(Mode::Normal, "?", "search_backward");
395    config.bind(Mode::Normal, "n", "search_next");
396    config.bind(Mode::Normal, "N", "search_prev");
397    config.bind(Mode::Normal, "g g", "goto_first");
398    config.bind(Mode::Normal, "G", "goto_last");
399    config.bind(Mode::Normal, "Ctrl-u", "page_up");
400    config.bind(Mode::Normal, "Ctrl-d", "page_down");
401    config.bind(Mode::Normal, "d d", "delete_line");
402    config.bind(Mode::Normal, "y y", "yank_line");
403    config.bind(Mode::Normal, "p", "paste_after");
404    config.bind(Mode::Normal, "P", "paste_before");
405    config.bind(Mode::Normal, "u", "undo");
406    config.bind(Mode::Normal, "Ctrl-r", "redo");
407
408    // Insert mode
409    config.bind(Mode::Insert, "Escape", "exit_insert");
410    config.bind(Mode::Insert, "Ctrl-c", "exit_insert");
411
412    // Visual mode
413    config.bind(Mode::Visual, "Escape", "exit_visual");
414    config.bind(Mode::Visual, "h", "extend_left");
415    config.bind(Mode::Visual, "j", "extend_down");
416    config.bind(Mode::Visual, "k", "extend_up");
417    config.bind(Mode::Visual, "l", "extend_right");
418    config.bind(Mode::Visual, "y", "yank_selection");
419    config.bind(Mode::Visual, "d", "delete_selection");
420
421    // Command mode
422    config.bind(Mode::Command, "Escape", "exit_command");
423    config.bind(Mode::Command, "Enter", "execute_command");
424
425    // Global
426    config.bind_global("Ctrl-c", "quit");
427    config.bind_global("Ctrl-z", "suspend");
428
429    config
430}
431
432/// Emacs-style keymap preset
433pub fn emacs_preset() -> KeymapConfig {
434    let mut config = KeymapConfig::new();
435
436    // Navigation
437    config.bind(Mode::Normal, "Ctrl-p", "move_up");
438    config.bind(Mode::Normal, "Ctrl-n", "move_down");
439    config.bind(Mode::Normal, "Ctrl-b", "move_left");
440    config.bind(Mode::Normal, "Ctrl-f", "move_right");
441    config.bind(Mode::Normal, "Ctrl-a", "line_start");
442    config.bind(Mode::Normal, "Ctrl-e", "line_end");
443    config.bind(Mode::Normal, "Alt-<", "goto_first");
444    config.bind(Mode::Normal, "Alt->", "goto_last");
445    config.bind(Mode::Normal, "Ctrl-v", "page_down");
446    config.bind(Mode::Normal, "Alt-v", "page_up");
447
448    // Editing
449    config.bind(Mode::Normal, "Ctrl-d", "delete_char");
450    config.bind(Mode::Normal, "Ctrl-k", "kill_line");
451    config.bind(Mode::Normal, "Ctrl-y", "yank");
452    config.bind(Mode::Normal, "Ctrl-w", "cut_region");
453    config.bind(Mode::Normal, "Alt-w", "copy_region");
454
455    // Search
456    config.bind(Mode::Normal, "Ctrl-s", "search_forward");
457    config.bind(Mode::Normal, "Ctrl-r", "search_backward");
458
459    // Undo
460    config.bind(Mode::Normal, "Ctrl-/", "undo");
461    config.bind(Mode::Normal, "Ctrl-x u", "undo");
462
463    // File operations
464    config.bind(Mode::Normal, "Ctrl-x Ctrl-s", "save");
465    config.bind(Mode::Normal, "Ctrl-x Ctrl-c", "quit");
466    config.bind(Mode::Normal, "Ctrl-x Ctrl-f", "open_file");
467
468    config
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn test_parse_key_binding() {
477        let binding = parse_key_binding("j").unwrap();
478        assert_eq!(binding.key, Key::Char('j'));
479        assert!(!binding.ctrl);
480
481        let binding = parse_key_binding("Ctrl-c").unwrap();
482        assert_eq!(binding.key, Key::Char('c'));
483        assert!(binding.ctrl);
484
485        let binding = parse_key_binding("Ctrl-Alt-Delete").unwrap();
486        assert_eq!(binding.key, Key::Delete);
487        assert!(binding.ctrl);
488        assert!(binding.alt);
489    }
490
491    #[test]
492    fn test_format_key_binding() {
493        let binding = KeyBinding {
494            key: Key::Char('c'),
495            ctrl: true,
496            alt: false,
497            shift: false,
498        };
499        assert_eq!(format_key_binding(&binding), "Ctrl-c");
500
501        let binding = KeyBinding {
502            key: Key::Enter,
503            ctrl: false,
504            alt: false,
505            shift: false,
506        };
507        assert_eq!(format_key_binding(&binding), "Enter");
508    }
509
510    #[test]
511    fn test_key_chord_parse() {
512        let chord = KeyChord::parse("Ctrl-x Ctrl-s").unwrap();
513        assert_eq!(chord.keys.len(), 2);
514        assert!(chord.keys[0].ctrl);
515        assert!(chord.keys[1].ctrl);
516    }
517
518    #[test]
519    fn test_keymap_single_key() {
520        let mut keymap = KeymapConfig::new();
521        keymap.bind(Mode::Normal, "j", "move_down");
522
523        let binding = parse_key_binding("j").unwrap();
524        let result = keymap.lookup(binding);
525        assert_eq!(result, LookupResult::Action("move_down".to_string()));
526    }
527
528    #[test]
529    fn test_keymap_multi_key() {
530        let mut keymap = KeymapConfig::new();
531        keymap.bind(Mode::Normal, "g g", "goto_first");
532
533        let g1 = parse_key_binding("g").unwrap();
534        let result = keymap.lookup(g1.clone());
535        assert_eq!(result, LookupResult::Pending);
536
537        let result = keymap.lookup(g1);
538        assert_eq!(result, LookupResult::Action("goto_first".to_string()));
539    }
540
541    #[test]
542    fn test_keymap_no_match() {
543        let mut keymap = KeymapConfig::new();
544        keymap.bind(Mode::Normal, "j", "move_down");
545
546        let binding = parse_key_binding("x").unwrap();
547        let result = keymap.lookup(binding);
548        assert_eq!(result, LookupResult::None);
549    }
550
551    #[test]
552    fn test_keymap_modes() {
553        let mut keymap = KeymapConfig::new();
554        keymap.bind(Mode::Normal, "i", "enter_insert");
555        keymap.bind(Mode::Insert, "Escape", "exit_insert");
556
557        keymap.set_mode(Mode::Normal);
558        let i = parse_key_binding("i").unwrap();
559        let result = keymap.lookup(i);
560        assert_eq!(result, LookupResult::Action("enter_insert".to_string()));
561
562        keymap.set_mode(Mode::Insert);
563        let esc = parse_key_binding("Escape").unwrap();
564        let result = keymap.lookup(esc);
565        assert_eq!(result, LookupResult::Action("exit_insert".to_string()));
566    }
567
568    #[test]
569    fn test_vim_preset() {
570        let mut keymap = vim_preset();
571
572        let j = parse_key_binding("j").unwrap();
573        let result = keymap.lookup(j);
574        assert_eq!(result, LookupResult::Action("move_down".to_string()));
575    }
576
577    #[test]
578    fn test_emacs_preset() {
579        let mut keymap = emacs_preset();
580
581        let ctrl_n = parse_key_binding("Ctrl-n").unwrap();
582        let result = keymap.lookup(ctrl_n);
583        assert_eq!(result, LookupResult::Action("move_down".to_string()));
584    }
585
586    #[test]
587    fn test_global_bindings() {
588        let mut keymap = KeymapConfig::new();
589        keymap.bind_global("Ctrl-c", "quit");
590
591        keymap.set_mode(Mode::Normal);
592        let ctrl_c = parse_key_binding("Ctrl-c").unwrap();
593        let result = keymap.lookup(ctrl_c.clone());
594        assert_eq!(result, LookupResult::Action("quit".to_string()));
595
596        keymap.set_mode(Mode::Insert);
597        let result = keymap.lookup(ctrl_c);
598        assert_eq!(result, LookupResult::Action("quit".to_string()));
599    }
600}