Skip to main content

scarab_plugin_api/key_tables/
defaults.rs

1//! Default Key Tables
2//!
3//! This module provides default key bindings for various modal editing modes,
4//! matching WezTerm's behavior where appropriate.
5
6use super::{CopyModeAction, Direction, KeyAction, KeyCode, KeyCombo, KeyTable, SearchAction};
7use std::collections::HashMap;
8
9/// Create the default copy mode key table
10///
11/// Copy mode is vim-like and allows for selecting and copying text from the terminal buffer.
12/// This table includes:
13/// - hjkl navigation
14/// - w/b/e word movement
15/// - 0/$/^ line movement
16/// - g/G document movement
17/// - Ctrl+u/d half page movement
18/// - v/V/Ctrl+v selection modes
19/// - / and ? for search
20/// - n/N for next/previous match
21/// - y yank (copy), Escape/q exit
22pub fn default_copy_mode_table() -> KeyTable {
23    let mut table = KeyTable::new("copy_mode");
24
25    // Basic movement - hjkl (vim-style)
26    table.bind(
27        KeyCombo::key(KeyCode::KeyH),
28        KeyAction::CopyMode(CopyModeAction::MoveLeft),
29    );
30    table.bind(
31        KeyCombo::key(KeyCode::KeyJ),
32        KeyAction::CopyMode(CopyModeAction::MoveDown),
33    );
34    table.bind(
35        KeyCombo::key(KeyCode::KeyK),
36        KeyAction::CopyMode(CopyModeAction::MoveUp),
37    );
38    table.bind(
39        KeyCombo::key(KeyCode::KeyL),
40        KeyAction::CopyMode(CopyModeAction::MoveRight),
41    );
42
43    // Arrow keys for movement (alternative to hjkl)
44    table.bind(
45        KeyCombo::key(KeyCode::Left),
46        KeyAction::CopyMode(CopyModeAction::MoveLeft),
47    );
48    table.bind(
49        KeyCombo::key(KeyCode::Down),
50        KeyAction::CopyMode(CopyModeAction::MoveDown),
51    );
52    table.bind(
53        KeyCombo::key(KeyCode::Up),
54        KeyAction::CopyMode(CopyModeAction::MoveUp),
55    );
56    table.bind(
57        KeyCombo::key(KeyCode::Right),
58        KeyAction::CopyMode(CopyModeAction::MoveRight),
59    );
60
61    // Word movement
62    table.bind(
63        KeyCombo::key(KeyCode::KeyW),
64        KeyAction::CopyMode(CopyModeAction::MoveWordForward),
65    );
66    table.bind(
67        KeyCombo::key(KeyCode::KeyB),
68        KeyAction::CopyMode(CopyModeAction::MoveWordBackward),
69    );
70    table.bind(
71        KeyCombo::key(KeyCode::KeyE),
72        KeyAction::CopyMode(CopyModeAction::MoveWordForward),
73    );
74
75    // Line movement
76    table.bind(
77        KeyCombo::key(KeyCode::Digit0),
78        KeyAction::CopyMode(CopyModeAction::MoveToLineStart),
79    );
80    table.bind(
81        KeyCombo::shift(KeyCode::Digit4), // $ (Shift+4)
82        KeyAction::CopyMode(CopyModeAction::MoveToLineEnd),
83    );
84    table.bind(
85        KeyCombo::shift(KeyCode::Digit6), // ^ (Shift+6)
86        KeyAction::CopyMode(CopyModeAction::MoveToLineStart),
87    );
88    table.bind(
89        KeyCombo::key(KeyCode::Home),
90        KeyAction::CopyMode(CopyModeAction::MoveToLineStart),
91    );
92    table.bind(
93        KeyCombo::key(KeyCode::End),
94        KeyAction::CopyMode(CopyModeAction::MoveToLineEnd),
95    );
96
97    // Document movement
98    table.bind(
99        KeyCombo::key(KeyCode::KeyG),
100        KeyAction::CopyMode(CopyModeAction::MoveToTop),
101    );
102    table.bind(
103        KeyCombo::shift(KeyCode::KeyG), // G (Shift+g)
104        KeyAction::CopyMode(CopyModeAction::MoveToBottom),
105    );
106
107    // Page movement
108    table.bind(
109        KeyCombo::ctrl(KeyCode::KeyU),
110        KeyAction::ScrollByPage(-1), // Half page up
111    );
112    table.bind(
113        KeyCombo::ctrl(KeyCode::KeyD),
114        KeyAction::ScrollByPage(1), // Half page down
115    );
116    table.bind(
117        KeyCombo::ctrl(KeyCode::KeyB),
118        KeyAction::ScrollByPage(-2), // Full page up
119    );
120    table.bind(
121        KeyCombo::ctrl(KeyCode::KeyF),
122        KeyAction::ScrollByPage(2), // Full page down
123    );
124    table.bind(KeyCombo::key(KeyCode::PageUp), KeyAction::ScrollByPage(-1));
125    table.bind(KeyCombo::key(KeyCode::PageDown), KeyAction::ScrollByPage(1));
126
127    // Selection modes
128    table.bind(
129        KeyCombo::key(KeyCode::KeyV),
130        KeyAction::CopyMode(CopyModeAction::ToggleSelection),
131    );
132    table.bind(
133        KeyCombo::shift(KeyCode::KeyV), // V (Shift+v)
134        KeyAction::CopyMode(CopyModeAction::ToggleLineSelection),
135    );
136    table.bind(
137        KeyCombo::ctrl(KeyCode::KeyV),
138        KeyAction::CopyMode(CopyModeAction::ToggleBlockSelection),
139    );
140
141    // Search
142    table.bind(
143        KeyCombo::key(KeyCode::Slash), // /
144        KeyAction::CopyMode(CopyModeAction::SearchForward),
145    );
146    table.bind(
147        KeyCombo::shift(KeyCode::Slash), // ? (Shift+/)
148        KeyAction::CopyMode(CopyModeAction::SearchBackward),
149    );
150    table.bind(
151        KeyCombo::key(KeyCode::KeyN),
152        KeyAction::CopyMode(CopyModeAction::NextMatch),
153    );
154    table.bind(
155        KeyCombo::shift(KeyCode::KeyN), // N (Shift+n)
156        KeyAction::CopyMode(CopyModeAction::PrevMatch),
157    );
158
159    // Copy and exit
160    table.bind(
161        KeyCombo::key(KeyCode::KeyY),
162        KeyAction::CopyMode(CopyModeAction::CopyAndExit),
163    );
164
165    // Exit copy mode
166    table.bind(
167        KeyCombo::key(KeyCode::Escape),
168        KeyAction::CopyMode(CopyModeAction::Exit),
169    );
170    table.bind(
171        KeyCombo::key(KeyCode::KeyQ),
172        KeyAction::CopyMode(CopyModeAction::Exit),
173    );
174    table.bind(
175        KeyCombo::ctrl(KeyCode::KeyC),
176        KeyAction::CopyMode(CopyModeAction::Exit),
177    );
178
179    table
180}
181
182/// Create the default search mode key table
183///
184/// Search mode allows for searching and navigating through search results.
185/// This table includes:
186/// - n/N for next/previous match
187/// - Enter to accept search
188/// - Escape to cancel search
189pub fn default_search_mode_table() -> KeyTable {
190    let mut table = KeyTable::new("search_mode");
191
192    // Navigate matches
193    table.bind(
194        KeyCombo::key(KeyCode::KeyN),
195        KeyAction::Search(SearchAction::NextMatch),
196    );
197    table.bind(
198        KeyCombo::shift(KeyCode::KeyN), // N (Shift+n)
199        KeyAction::Search(SearchAction::PrevMatch),
200    );
201
202    // Accept/confirm search
203    table.bind(
204        KeyCombo::key(KeyCode::Enter),
205        KeyAction::Search(SearchAction::Confirm),
206    );
207
208    // Cancel search
209    table.bind(
210        KeyCombo::key(KeyCode::Escape),
211        KeyAction::Search(SearchAction::Cancel),
212    );
213    table.bind(
214        KeyCombo::ctrl(KeyCode::KeyC),
215        KeyAction::Search(SearchAction::Cancel),
216    );
217
218    // Arrow key navigation
219    table.bind(
220        KeyCombo::key(KeyCode::Down),
221        KeyAction::Search(SearchAction::NextMatch),
222    );
223    table.bind(
224        KeyCombo::key(KeyCode::Up),
225        KeyAction::Search(SearchAction::PrevMatch),
226    );
227
228    // Ctrl+n/p for next/previous (emacs-style)
229    table.bind(
230        KeyCombo::ctrl(KeyCode::KeyN),
231        KeyAction::Search(SearchAction::NextMatch),
232    );
233    table.bind(
234        KeyCombo::ctrl(KeyCode::KeyP),
235        KeyAction::Search(SearchAction::PrevMatch),
236    );
237
238    table
239}
240
241/// Create the default resize mode key table
242///
243/// Resize mode allows for resizing panes using keyboard shortcuts.
244/// This table includes:
245/// - hjkl to resize pane in direction
246/// - Arrow keys as alternative
247/// - Enter/Escape to exit resize mode
248pub fn default_resize_mode_table() -> KeyTable {
249    let mut table = KeyTable::new("resize_pane");
250
251    // hjkl resizing (vim-style)
252    // Each keypress adjusts the pane boundary in that direction
253    table.bind(
254        KeyCombo::key(KeyCode::KeyH),
255        KeyAction::AdjustPaneSize {
256            direction: Direction::Left,
257            amount: 2,
258        },
259    );
260    table.bind(
261        KeyCombo::key(KeyCode::KeyJ),
262        KeyAction::AdjustPaneSize {
263            direction: Direction::Down,
264            amount: 2,
265        },
266    );
267    table.bind(
268        KeyCombo::key(KeyCode::KeyK),
269        KeyAction::AdjustPaneSize {
270            direction: Direction::Up,
271            amount: 2,
272        },
273    );
274    table.bind(
275        KeyCombo::key(KeyCode::KeyL),
276        KeyAction::AdjustPaneSize {
277            direction: Direction::Right,
278            amount: 2,
279        },
280    );
281
282    // Arrow keys for resizing
283    table.bind(
284        KeyCombo::key(KeyCode::Left),
285        KeyAction::AdjustPaneSize {
286            direction: Direction::Left,
287            amount: 2,
288        },
289    );
290    table.bind(
291        KeyCombo::key(KeyCode::Down),
292        KeyAction::AdjustPaneSize {
293            direction: Direction::Down,
294            amount: 2,
295        },
296    );
297    table.bind(
298        KeyCombo::key(KeyCode::Up),
299        KeyAction::AdjustPaneSize {
300            direction: Direction::Up,
301            amount: 2,
302        },
303    );
304    table.bind(
305        KeyCombo::key(KeyCode::Right),
306        KeyAction::AdjustPaneSize {
307            direction: Direction::Right,
308            amount: 2,
309        },
310    );
311
312    // Shift + hjkl for larger adjustments
313    table.bind(
314        KeyCombo::shift(KeyCode::KeyH),
315        KeyAction::AdjustPaneSize {
316            direction: Direction::Left,
317            amount: 5,
318        },
319    );
320    table.bind(
321        KeyCombo::shift(KeyCode::KeyJ),
322        KeyAction::AdjustPaneSize {
323            direction: Direction::Down,
324            amount: 5,
325        },
326    );
327    table.bind(
328        KeyCombo::shift(KeyCode::KeyK),
329        KeyAction::AdjustPaneSize {
330            direction: Direction::Up,
331            amount: 5,
332        },
333    );
334    table.bind(
335        KeyCombo::shift(KeyCode::KeyL),
336        KeyAction::AdjustPaneSize {
337            direction: Direction::Right,
338            amount: 5,
339        },
340    );
341
342    // Exit resize mode
343    table.bind(KeyCombo::key(KeyCode::Enter), KeyAction::PopKeyTable);
344    table.bind(KeyCombo::key(KeyCode::Escape), KeyAction::PopKeyTable);
345    table.bind(KeyCombo::ctrl(KeyCode::KeyC), KeyAction::PopKeyTable);
346    table.bind(KeyCombo::key(KeyCode::KeyQ), KeyAction::PopKeyTable);
347
348    table
349}
350
351/// Key table registry for managing named key tables
352///
353/// This registry stores all available key tables and provides lookup functionality.
354pub struct KeyTableRegistry {
355    tables: HashMap<String, KeyTable>,
356}
357
358impl KeyTableRegistry {
359    /// Create a new registry with default key tables registered
360    pub fn new() -> Self {
361        let mut registry = Self {
362            tables: HashMap::new(),
363        };
364
365        // Register default tables
366        registry.register_table(default_copy_mode_table());
367        registry.register_table(default_search_mode_table());
368        registry.register_table(default_resize_mode_table());
369
370        registry
371    }
372
373    /// Get a key table by name
374    pub fn get(&self, name: &str) -> Option<&KeyTable> {
375        self.tables.get(name)
376    }
377
378    /// Register a key table with the given name
379    pub fn register(&mut self, name: impl Into<String>, table: KeyTable) {
380        self.tables.insert(name.into(), table);
381    }
382
383    /// Register a key table using its own name
384    fn register_table(&mut self, table: KeyTable) {
385        let name = table.name.clone();
386        self.tables.insert(name, table);
387    }
388
389    /// Get a mutable reference to a key table
390    pub fn get_mut(&mut self, name: &str) -> Option<&mut KeyTable> {
391        self.tables.get_mut(name)
392    }
393
394    /// Check if a table exists
395    pub fn contains(&self, name: &str) -> bool {
396        self.tables.contains_key(name)
397    }
398
399    /// Get all table names
400    pub fn table_names(&self) -> Vec<&str> {
401        self.tables.keys().map(|s| s.as_str()).collect()
402    }
403}
404
405impl Default for KeyTableRegistry {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_default_copy_mode_table() {
417        let table = default_copy_mode_table();
418        assert_eq!(table.name, "copy_mode");
419
420        // Test basic movement
421        let h_action = table.get(&KeyCombo::key(KeyCode::KeyH));
422        assert!(matches!(
423            h_action,
424            Some(KeyAction::CopyMode(CopyModeAction::MoveLeft))
425        ));
426
427        let j_action = table.get(&KeyCombo::key(KeyCode::KeyJ));
428        assert!(matches!(
429            j_action,
430            Some(KeyAction::CopyMode(CopyModeAction::MoveDown))
431        ));
432
433        // Test exit bindings
434        let esc_action = table.get(&KeyCombo::key(KeyCode::Escape));
435        assert!(matches!(
436            esc_action,
437            Some(KeyAction::CopyMode(CopyModeAction::Exit))
438        ));
439
440        let q_action = table.get(&KeyCombo::key(KeyCode::KeyQ));
441        assert!(matches!(
442            q_action,
443            Some(KeyAction::CopyMode(CopyModeAction::Exit))
444        ));
445
446        // Test selection modes
447        let v_action = table.get(&KeyCombo::key(KeyCode::KeyV));
448        assert!(matches!(
449            v_action,
450            Some(KeyAction::CopyMode(CopyModeAction::ToggleSelection))
451        ));
452
453        // Test copy and exit
454        let y_action = table.get(&KeyCombo::key(KeyCode::KeyY));
455        assert!(matches!(
456            y_action,
457            Some(KeyAction::CopyMode(CopyModeAction::CopyAndExit))
458        ));
459
460        // Test search bindings
461        let slash_action = table.get(&KeyCombo::key(KeyCode::Slash));
462        assert!(matches!(
463            slash_action,
464            Some(KeyAction::CopyMode(CopyModeAction::SearchForward))
465        ));
466
467        let shift_slash_action = table.get(&KeyCombo::shift(KeyCode::Slash));
468        assert!(matches!(
469            shift_slash_action,
470            Some(KeyAction::CopyMode(CopyModeAction::SearchBackward))
471        ));
472
473        let n_action = table.get(&KeyCombo::key(KeyCode::KeyN));
474        assert!(matches!(
475            n_action,
476            Some(KeyAction::CopyMode(CopyModeAction::NextMatch))
477        ));
478
479        let shift_n_action = table.get(&KeyCombo::shift(KeyCode::KeyN));
480        assert!(matches!(
481            shift_n_action,
482            Some(KeyAction::CopyMode(CopyModeAction::PrevMatch))
483        ));
484    }
485
486    #[test]
487    fn test_default_search_mode_table() {
488        let table = default_search_mode_table();
489        assert_eq!(table.name, "search_mode");
490
491        // Test next/previous match
492        let n_action = table.get(&KeyCombo::key(KeyCode::KeyN));
493        assert!(matches!(
494            n_action,
495            Some(KeyAction::Search(SearchAction::NextMatch))
496        ));
497
498        let shift_n_action = table.get(&KeyCombo::shift(KeyCode::KeyN));
499        assert!(matches!(
500            shift_n_action,
501            Some(KeyAction::Search(SearchAction::PrevMatch))
502        ));
503
504        // Test confirm
505        let enter_action = table.get(&KeyCombo::key(KeyCode::Enter));
506        assert!(matches!(
507            enter_action,
508            Some(KeyAction::Search(SearchAction::Confirm))
509        ));
510
511        // Test cancel
512        let esc_action = table.get(&KeyCombo::key(KeyCode::Escape));
513        assert!(matches!(
514            esc_action,
515            Some(KeyAction::Search(SearchAction::Cancel))
516        ));
517    }
518
519    #[test]
520    fn test_default_resize_mode_table() {
521        let table = default_resize_mode_table();
522        assert_eq!(table.name, "resize_pane");
523
524        // Test hjkl resizing
525        let h_action = table.get(&KeyCombo::key(KeyCode::KeyH));
526        assert!(matches!(
527            h_action,
528            Some(KeyAction::AdjustPaneSize {
529                direction: Direction::Left,
530                amount: 2
531            })
532        ));
533
534        // Test larger adjustments with Shift
535        let shift_h_action = table.get(&KeyCombo::shift(KeyCode::KeyH));
536        assert!(matches!(
537            shift_h_action,
538            Some(KeyAction::AdjustPaneSize {
539                direction: Direction::Left,
540                amount: 5
541            })
542        ));
543
544        // Test exit
545        let enter_action = table.get(&KeyCombo::key(KeyCode::Enter));
546        assert_eq!(enter_action, Some(&KeyAction::PopKeyTable));
547
548        let esc_action = table.get(&KeyCombo::key(KeyCode::Escape));
549        assert_eq!(esc_action, Some(&KeyAction::PopKeyTable));
550    }
551
552    #[test]
553    fn test_key_table_registry_creation() {
554        let registry = KeyTableRegistry::new();
555
556        // Verify default tables are registered
557        assert!(registry.contains("copy_mode"));
558        assert!(registry.contains("search_mode"));
559        assert!(registry.contains("resize_pane"));
560    }
561
562    #[test]
563    fn test_key_table_registry_lookup() {
564        let registry = KeyTableRegistry::new();
565
566        // Test lookup
567        let copy_table = registry.get("copy_mode");
568        assert!(copy_table.is_some());
569        assert_eq!(copy_table.unwrap().name, "copy_mode");
570
571        let nonexistent = registry.get("nonexistent");
572        assert!(nonexistent.is_none());
573    }
574
575    #[test]
576    fn test_key_table_registry_custom_registration() {
577        let mut registry = KeyTableRegistry::new();
578
579        let mut custom_table = KeyTable::new("custom");
580        custom_table.bind(KeyCombo::key(KeyCode::KeyA), KeyAction::Noop);
581
582        registry.register("custom", custom_table);
583
584        assert!(registry.contains("custom"));
585        let retrieved = registry.get("custom");
586        assert!(retrieved.is_some());
587        assert_eq!(retrieved.unwrap().name, "custom");
588    }
589
590    #[test]
591    fn test_key_table_registry_table_names() {
592        let registry = KeyTableRegistry::new();
593        let names = registry.table_names();
594
595        assert_eq!(names.len(), 3);
596        assert!(names.contains(&"copy_mode"));
597        assert!(names.contains(&"search_mode"));
598        assert!(names.contains(&"resize_pane"));
599    }
600
601    #[test]
602    fn test_copy_mode_arrow_keys() {
603        let table = default_copy_mode_table();
604
605        // Test arrow key alternatives
606        let left_action = table.get(&KeyCombo::key(KeyCode::Left));
607        assert!(matches!(
608            left_action,
609            Some(KeyAction::CopyMode(CopyModeAction::MoveLeft))
610        ));
611
612        let down_action = table.get(&KeyCombo::key(KeyCode::Down));
613        assert!(matches!(
614            down_action,
615            Some(KeyAction::CopyMode(CopyModeAction::MoveDown))
616        ));
617    }
618
619    #[test]
620    fn test_copy_mode_word_movement() {
621        let table = default_copy_mode_table();
622
623        let w_action = table.get(&KeyCombo::key(KeyCode::KeyW));
624        assert!(matches!(
625            w_action,
626            Some(KeyAction::CopyMode(CopyModeAction::MoveWordForward))
627        ));
628
629        let b_action = table.get(&KeyCombo::key(KeyCode::KeyB));
630        assert!(matches!(
631            b_action,
632            Some(KeyAction::CopyMode(CopyModeAction::MoveWordBackward))
633        ));
634    }
635
636    #[test]
637    fn test_copy_mode_page_movement() {
638        let table = default_copy_mode_table();
639
640        let ctrl_u = table.get(&KeyCombo::ctrl(KeyCode::KeyU));
641        assert!(matches!(ctrl_u, Some(KeyAction::ScrollByPage(-1))));
642
643        let ctrl_d = table.get(&KeyCombo::ctrl(KeyCode::KeyD));
644        assert!(matches!(ctrl_d, Some(KeyAction::ScrollByPage(1))));
645    }
646}