agent_core/tui/keys/
bindings.rs

1//! Key binding presets and configuration.
2
3use crossterm::event::{KeyCode, KeyEvent};
4
5use super::types::KeyCombo;
6
7/// Default exit confirmation timeout in seconds.
8pub const DEFAULT_EXIT_TIMEOUT_SECS: u64 = 2;
9
10/// Key binding configuration.
11///
12/// Specifies which key combinations trigger which actions.
13/// Multiple key combinations can be assigned to the same action.
14///
15/// The default is [`bare_minimum()`](Self::bare_minimum) which only provides
16/// basic functionality. Apps should explicitly choose their bindings:
17/// - [`emacs()`](Self::emacs) for full Emacs-style bindings
18/// - [`minimal()`](Self::minimal) for simple arrow-key navigation
19#[derive(Debug, Clone)]
20pub struct KeyBindings {
21    // Navigation
22    /// Move cursor up.
23    pub move_up: Vec<KeyCombo>,
24    /// Move cursor down.
25    pub move_down: Vec<KeyCombo>,
26    /// Move cursor left.
27    pub move_left: Vec<KeyCombo>,
28    /// Move cursor right.
29    pub move_right: Vec<KeyCombo>,
30    /// Move to line start.
31    pub move_line_start: Vec<KeyCombo>,
32    /// Move to line end.
33    pub move_line_end: Vec<KeyCombo>,
34
35    // Editing
36    /// Delete char before cursor.
37    pub delete_char_before: Vec<KeyCombo>,
38    /// Delete char at cursor.
39    pub delete_char_at: Vec<KeyCombo>,
40    /// Kill to end of line.
41    pub kill_line: Vec<KeyCombo>,
42    /// Insert newline in multi-line input.
43    pub insert_newline: Vec<KeyCombo>,
44
45    // Application
46    /// Submit message.
47    pub submit: Vec<KeyCombo>,
48    /// Interrupt current request.
49    pub interrupt: Vec<KeyCombo>,
50    /// Quit immediately (only when input empty and no modal is blocking).
51    pub quit: Vec<KeyCombo>,
52    /// Force quit (works even in modals).
53    pub force_quit: Vec<KeyCombo>,
54    /// Enter exit confirmation mode (requires pressing twice to exit).
55    pub enter_exit_mode: Vec<KeyCombo>,
56    /// Timeout in seconds for exit confirmation mode.
57    pub exit_timeout_secs: u64,
58}
59
60impl Default for KeyBindings {
61    fn default() -> Self {
62        Self::bare_minimum()
63    }
64}
65
66impl KeyBindings {
67    /// Bare minimum bindings - only Esc to quit.
68    ///
69    /// This is the default when no bindings are specified.
70    /// Apps should explicitly choose their bindings (e.g., `emacs()` or `minimal()`).
71    ///
72    /// Only provides:
73    /// - Esc to quit (when input is empty)
74    /// - Ctrl+Q force quit (always works)
75    /// - Enter to submit
76    /// - Backspace/Delete for basic editing
77    /// - Arrow keys for navigation
78    pub fn bare_minimum() -> Self {
79        Self {
80            move_up: vec![KeyCombo::key(KeyCode::Up)],
81            move_down: vec![KeyCombo::key(KeyCode::Down)],
82            move_left: vec![KeyCombo::key(KeyCode::Left)],
83            move_right: vec![KeyCombo::key(KeyCode::Right)],
84            move_line_start: vec![KeyCombo::key(KeyCode::Home)],
85            move_line_end: vec![KeyCombo::key(KeyCode::End)],
86
87            delete_char_before: vec![KeyCombo::key(KeyCode::Backspace)],
88            delete_char_at: vec![KeyCombo::key(KeyCode::Delete)],
89            kill_line: vec![],
90            insert_newline: vec![],
91
92            submit: vec![KeyCombo::key(KeyCode::Enter)],
93            interrupt: vec![],
94            quit: vec![KeyCombo::key(KeyCode::Esc)], // Esc quits when input empty
95            force_quit: vec![KeyCombo::ctrl('q')],
96            enter_exit_mode: vec![],
97            exit_timeout_secs: DEFAULT_EXIT_TIMEOUT_SECS,
98        }
99    }
100
101    /// Emacs-style bindings.
102    ///
103    /// Full-featured bindings for power users:
104    /// - Ctrl+P/N/B/F for navigation
105    /// - Ctrl+A/E for line start/end
106    /// - Ctrl+K to kill line
107    /// - Ctrl+D for exit mode (or delete char if not empty)
108    /// - Esc to interrupt
109    pub fn emacs() -> Self {
110        Self {
111            move_up: vec![KeyCombo::key(KeyCode::Up), KeyCombo::ctrl('p')],
112            move_down: vec![KeyCombo::key(KeyCode::Down), KeyCombo::ctrl('n')],
113            move_left: vec![KeyCombo::key(KeyCode::Left), KeyCombo::ctrl('b')],
114            move_right: vec![KeyCombo::key(KeyCode::Right), KeyCombo::ctrl('f')],
115            move_line_start: vec![KeyCombo::key(KeyCode::Home), KeyCombo::ctrl('a')],
116            move_line_end: vec![KeyCombo::key(KeyCode::End), KeyCombo::ctrl('e')],
117
118            delete_char_before: vec![KeyCombo::key(KeyCode::Backspace)],
119            delete_char_at: vec![KeyCombo::key(KeyCode::Delete)],
120            kill_line: vec![KeyCombo::ctrl('k')],
121            insert_newline: vec![KeyCombo::ctrl('j')],
122
123            submit: vec![KeyCombo::key(KeyCode::Enter)],
124            interrupt: vec![KeyCombo::key(KeyCode::Esc)],
125            quit: vec![], // No direct quit, use exit mode
126            force_quit: vec![KeyCombo::ctrl('q')],
127            enter_exit_mode: vec![KeyCombo::ctrl('d')],
128            exit_timeout_secs: DEFAULT_EXIT_TIMEOUT_SECS,
129        }
130    }
131
132    /// Minimal bindings (arrows only, Esc to quit).
133    ///
134    /// This is simpler for users unfamiliar with Emacs:
135    /// - Arrow keys only for navigation
136    /// - Esc quits (when input empty and no modal)
137    /// - No Ctrl key requirements for basic use
138    pub fn minimal() -> Self {
139        Self {
140            move_up: vec![KeyCombo::key(KeyCode::Up)],
141            move_down: vec![KeyCombo::key(KeyCode::Down)],
142            move_left: vec![KeyCombo::key(KeyCode::Left)],
143            move_right: vec![KeyCombo::key(KeyCode::Right)],
144            move_line_start: vec![KeyCombo::key(KeyCode::Home)],
145            move_line_end: vec![KeyCombo::key(KeyCode::End)],
146
147            delete_char_before: vec![KeyCombo::key(KeyCode::Backspace)],
148            delete_char_at: vec![KeyCombo::key(KeyCode::Delete)],
149            kill_line: vec![],
150            insert_newline: vec![KeyCombo::ctrl('j')],
151
152            submit: vec![KeyCombo::key(KeyCode::Enter)],
153            interrupt: vec![],
154            quit: vec![KeyCombo::key(KeyCode::Esc)], // Esc quits (when no modal)
155            force_quit: vec![KeyCombo::ctrl('q')],   // Ctrl+Q always works
156            enter_exit_mode: vec![],
157            exit_timeout_secs: DEFAULT_EXIT_TIMEOUT_SECS,
158        }
159    }
160
161    /// Check if any combo in a list matches the key event.
162    pub(crate) fn matches_any(combos: &[KeyCombo], event: &KeyEvent) -> bool {
163        combos.iter().any(|combo| combo.matches(event))
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_emacs_bindings() {
173        let bindings = KeyBindings::emacs();
174
175        // Ctrl+P should be in move_up
176        let ctrl_p = KeyCombo::ctrl('p');
177        assert!(bindings.move_up.contains(&ctrl_p));
178
179        // Up arrow should also be in move_up
180        let up = KeyCombo::key(KeyCode::Up);
181        assert!(bindings.move_up.contains(&up));
182
183        // quit should be empty (use exit mode instead)
184        assert!(bindings.quit.is_empty());
185    }
186
187    #[test]
188    fn test_minimal_bindings() {
189        let bindings = KeyBindings::minimal();
190
191        // Esc should quit (not just interrupt)
192        let esc = KeyCombo::key(KeyCode::Esc);
193        assert!(bindings.quit.contains(&esc));
194
195        // No Emacs bindings
196        let ctrl_p = KeyCombo::ctrl('p');
197        assert!(!bindings.move_up.contains(&ctrl_p));
198    }
199
200    #[test]
201    fn test_bare_minimum_bindings() {
202        let bindings = KeyBindings::bare_minimum();
203
204        // Esc should quit (not interrupt)
205        let esc = KeyCombo::key(KeyCode::Esc);
206        assert!(bindings.quit.contains(&esc));
207        assert!(bindings.interrupt.is_empty());
208
209        // No Emacs bindings
210        let ctrl_p = KeyCombo::ctrl('p');
211        assert!(!bindings.move_up.contains(&ctrl_p));
212
213        // No exit mode
214        assert!(bindings.enter_exit_mode.is_empty());
215    }
216}