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