sql_cli/handlers/
navigation.rs

1// Navigation key handler
2// Handles all movement-related key events in Results mode
3
4use crate::ui::input::actions::{Action, NavigateAction};
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6
7pub struct NavigationHandler;
8
9impl NavigationHandler {
10    pub fn new() -> Self {
11        NavigationHandler
12    }
13
14    /// Process navigation keys and convert to actions
15    /// Returns Some(Action) if key was handled, None otherwise
16    pub fn handle_key(&self, key: KeyEvent, mode: &crate::buffer::AppMode) -> Option<Action> {
17        use crate::buffer::AppMode;
18
19        // Only handle navigation in Results mode (for now)
20        if !matches!(mode, AppMode::Results) {
21            return None;
22        }
23
24        match key.code {
25            // Vim-style navigation
26            KeyCode::Char('h') if !key.modifiers.contains(KeyModifiers::SHIFT) => {
27                Some(Action::Navigate(NavigateAction::Left(1)))
28            }
29            KeyCode::Char('j') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
30                Some(Action::Navigate(NavigateAction::Down(1)))
31            }
32            KeyCode::Char('k') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
33                Some(Action::Navigate(NavigateAction::Up(1)))
34            }
35            KeyCode::Char('l') => Some(Action::Navigate(NavigateAction::Right(1))),
36
37            // Arrow keys
38            KeyCode::Up if !key.modifiers.contains(KeyModifiers::ALT) => {
39                Some(Action::Navigate(NavigateAction::Up(1)))
40            }
41            KeyCode::Down if !key.modifiers.contains(KeyModifiers::ALT) => {
42                Some(Action::Navigate(NavigateAction::Down(1)))
43            }
44            KeyCode::Left
45                if !key.modifiers.contains(KeyModifiers::SHIFT)
46                    && !key.modifiers.contains(KeyModifiers::CONTROL) =>
47            {
48                Some(Action::Navigate(NavigateAction::Left(1)))
49            }
50            KeyCode::Right
51                if !key.modifiers.contains(KeyModifiers::SHIFT)
52                    && !key.modifiers.contains(KeyModifiers::CONTROL) =>
53            {
54                Some(Action::Navigate(NavigateAction::Right(1)))
55            }
56
57            // Page navigation
58            KeyCode::PageUp => Some(Action::Navigate(NavigateAction::PageUp)),
59            KeyCode::PageDown => Some(Action::Navigate(NavigateAction::PageDown)),
60            KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
61                Some(Action::Navigate(NavigateAction::PageDown))
62            }
63            KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
64                Some(Action::Navigate(NavigateAction::PageUp))
65            }
66
67            // Home/End for vertical navigation
68            KeyCode::Home if !key.modifiers.contains(KeyModifiers::SHIFT) => {
69                Some(Action::Navigate(NavigateAction::Home))
70            }
71            KeyCode::End if !key.modifiers.contains(KeyModifiers::SHIFT) => {
72                Some(Action::Navigate(NavigateAction::End))
73            }
74
75            // g/G for top/bottom
76            KeyCode::Char('g') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
77                Some(Action::Navigate(NavigateAction::Home))
78            }
79            KeyCode::Char('G') => Some(Action::Navigate(NavigateAction::End)),
80
81            // H, M, L for viewport navigation
82            KeyCode::Char('H') if key.modifiers.contains(KeyModifiers::SHIFT) => {
83                Some(Action::NavigateToViewportTop)
84            }
85            KeyCode::Char('M') => Some(Action::NavigateToViewportMiddle),
86            KeyCode::Char('L') if key.modifiers.contains(KeyModifiers::SHIFT) => {
87                Some(Action::NavigateToViewportBottom)
88            }
89
90            // ^/$ for horizontal navigation
91            KeyCode::Char('^') => Some(Action::Navigate(NavigateAction::FirstColumn)),
92            KeyCode::Char('$') => Some(Action::Navigate(NavigateAction::LastColumn)),
93
94            // Tab navigation for columns
95            KeyCode::Tab if !key.modifiers.contains(KeyModifiers::SHIFT) => {
96                Some(Action::NextColumn)
97            }
98            KeyCode::BackTab | KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
99                Some(Action::PreviousColumn)
100            }
101
102            // Lock modes
103            KeyCode::Char('x') => Some(Action::ToggleCursorLock),
104            KeyCode::Char(' ') if key.modifiers.contains(KeyModifiers::CONTROL) => {
105                Some(Action::ToggleViewportLock)
106            }
107
108            _ => None,
109        }
110    }
111
112    /// Check if a key is a navigation key that this handler manages
113    pub fn is_navigation_key(&self, key: &KeyEvent, mode: &crate::buffer::AppMode) -> bool {
114        self.handle_key(*key, mode).is_some()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::buffer::AppMode;
122    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
123
124    #[test]
125    fn test_vim_navigation() {
126        let handler = NavigationHandler::new();
127        let mode = AppMode::Results;
128
129        // Test h,j,k,l
130        let h_key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty());
131        assert_eq!(
132            handler.handle_key(h_key, &mode),
133            Some(Action::Navigate(NavigateAction::Left(1)))
134        );
135
136        let j_key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty());
137        assert_eq!(
138            handler.handle_key(j_key, &mode),
139            Some(Action::Navigate(NavigateAction::Down(1)))
140        );
141
142        let k_key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty());
143        assert_eq!(
144            handler.handle_key(k_key, &mode),
145            Some(Action::Navigate(NavigateAction::Up(1)))
146        );
147
148        let l_key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::empty());
149        assert_eq!(
150            handler.handle_key(l_key, &mode),
151            Some(Action::Navigate(NavigateAction::Right(1)))
152        );
153    }
154
155    #[test]
156    fn test_arrow_keys() {
157        let handler = NavigationHandler::new();
158        let mode = AppMode::Results;
159
160        let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
161        assert_eq!(
162            handler.handle_key(up_key, &mode),
163            Some(Action::Navigate(NavigateAction::Up(1)))
164        );
165
166        let down_key = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
167        assert_eq!(
168            handler.handle_key(down_key, &mode),
169            Some(Action::Navigate(NavigateAction::Down(1)))
170        );
171    }
172
173    #[test]
174    fn test_page_navigation() {
175        let handler = NavigationHandler::new();
176        let mode = AppMode::Results;
177
178        let pageup_key = KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty());
179        assert_eq!(
180            handler.handle_key(pageup_key, &mode),
181            Some(Action::Navigate(NavigateAction::PageUp))
182        );
183
184        let pagedown_key = KeyEvent::new(KeyCode::PageDown, KeyModifiers::empty());
185        assert_eq!(
186            handler.handle_key(pagedown_key, &mode),
187            Some(Action::Navigate(NavigateAction::PageDown))
188        );
189    }
190
191    #[test]
192    fn test_not_in_results_mode() {
193        let handler = NavigationHandler::new();
194        let mode = AppMode::Command;
195
196        let h_key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty());
197        assert_eq!(handler.handle_key(h_key, &mode), None);
198    }
199}