sql_cli/ui/key_handling/
dispatcher.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use std::collections::HashMap;
3
4/// Represents a key binding
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct KeyBinding {
7    pub code: KeyCode,
8    pub modifiers: KeyModifiers,
9}
10
11impl KeyBinding {
12    #[must_use]
13    pub fn new(code: KeyCode) -> Self {
14        Self {
15            code,
16            modifiers: KeyModifiers::empty(),
17        }
18    }
19
20    #[must_use]
21    pub fn with_ctrl(code: KeyCode) -> Self {
22        Self {
23            code,
24            modifiers: KeyModifiers::CONTROL,
25        }
26    }
27
28    #[must_use]
29    pub fn with_alt(code: KeyCode) -> Self {
30        Self {
31            code,
32            modifiers: KeyModifiers::ALT,
33        }
34    }
35
36    #[must_use]
37    pub fn with_shift(code: KeyCode) -> Self {
38        Self {
39            code,
40            modifiers: KeyModifiers::SHIFT,
41        }
42    }
43
44    #[must_use]
45    pub fn from_event(event: &KeyEvent) -> Self {
46        Self {
47            code: event.code,
48            modifiers: event.modifiers,
49        }
50    }
51}
52
53/// Simple key dispatcher that maps keys to action names
54#[derive(Clone)]
55pub struct KeyDispatcher {
56    // Mode-specific key maps
57    command_map: HashMap<KeyBinding, String>,
58    results_map: HashMap<KeyBinding, String>,
59    search_map: HashMap<KeyBinding, String>,
60    filter_map: HashMap<KeyBinding, String>,
61    help_map: HashMap<KeyBinding, String>,
62    debug_map: HashMap<KeyBinding, String>,
63}
64
65impl Default for KeyDispatcher {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl KeyDispatcher {
72    #[must_use]
73    pub fn new() -> Self {
74        let mut dispatcher = Self {
75            command_map: HashMap::new(),
76            results_map: HashMap::new(),
77            search_map: HashMap::new(),
78            filter_map: HashMap::new(),
79            help_map: HashMap::new(),
80            debug_map: HashMap::new(),
81        };
82        dispatcher.setup_default_bindings();
83        dispatcher
84    }
85
86    fn setup_default_bindings(&mut self) {
87        // Command mode bindings
88        self.setup_command_bindings();
89
90        // Results mode bindings
91        self.setup_results_bindings();
92
93        // Search/Filter mode bindings
94        self.setup_search_bindings();
95        self.setup_filter_bindings();
96
97        // Help/Debug mode bindings
98        self.setup_help_bindings();
99        self.setup_debug_bindings();
100    }
101
102    fn setup_command_bindings(&mut self) {
103        // Basic navigation and editing
104        self.command_map
105            .insert(KeyBinding::new(KeyCode::Enter), "execute_query".into());
106        self.command_map
107            .insert(KeyBinding::new(KeyCode::Tab), "handle_completion".into());
108        self.command_map.insert(
109            KeyBinding::new(KeyCode::Backspace),
110            "delete_char_backward".into(),
111        );
112        self.command_map.insert(
113            KeyBinding::new(KeyCode::Delete),
114            "delete_char_forward".into(),
115        );
116        self.command_map
117            .insert(KeyBinding::new(KeyCode::Left), "move_cursor_left".into());
118        self.command_map
119            .insert(KeyBinding::new(KeyCode::Right), "move_cursor_right".into());
120        self.command_map
121            .insert(KeyBinding::new(KeyCode::Home), "move_to_line_start".into());
122        self.command_map
123            .insert(KeyBinding::new(KeyCode::End), "move_to_line_end".into());
124
125        // Control combinations
126        self.command_map
127            .insert(KeyBinding::with_ctrl(KeyCode::Char('c')), "quit".into());
128        self.command_map
129            .insert(KeyBinding::with_ctrl(KeyCode::Char('d')), "quit".into());
130        self.command_map.insert(
131            KeyBinding::with_ctrl(KeyCode::Char('x')),
132            "expand_asterisk".into(),
133        );
134        self.command_map.insert(
135            KeyBinding::with_alt(KeyCode::Char('x')),
136            "expand_asterisk_visible".into(),
137        );
138        self.command_map.insert(
139            KeyBinding::with_ctrl(KeyCode::Char('r')),
140            "search_history".into(),
141        );
142        self.command_map.insert(
143            KeyBinding::with_ctrl(KeyCode::Char('p')),
144            "previous_history".into(),
145        );
146        self.command_map.insert(
147            KeyBinding::with_ctrl(KeyCode::Char('n')),
148            "next_history".into(),
149        );
150        self.command_map.insert(
151            KeyBinding::with_ctrl(KeyCode::Char('a')),
152            "move_to_line_start".into(),
153        );
154        self.command_map.insert(
155            KeyBinding::with_ctrl(KeyCode::Char('e')),
156            "move_to_line_end".into(),
157        );
158        self.command_map.insert(
159            KeyBinding::with_ctrl(KeyCode::Char('w')),
160            "delete_word_backward".into(),
161        );
162        self.command_map.insert(
163            KeyBinding::with_ctrl(KeyCode::Char('k')),
164            "kill_line".into(),
165        );
166        self.command_map.insert(
167            KeyBinding::with_ctrl(KeyCode::Char('u')),
168            "kill_line_backward".into(),
169        );
170        self.command_map
171            .insert(KeyBinding::with_ctrl(KeyCode::Char('y')), "yank".into());
172        self.command_map
173            .insert(KeyBinding::with_ctrl(KeyCode::Char('z')), "undo".into());
174        self.command_map.insert(
175            KeyBinding::with_ctrl(KeyCode::Char('v')),
176            "paste_from_clipboard".into(),
177        );
178        self.command_map.insert(
179            KeyBinding::with_ctrl(KeyCode::Char('6')),
180            "quick_switch_buffer".into(),
181        );
182
183        // Ctrl+Left/Right for word navigation (standard in most editors)
184        self.command_map.insert(
185            KeyBinding::with_ctrl(KeyCode::Left),
186            "move_word_backward".into(),
187        );
188        self.command_map.insert(
189            KeyBinding::with_ctrl(KeyCode::Right),
190            "move_word_forward".into(),
191        );
192
193        // Alt+number for buffer switching (1-9)
194        for i in 1..=9 {
195            let digit_char = char::from_digit(i, 10).unwrap();
196            self.command_map.insert(
197                KeyBinding::with_alt(KeyCode::Char(digit_char)),
198                format!("switch_to_buffer_{i}"),
199            );
200        }
201
202        // Alt combinations
203        self.command_map.insert(
204            KeyBinding::with_alt(KeyCode::Char('d')),
205            "delete_word_forward".into(),
206        );
207        self.command_map.insert(
208            KeyBinding::with_alt(KeyCode::Char('b')),
209            "list_buffers".into(), // Alt+b for buffer list
210        );
211        self.command_map.insert(
212            KeyBinding::with_alt(KeyCode::Char('f')),
213            "move_word_forward".into(),
214        );
215        self.command_map.insert(
216            KeyBinding::with_alt(KeyCode::Char('n')),
217            "new_buffer".into(),
218        );
219        self.command_map.insert(
220            KeyBinding::with_alt(KeyCode::Char('w')),
221            "close_buffer".into(),
222        );
223        self.command_map
224            .insert(KeyBinding::with_alt(KeyCode::Tab), "next_buffer".into());
225        self.command_map.insert(
226            KeyBinding::with_alt(KeyCode::Char('[')),
227            "jump_to_prev_token".into(),
228        );
229        self.command_map.insert(
230            KeyBinding::with_alt(KeyCode::Char(']')),
231            "jump_to_next_token".into(),
232        );
233
234        // Function keys
235        self.command_map
236            .insert(KeyBinding::new(KeyCode::F(1)), "toggle_help".into());
237        self.command_map
238            .insert(KeyBinding::new(KeyCode::F(5)), "toggle_debug".into());
239        self.command_map
240            .insert(KeyBinding::new(KeyCode::F(6)), "show_pretty_query".into());
241        self.command_map.insert(
242            KeyBinding::new(KeyCode::F(8)),
243            "toggle_case_insensitive".into(),
244        );
245        self.command_map
246            .insert(KeyBinding::new(KeyCode::F(9)), "kill_line".into());
247        self.command_map
248            .insert(KeyBinding::new(KeyCode::F(10)), "kill_line_backward".into());
249        self.command_map
250            .insert(KeyBinding::new(KeyCode::F(11)), "previous_buffer".into());
251        self.command_map
252            .insert(KeyBinding::new(KeyCode::F(12)), "next_buffer".into());
253
254        // Navigation to results
255        self.command_map
256            .insert(KeyBinding::new(KeyCode::Down), "enter_results_mode".into());
257        self.command_map.insert(
258            KeyBinding::new(KeyCode::PageDown),
259            "enter_results_mode".into(),
260        );
261    }
262
263    fn setup_results_bindings(&mut self) {
264        // Exit and navigation
265        self.results_map
266            .insert(KeyBinding::new(KeyCode::Esc), "exit_results_mode".into());
267        self.results_map
268            .insert(KeyBinding::new(KeyCode::Up), "exit_results_mode".into());
269        self.results_map
270            .insert(KeyBinding::with_ctrl(KeyCode::Char('c')), "quit".into());
271        self.results_map
272            .insert(KeyBinding::new(KeyCode::Char('q')), "quit".into());
273
274        // Row navigation
275        self.results_map
276            .insert(KeyBinding::new(KeyCode::Char('j')), "next_row".into());
277        self.results_map
278            .insert(KeyBinding::new(KeyCode::Down), "next_row".into());
279        self.results_map
280            .insert(KeyBinding::new(KeyCode::Char('k')), "previous_row".into());
281
282        // Column navigation
283        self.results_map.insert(
284            KeyBinding::new(KeyCode::Char('h')),
285            "move_column_left".into(),
286        );
287        self.results_map
288            .insert(KeyBinding::new(KeyCode::Left), "move_column_left".into());
289        self.results_map.insert(
290            KeyBinding::new(KeyCode::Char('l')),
291            "move_column_right".into(),
292        );
293        self.results_map
294            .insert(KeyBinding::new(KeyCode::Right), "move_column_right".into());
295
296        // Jump navigation
297        self.results_map
298            .insert(KeyBinding::new(KeyCode::Char('g')), "goto_first_row".into());
299        // Uppercase G comes with SHIFT modifier in crossterm
300        self.results_map.insert(
301            KeyBinding::with_shift(KeyCode::Char('G')),
302            "goto_last_row".into(),
303        );
304
305        // Viewport navigation (vim-style H/M/L)
306        self.results_map.insert(
307            KeyBinding::with_shift(KeyCode::Char('H')),
308            "goto_viewport_top".into(),
309        );
310        self.results_map.insert(
311            KeyBinding::with_shift(KeyCode::Char('M')),
312            "goto_viewport_middle".into(),
313        );
314        self.results_map.insert(
315            KeyBinding::with_shift(KeyCode::Char('L')),
316            "goto_viewport_bottom".into(),
317        );
318
319        self.results_map.insert(
320            KeyBinding::new(KeyCode::Char('^')),
321            "goto_first_column".into(),
322        );
323        self.results_map.insert(
324            KeyBinding::new(KeyCode::Char('0')),
325            "goto_first_column".into(),
326        );
327        self.results_map.insert(
328            KeyBinding::new(KeyCode::Char('$')),
329            "goto_last_column".into(),
330        );
331        self.results_map
332            .insert(KeyBinding::new(KeyCode::PageUp), "page_up".into());
333        self.results_map
334            .insert(KeyBinding::new(KeyCode::PageDown), "page_down".into());
335
336        // Features
337        self.results_map
338            .insert(KeyBinding::new(KeyCode::Char('/')), "start_search".into());
339        self.results_map.insert(
340            KeyBinding::new(KeyCode::Char('\\')),
341            "start_column_search".into(),
342        );
343        self.results_map.insert(
344            KeyBinding::with_shift(KeyCode::Char('F')),
345            "start_filter".into(),
346        );
347        self.results_map.insert(
348            KeyBinding::new(KeyCode::Char('f')),
349            "start_fuzzy_filter".into(),
350        );
351        self.results_map
352            .insert(KeyBinding::new(KeyCode::Char('s')), "sort_by_column".into());
353        self.results_map.insert(
354            KeyBinding::with_shift(KeyCode::Char('S')),
355            "show_column_stats".into(),
356        );
357        self.results_map.insert(
358            KeyBinding::new(KeyCode::Char('n')),
359            "next_search_match".into(),
360        );
361        self.results_map.insert(
362            KeyBinding::with_shift(KeyCode::Char('N')),
363            "previous_search_match".into(),
364        );
365
366        // Display options
367        self.results_map.insert(
368            KeyBinding::with_shift(KeyCode::Char('C')),
369            "toggle_compact_mode".into(),
370        );
371        self.results_map.insert(
372            KeyBinding::with_shift(KeyCode::Char('1')),
373            "toggle_row_numbers".into(),
374        );
375        self.results_map
376            .insert(KeyBinding::new(KeyCode::Char(':')), "jump_to_row".into());
377        self.results_map
378            .insert(KeyBinding::new(KeyCode::Char('p')), "pin_column".into());
379        self.results_map.insert(
380            KeyBinding::with_shift(KeyCode::Char('P')),
381            "clear_pins".into(),
382        );
383
384        // Selection and clipboard
385        self.results_map.insert(
386            KeyBinding::new(KeyCode::Char('v')),
387            "toggle_selection_mode".into(),
388        );
389        // Note: 'y' is handled specially in enhanced_tui based on selection mode
390        // In cell mode: 'y' yanks cell directly
391        // In row mode: starts chord sequence (yy=row, yc=column, ya=all, yv=cell)
392
393        // Export
394        self.results_map.insert(
395            KeyBinding::with_ctrl(KeyCode::Char('e')),
396            "export_to_csv".into(),
397        );
398        self.results_map.insert(
399            KeyBinding::with_ctrl(KeyCode::Char('j')),
400            "export_to_json".into(),
401        );
402
403        // Debug/Help
404        self.results_map
405            .insert(KeyBinding::new(KeyCode::F(1)), "toggle_help".into());
406        self.results_map
407            .insert(KeyBinding::new(KeyCode::Char('?')), "toggle_help".into());
408        self.results_map
409            .insert(KeyBinding::new(KeyCode::F(5)), "toggle_debug".into());
410        self.results_map.insert(
411            KeyBinding::new(KeyCode::F(8)),
412            "toggle_case_insensitive".into(),
413        );
414    }
415
416    fn setup_search_bindings(&mut self) {
417        self.search_map
418            .insert(KeyBinding::new(KeyCode::Enter), "apply_search".into());
419        self.search_map
420            .insert(KeyBinding::new(KeyCode::Esc), "cancel_search".into());
421        self.search_map.insert(
422            KeyBinding::new(KeyCode::Backspace),
423            "delete_char_backward".into(),
424        );
425        self.search_map.insert(
426            KeyBinding::new(KeyCode::Delete),
427            "delete_char_forward".into(),
428        );
429        self.search_map
430            .insert(KeyBinding::new(KeyCode::Left), "move_cursor_left".into());
431        self.search_map
432            .insert(KeyBinding::new(KeyCode::Right), "move_cursor_right".into());
433        self.search_map.insert(
434            KeyBinding::with_ctrl(KeyCode::Char('u')),
435            "clear_input".into(),
436        );
437    }
438
439    fn setup_filter_bindings(&mut self) {
440        self.filter_map
441            .insert(KeyBinding::new(KeyCode::Enter), "apply_filter".into());
442        self.filter_map
443            .insert(KeyBinding::new(KeyCode::Esc), "cancel_filter".into());
444        self.filter_map.insert(
445            KeyBinding::new(KeyCode::Backspace),
446            "delete_char_backward".into(),
447        );
448        self.filter_map.insert(
449            KeyBinding::new(KeyCode::Delete),
450            "delete_char_forward".into(),
451        );
452        self.filter_map
453            .insert(KeyBinding::new(KeyCode::Left), "move_cursor_left".into());
454        self.filter_map
455            .insert(KeyBinding::new(KeyCode::Right), "move_cursor_right".into());
456        self.filter_map.insert(
457            KeyBinding::with_ctrl(KeyCode::Char('u')),
458            "clear_input".into(),
459        );
460    }
461
462    fn setup_help_bindings(&mut self) {
463        self.help_map
464            .insert(KeyBinding::new(KeyCode::Esc), "exit_help".into());
465        self.help_map
466            .insert(KeyBinding::new(KeyCode::Char('q')), "exit_help".into());
467        self.help_map
468            .insert(KeyBinding::new(KeyCode::Down), "scroll_help_down".into());
469        self.help_map
470            .insert(KeyBinding::new(KeyCode::Up), "scroll_help_up".into());
471        self.help_map
472            .insert(KeyBinding::new(KeyCode::PageDown), "help_page_down".into());
473        self.help_map
474            .insert(KeyBinding::new(KeyCode::PageUp), "help_page_up".into());
475    }
476
477    fn setup_debug_bindings(&mut self) {
478        self.debug_map
479            .insert(KeyBinding::new(KeyCode::Esc), "exit_debug".into());
480        self.debug_map
481            .insert(KeyBinding::new(KeyCode::Enter), "exit_debug".into());
482        self.debug_map
483            .insert(KeyBinding::new(KeyCode::Char('q')), "exit_debug".into());
484        self.debug_map
485            .insert(KeyBinding::new(KeyCode::Down), "scroll_debug_down".into());
486        self.debug_map
487            .insert(KeyBinding::new(KeyCode::Up), "scroll_debug_up".into());
488        self.debug_map
489            .insert(KeyBinding::new(KeyCode::PageDown), "debug_page_down".into());
490        self.debug_map
491            .insert(KeyBinding::new(KeyCode::PageUp), "debug_page_up".into());
492        self.debug_map
493            .insert(KeyBinding::new(KeyCode::Home), "debug_go_to_top".into());
494        self.debug_map
495            .insert(KeyBinding::new(KeyCode::End), "debug_go_to_bottom".into());
496        // Vim-style navigation
497        self.debug_map.insert(
498            KeyBinding::new(KeyCode::Char('j')),
499            "scroll_debug_down".into(),
500        );
501        self.debug_map.insert(
502            KeyBinding::new(KeyCode::Char('k')),
503            "scroll_debug_up".into(),
504        );
505        self.debug_map.insert(
506            KeyBinding::new(KeyCode::Char('g')),
507            "debug_go_to_top".into(),
508        );
509        // Uppercase G comes with SHIFT modifier in crossterm
510        self.debug_map.insert(
511            KeyBinding::with_shift(KeyCode::Char('G')),
512            "debug_go_to_bottom".into(),
513        );
514
515        // Yank functionality in debug mode
516        self.debug_map.insert(
517            KeyBinding::with_ctrl(KeyCode::Char('t')),
518            "yank_as_test_case".into(),
519        );
520        self.debug_map.insert(
521            KeyBinding::with_shift(KeyCode::Char('Y')),
522            "yank_debug_context".into(),
523        );
524    }
525
526    /// Get action for a key in command mode
527    #[must_use]
528    pub fn get_command_action(&self, key: &KeyEvent) -> Option<&str> {
529        let binding = KeyBinding::from_event(key);
530        self.command_map
531            .get(&binding)
532            .map(std::string::String::as_str)
533    }
534
535    /// Get action for a key in results mode
536    #[must_use]
537    pub fn get_results_action(&self, key: &KeyEvent) -> Option<&str> {
538        let binding = KeyBinding::from_event(key);
539        self.results_map
540            .get(&binding)
541            .map(std::string::String::as_str)
542    }
543
544    /// Get action for a key in search mode
545    #[must_use]
546    pub fn get_search_action(&self, key: &KeyEvent) -> Option<&str> {
547        let binding = KeyBinding::from_event(key);
548        self.search_map
549            .get(&binding)
550            .map(std::string::String::as_str)
551    }
552
553    /// Get action for a key in filter mode
554    #[must_use]
555    pub fn get_filter_action(&self, key: &KeyEvent) -> Option<&str> {
556        let binding = KeyBinding::from_event(key);
557        self.filter_map
558            .get(&binding)
559            .map(std::string::String::as_str)
560    }
561
562    /// Get action for a key in help mode
563    #[must_use]
564    pub fn get_help_action(&self, key: &KeyEvent) -> Option<&str> {
565        let binding = KeyBinding::from_event(key);
566        self.help_map.get(&binding).map(std::string::String::as_str)
567    }
568
569    /// Get action for a key in debug mode
570    #[must_use]
571    pub fn get_debug_action(&self, key: &KeyEvent) -> Option<&str> {
572        let binding = KeyBinding::from_event(key);
573        self.debug_map
574            .get(&binding)
575            .map(std::string::String::as_str)
576    }
577
578    /// Load custom bindings from config (future feature)
579    pub fn load_custom_bindings(&mut self, mode: &str, _bindings: HashMap<String, String>) {
580        let _map = match mode {
581            "command" => &mut self.command_map,
582            "results" => &mut self.results_map,
583            "search" => &mut self.search_map,
584            "filter" => &mut self.filter_map,
585            "help" => &mut self.help_map,
586            "debug" => &mut self.debug_map,
587            _ => return,
588        };
589
590        // Parse and add custom bindings
591        // Format: "Ctrl+X" -> "expand_asterisk"
592        // This would parse the key string and create the binding
593    }
594}