agent_core/tui/keys/
nav.rs

1//! Navigation helper for widgets to respect configured key bindings.
2//!
3//! When modal widgets block input, they receive raw `KeyEvent`s and bypass the
4//! `KeyBindings` system. The `NavigationHelper` wraps `KeyBindings` and provides
5//! semantic key checking methods so widgets can honor user-configured bindings.
6
7use crossterm::event::KeyEvent;
8
9use super::bindings::KeyBindings;
10
11/// Helper for widgets to check navigation keys against configured bindings.
12///
13/// Pass this to widgets so they can respect configured key bindings instead of
14/// hardcoding key codes.
15///
16/// # Example
17///
18/// ```ignore
19/// fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult {
20///     if ctx.nav.is_move_up(&key) {
21///         self.select_previous();
22///         WidgetKeyResult::Handled
23///     } else if ctx.nav.is_move_down(&key) {
24///         self.select_next();
25///         WidgetKeyResult::Handled
26///     } else if ctx.nav.is_select(&key) {
27///         // Handle selection
28///     } else if ctx.nav.is_cancel(&key) {
29///         // Handle cancel
30///     } else {
31///         WidgetKeyResult::NotHandled
32///     }
33/// }
34/// ```
35pub struct NavigationHelper<'a> {
36    bindings: &'a KeyBindings,
37}
38
39impl<'a> NavigationHelper<'a> {
40    /// Create a new navigation helper from key bindings.
41    pub fn new(bindings: &'a KeyBindings) -> Self {
42        Self { bindings }
43    }
44
45    /// Check if the key matches the move up binding.
46    pub fn is_move_up(&self, key: &KeyEvent) -> bool {
47        KeyBindings::matches_any(&self.bindings.move_up, key)
48    }
49
50    /// Check if the key matches the move down binding.
51    pub fn is_move_down(&self, key: &KeyEvent) -> bool {
52        KeyBindings::matches_any(&self.bindings.move_down, key)
53    }
54
55    /// Check if the key matches the move left binding.
56    pub fn is_move_left(&self, key: &KeyEvent) -> bool {
57        KeyBindings::matches_any(&self.bindings.move_left, key)
58    }
59
60    /// Check if the key matches the move right binding.
61    pub fn is_move_right(&self, key: &KeyEvent) -> bool {
62        KeyBindings::matches_any(&self.bindings.move_right, key)
63    }
64
65    /// Check if the key matches the select binding (Enter, Space).
66    pub fn is_select(&self, key: &KeyEvent) -> bool {
67        KeyBindings::matches_any(&self.bindings.select, key)
68    }
69
70    /// Check if the key matches the cancel binding (Esc).
71    pub fn is_cancel(&self, key: &KeyEvent) -> bool {
72        KeyBindings::matches_any(&self.bindings.cancel, key)
73    }
74
75    /// Check if the key matches the submit binding (Enter).
76    pub fn is_submit(&self, key: &KeyEvent) -> bool {
77        KeyBindings::matches_any(&self.bindings.submit, key)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crossterm::event::{KeyCode, KeyModifiers};
85
86    #[test]
87    fn test_navigation_helper_emacs_bindings() {
88        let bindings = KeyBindings::emacs();
89        let nav = NavigationHelper::new(&bindings);
90
91        // Ctrl+P should match move_up
92        let ctrl_p = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
93        assert!(nav.is_move_up(&ctrl_p));
94
95        // Up arrow should also match move_up
96        let up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
97        assert!(nav.is_move_up(&up));
98
99        // Ctrl+N should match move_down
100        let ctrl_n = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL);
101        assert!(nav.is_move_down(&ctrl_n));
102
103        // Down arrow should also match move_down
104        let down = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
105        assert!(nav.is_move_down(&down));
106    }
107
108    #[test]
109    fn test_navigation_helper_select_cancel() {
110        let bindings = KeyBindings::emacs();
111        let nav = NavigationHelper::new(&bindings);
112
113        // Enter should match select
114        let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
115        assert!(nav.is_select(&enter));
116
117        // Space should match select
118        let space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
119        assert!(nav.is_select(&space));
120
121        // Esc should match cancel
122        let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
123        assert!(nav.is_cancel(&esc));
124    }
125
126    #[test]
127    fn test_navigation_helper_minimal_bindings() {
128        let bindings = KeyBindings::minimal();
129        let nav = NavigationHelper::new(&bindings);
130
131        // Only arrow keys should work for navigation
132        let up = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
133        assert!(nav.is_move_up(&up));
134
135        // Ctrl+P should NOT match move_up in minimal bindings
136        let ctrl_p = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
137        assert!(!nav.is_move_up(&ctrl_p));
138    }
139}