Skip to main content

imp_tui/
keybindings.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2
3/// High-level action triggered by a key binding.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum Action {
6    // Editor actions
7    Submit,
8    NewLine,
9    InsertChar(char),
10    Backspace,
11    Delete,
12    CursorLeft,
13    CursorRight,
14    CursorUp,
15    CursorDown,
16    CursorHome,
17    CursorEnd,
18    WordLeft,
19    WordRight,
20    DeleteWordBack,
21    DeleteToStart,
22    DeleteToEnd,
23    SelectAll,
24    // Navigation
25    ScrollUp,
26    ScrollDown,
27    PageUp,
28    PageDown,
29    // Agent
30    Cancel,
31    FollowUp,
32    // Mode switching
33    SelectModel,
34    CycleModelForward,
35    CycleModelBackward,
36    CycleThinking,
37    Peek,
38    SidebarToggle,
39    SessionTree,
40    Reload,
41    Quit,
42    // Overlays
43    OpenCommandPalette,
44    // Overlay navigation
45    OverlayUp,
46    OverlayDown,
47    OverlaySelect,
48    OverlayDismiss,
49    OverlayFilter(char),
50    OverlayBackspace,
51    // Tool call navigation
52    ToolFocusNext,
53    ToolFocusPrev,
54    /// Open the file referenced by the selected read tool call.
55    OpenSelectedReadFile,
56    /// Toggle the focused tool call's expansion (or all if no focus).
57    ToolToggle,
58}
59
60/// Resolve a key event to an action in normal mode.
61pub fn resolve_normal(key: KeyEvent) -> Option<Action> {
62    let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
63    let shift = key.modifiers.contains(KeyModifiers::SHIFT);
64    let alt = key.modifiers.contains(KeyModifiers::ALT);
65
66    match key.code {
67        // Submit / newline
68        KeyCode::Enter if alt => Some(Action::FollowUp),
69        KeyCode::Enter if shift => Some(Action::NewLine),
70        KeyCode::Enter => Some(Action::Submit),
71        KeyCode::Char('j') if ctrl => Some(Action::NewLine),
72
73        // Cancel / quit
74        KeyCode::Char('c') if ctrl => Some(Action::Cancel),
75        KeyCode::Esc => Some(Action::Cancel),
76
77        // Model / thinking
78        KeyCode::Char('l') if ctrl => Some(Action::SelectModel),
79        KeyCode::Char('p') if ctrl && shift => Some(Action::CycleModelBackward),
80        KeyCode::Char('p') if ctrl => Some(Action::CycleModelForward),
81        KeyCode::Char('o') if ctrl => Some(Action::OpenSelectedReadFile),
82        KeyCode::BackTab => Some(Action::CycleThinking),
83
84        // Sidebar / tool navigation
85        KeyCode::Tab => Some(Action::SidebarToggle),
86
87        // Cursor movement
88        KeyCode::Left if ctrl => Some(Action::WordLeft),
89        KeyCode::Right if ctrl => Some(Action::WordRight),
90        KeyCode::Left => Some(Action::CursorLeft),
91        KeyCode::Right => Some(Action::CursorRight),
92        KeyCode::Up if ctrl => Some(Action::ToolFocusPrev),
93        KeyCode::Down if ctrl => Some(Action::ToolFocusNext),
94        KeyCode::Up => Some(Action::CursorUp),
95        KeyCode::Down => Some(Action::CursorDown),
96        KeyCode::Home => Some(Action::CursorHome),
97        KeyCode::End => Some(Action::CursorEnd),
98
99        // Editing shortcuts
100        KeyCode::Char('a') if ctrl => Some(Action::CursorHome),
101        KeyCode::Char('e') if ctrl => Some(Action::CursorEnd),
102        KeyCode::Char('w') if ctrl => Some(Action::DeleteWordBack),
103        KeyCode::Char('u') if ctrl => Some(Action::DeleteToStart),
104        KeyCode::Char('k') if ctrl => Some(Action::DeleteToEnd),
105
106        // Delete
107        KeyCode::Backspace => Some(Action::Backspace),
108        KeyCode::Delete => Some(Action::Delete),
109
110        // Scroll
111        KeyCode::PageUp => Some(Action::PageUp),
112        KeyCode::PageDown => Some(Action::PageDown),
113        KeyCode::Char('b') if ctrl => Some(Action::PageUp),
114        KeyCode::Char('f') if ctrl => Some(Action::PageDown),
115
116        // Character input
117        KeyCode::Char(c) => Some(Action::InsertChar(c)),
118
119        _ => None,
120    }
121}
122
123/// Resolve a key event to an action in overlay mode (model selector, command palette, file finder).
124pub fn resolve_overlay(key: KeyEvent) -> Option<Action> {
125    let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
126
127    match key.code {
128        KeyCode::Up => Some(Action::OverlayUp),
129        KeyCode::Down => Some(Action::OverlayDown),
130        KeyCode::Tab => Some(Action::OverlayDown),
131        KeyCode::BackTab => Some(Action::OverlayUp),
132        KeyCode::Char('n') if ctrl => Some(Action::OverlayDown),
133        KeyCode::Char('p') if ctrl => Some(Action::OverlayUp),
134        KeyCode::Enter => Some(Action::OverlaySelect),
135        KeyCode::Esc => Some(Action::OverlayDismiss),
136        KeyCode::Backspace => Some(Action::OverlayBackspace),
137        KeyCode::Char(c) => Some(Action::OverlayFilter(c)),
138        _ => None,
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn ctrl_p_cycles_model_forward() {
148        assert_eq!(
149            resolve_normal(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)),
150            Some(Action::CycleModelForward)
151        );
152    }
153
154    #[test]
155    fn ctrl_shift_p_cycles_model_backward() {
156        assert_eq!(
157            resolve_normal(KeyEvent::new(
158                KeyCode::Char('p'),
159                KeyModifiers::CONTROL | KeyModifiers::SHIFT
160            )),
161            Some(Action::CycleModelBackward)
162        );
163    }
164
165    #[test]
166    fn tab_toggles_sidebar() {
167        assert_eq!(
168            resolve_normal(KeyEvent::new(KeyCode::Tab, KeyModifiers::empty())),
169            Some(Action::SidebarToggle)
170        );
171    }
172
173    #[test]
174    fn ctrl_p_no_longer_toggles_sidebar() {
175        assert_ne!(
176            resolve_normal(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)),
177            Some(Action::SidebarToggle)
178        );
179    }
180}