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