Skip to main content

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 [`minimal()`](Self::minimal) which provides simple arrow-key
16/// navigation. For power users, use [`emacs()`](Self::emacs) for full Emacs-style bindings.
17#[derive(Debug, Clone)]
18pub struct KeyBindings {
19    // Navigation
20    /// Move cursor up.
21    pub move_up: Vec<KeyCombo>,
22    /// Move cursor down.
23    pub move_down: Vec<KeyCombo>,
24    /// Move cursor left.
25    pub move_left: Vec<KeyCombo>,
26    /// Move cursor right.
27    pub move_right: Vec<KeyCombo>,
28    /// Move to line start.
29    pub move_line_start: Vec<KeyCombo>,
30    /// Move to line end.
31    pub move_line_end: Vec<KeyCombo>,
32
33    // Editing
34    /// Delete char before cursor.
35    pub delete_char_before: Vec<KeyCombo>,
36    /// Delete char at cursor.
37    pub delete_char_at: Vec<KeyCombo>,
38    /// Kill to end of line.
39    pub kill_line: Vec<KeyCombo>,
40    /// Insert newline in multi-line input.
41    pub insert_newline: Vec<KeyCombo>,
42
43    // Application
44    /// Submit message.
45    pub submit: Vec<KeyCombo>,
46    /// Interrupt current request.
47    pub interrupt: Vec<KeyCombo>,
48    /// Quit immediately (only when input empty and no modal is blocking).
49    pub quit: Vec<KeyCombo>,
50    /// Force quit (works even in modals).
51    pub force_quit: Vec<KeyCombo>,
52    /// Enter exit confirmation mode (requires pressing twice to exit).
53    pub enter_exit_mode: Vec<KeyCombo>,
54    /// Timeout in seconds for exit confirmation mode.
55    pub exit_timeout_secs: u64,
56
57    // Widget navigation
58    /// Select/confirm in widgets (Enter, Space).
59    pub select: Vec<KeyCombo>,
60    /// Cancel/close in widgets (Esc).
61    pub cancel: Vec<KeyCombo>,
62}
63
64impl Default for KeyBindings {
65    fn default() -> Self {
66        Self::minimal()
67    }
68}
69
70impl KeyBindings {
71    /// Emacs-style bindings.
72    ///
73    /// Full-featured bindings for power users:
74    /// - Ctrl+P/N/B/F for navigation
75    /// - Ctrl+A/E for line start/end
76    /// - Ctrl+K to kill line
77    /// - Ctrl+D for exit mode (or delete char if not empty)
78    /// - Esc to interrupt
79    pub fn emacs() -> Self {
80        Self {
81            move_up: vec![KeyCombo::key(KeyCode::Up), KeyCombo::ctrl('p')],
82            move_down: vec![KeyCombo::key(KeyCode::Down), KeyCombo::ctrl('n')],
83            move_left: vec![KeyCombo::key(KeyCode::Left), KeyCombo::ctrl('b')],
84            move_right: vec![KeyCombo::key(KeyCode::Right), KeyCombo::ctrl('f')],
85            move_line_start: vec![KeyCombo::key(KeyCode::Home), KeyCombo::ctrl('a')],
86            move_line_end: vec![KeyCombo::key(KeyCode::End), KeyCombo::ctrl('e')],
87
88            delete_char_before: vec![KeyCombo::key(KeyCode::Backspace)],
89            delete_char_at: vec![KeyCombo::key(KeyCode::Delete)],
90            kill_line: vec![KeyCombo::ctrl('k')],
91            insert_newline: vec![KeyCombo::ctrl('j')],
92
93            submit: vec![KeyCombo::key(KeyCode::Enter)],
94            interrupt: vec![KeyCombo::key(KeyCode::Esc)],
95            quit: vec![], // No direct quit, use exit mode
96            force_quit: vec![KeyCombo::ctrl('q')],
97            enter_exit_mode: vec![KeyCombo::ctrl('d')],
98            exit_timeout_secs: DEFAULT_EXIT_TIMEOUT_SECS,
99
100            select: vec![KeyCombo::key(KeyCode::Enter), KeyCombo::key(KeyCode::Char(' '))],
101            cancel: vec![KeyCombo::key(KeyCode::Esc)],
102        }
103    }
104
105    /// Minimal bindings (arrows only, Esc to quit).
106    ///
107    /// This is simpler for users unfamiliar with Emacs:
108    /// - Arrow keys only for navigation
109    /// - Esc quits (when input empty and no modal)
110    /// - No Ctrl key requirements for basic use
111    pub fn minimal() -> Self {
112        Self {
113            move_up: vec![KeyCombo::key(KeyCode::Up)],
114            move_down: vec![KeyCombo::key(KeyCode::Down)],
115            move_left: vec![KeyCombo::key(KeyCode::Left)],
116            move_right: vec![KeyCombo::key(KeyCode::Right)],
117            move_line_start: vec![KeyCombo::key(KeyCode::Home)],
118            move_line_end: vec![KeyCombo::key(KeyCode::End)],
119
120            delete_char_before: vec![KeyCombo::key(KeyCode::Backspace)],
121            delete_char_at: vec![KeyCombo::key(KeyCode::Delete)],
122            kill_line: vec![],
123            insert_newline: vec![KeyCombo::ctrl('j')],
124
125            submit: vec![KeyCombo::key(KeyCode::Enter)],
126            interrupt: vec![],
127            quit: vec![KeyCombo::key(KeyCode::Esc)], // Esc quits (when no modal)
128            force_quit: vec![KeyCombo::ctrl('q')],   // Ctrl+Q always works
129            enter_exit_mode: vec![],
130            exit_timeout_secs: DEFAULT_EXIT_TIMEOUT_SECS,
131
132            select: vec![KeyCombo::key(KeyCode::Enter), KeyCombo::key(KeyCode::Char(' '))],
133            cancel: vec![KeyCombo::key(KeyCode::Esc)],
134        }
135    }
136
137    /// Check if any combo in a list matches the key event.
138    pub(crate) fn matches_any(combos: &[KeyCombo], event: &KeyEvent) -> bool {
139        combos.iter().any(|combo| combo.matches(event))
140    }
141
142    // -------------------------------------------------------------------------
143    // Builder pattern methods: with_* setters
144    // -------------------------------------------------------------------------
145
146    /// Set the move up key bindings.
147    pub fn with_move_up(mut self, combos: Vec<KeyCombo>) -> Self {
148        self.move_up = combos;
149        self
150    }
151
152    /// Set the move down key bindings.
153    pub fn with_move_down(mut self, combos: Vec<KeyCombo>) -> Self {
154        self.move_down = combos;
155        self
156    }
157
158    /// Set the move left key bindings.
159    pub fn with_move_left(mut self, combos: Vec<KeyCombo>) -> Self {
160        self.move_left = combos;
161        self
162    }
163
164    /// Set the move right key bindings.
165    pub fn with_move_right(mut self, combos: Vec<KeyCombo>) -> Self {
166        self.move_right = combos;
167        self
168    }
169
170    /// Set the move to line start key bindings.
171    pub fn with_move_line_start(mut self, combos: Vec<KeyCombo>) -> Self {
172        self.move_line_start = combos;
173        self
174    }
175
176    /// Set the move to line end key bindings.
177    pub fn with_move_line_end(mut self, combos: Vec<KeyCombo>) -> Self {
178        self.move_line_end = combos;
179        self
180    }
181
182    /// Set the delete char before (backspace) key bindings.
183    pub fn with_delete_char_before(mut self, combos: Vec<KeyCombo>) -> Self {
184        self.delete_char_before = combos;
185        self
186    }
187
188    /// Set the delete char at (delete) key bindings.
189    pub fn with_delete_char_at(mut self, combos: Vec<KeyCombo>) -> Self {
190        self.delete_char_at = combos;
191        self
192    }
193
194    /// Set the kill line key bindings.
195    pub fn with_kill_line(mut self, combos: Vec<KeyCombo>) -> Self {
196        self.kill_line = combos;
197        self
198    }
199
200    /// Set the insert newline key bindings.
201    pub fn with_insert_newline(mut self, combos: Vec<KeyCombo>) -> Self {
202        self.insert_newline = combos;
203        self
204    }
205
206    /// Set the submit key bindings.
207    pub fn with_submit(mut self, combos: Vec<KeyCombo>) -> Self {
208        self.submit = combos;
209        self
210    }
211
212    /// Set the interrupt key bindings.
213    pub fn with_interrupt(mut self, combos: Vec<KeyCombo>) -> Self {
214        self.interrupt = combos;
215        self
216    }
217
218    /// Set the quit key bindings.
219    pub fn with_quit(mut self, combos: Vec<KeyCombo>) -> Self {
220        self.quit = combos;
221        self
222    }
223
224    /// Set the force quit key bindings.
225    pub fn with_force_quit(mut self, combos: Vec<KeyCombo>) -> Self {
226        self.force_quit = combos;
227        self
228    }
229
230    /// Set the enter exit mode key bindings.
231    pub fn with_enter_exit_mode(mut self, combos: Vec<KeyCombo>) -> Self {
232        self.enter_exit_mode = combos;
233        self
234    }
235
236    /// Set the exit timeout in seconds.
237    pub fn with_exit_timeout_secs(mut self, secs: u64) -> Self {
238        self.exit_timeout_secs = secs;
239        self
240    }
241
242    /// Set the select key bindings (for widget selection).
243    pub fn with_select(mut self, combos: Vec<KeyCombo>) -> Self {
244        self.select = combos;
245        self
246    }
247
248    /// Set the cancel key bindings (for widget cancellation).
249    pub fn with_cancel(mut self, combos: Vec<KeyCombo>) -> Self {
250        self.cancel = combos;
251        self
252    }
253
254    // -------------------------------------------------------------------------
255    // Builder pattern methods: without_* for disabling
256    // -------------------------------------------------------------------------
257
258    /// Disable exit mode (sets enter_exit_mode to empty).
259    pub fn without_exit_mode(mut self) -> Self {
260        self.enter_exit_mode = vec![];
261        self
262    }
263
264    /// Disable quit binding (sets quit to empty).
265    pub fn without_quit(mut self) -> Self {
266        self.quit = vec![];
267        self
268    }
269
270    /// Disable force quit binding (sets force_quit to empty).
271    pub fn without_force_quit(mut self) -> Self {
272        self.force_quit = vec![];
273        self
274    }
275
276    /// Disable interrupt binding (sets interrupt to empty).
277    pub fn without_interrupt(mut self) -> Self {
278        self.interrupt = vec![];
279        self
280    }
281
282    /// Disable kill line binding (sets kill_line to empty).
283    pub fn without_kill_line(mut self) -> Self {
284        self.kill_line = vec![];
285        self
286    }
287
288    /// Disable insert newline binding (sets insert_newline to empty).
289    pub fn without_insert_newline(mut self) -> Self {
290        self.insert_newline = vec![];
291        self
292    }
293
294    // -------------------------------------------------------------------------
295    // Builder pattern methods: add_* for appending
296    // -------------------------------------------------------------------------
297
298    /// Add a key combo to the move up bindings.
299    pub fn add_move_up(mut self, combo: KeyCombo) -> Self {
300        self.move_up.push(combo);
301        self
302    }
303
304    /// Add a key combo to the move down bindings.
305    pub fn add_move_down(mut self, combo: KeyCombo) -> Self {
306        self.move_down.push(combo);
307        self
308    }
309
310    /// Add a key combo to the move left bindings.
311    pub fn add_move_left(mut self, combo: KeyCombo) -> Self {
312        self.move_left.push(combo);
313        self
314    }
315
316    /// Add a key combo to the move right bindings.
317    pub fn add_move_right(mut self, combo: KeyCombo) -> Self {
318        self.move_right.push(combo);
319        self
320    }
321
322    /// Add a key combo to the quit bindings.
323    pub fn add_quit(mut self, combo: KeyCombo) -> Self {
324        self.quit.push(combo);
325        self
326    }
327
328    /// Add a key combo to the submit bindings.
329    pub fn add_submit(mut self, combo: KeyCombo) -> Self {
330        self.submit.push(combo);
331        self
332    }
333
334    /// Add a key combo to the interrupt bindings.
335    pub fn add_interrupt(mut self, combo: KeyCombo) -> Self {
336        self.interrupt.push(combo);
337        self
338    }
339
340    /// Add a key combo to the enter exit mode bindings.
341    pub fn add_enter_exit_mode(mut self, combo: KeyCombo) -> Self {
342        self.enter_exit_mode.push(combo);
343        self
344    }
345
346    /// Add a key combo to the force quit bindings.
347    pub fn add_force_quit(mut self, combo: KeyCombo) -> Self {
348        self.force_quit.push(combo);
349        self
350    }
351
352    /// Add a key combo to the select bindings.
353    pub fn add_select(mut self, combo: KeyCombo) -> Self {
354        self.select.push(combo);
355        self
356    }
357
358    /// Add a key combo to the cancel bindings.
359    pub fn add_cancel(mut self, combo: KeyCombo) -> Self {
360        self.cancel.push(combo);
361        self
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_emacs_bindings() {
371        let bindings = KeyBindings::emacs();
372
373        // Ctrl+P should be in move_up
374        let ctrl_p = KeyCombo::ctrl('p');
375        assert!(bindings.move_up.contains(&ctrl_p));
376
377        // Up arrow should also be in move_up
378        let up = KeyCombo::key(KeyCode::Up);
379        assert!(bindings.move_up.contains(&up));
380
381        // quit should be empty (use exit mode instead)
382        assert!(bindings.quit.is_empty());
383    }
384
385    #[test]
386    fn test_minimal_bindings() {
387        let bindings = KeyBindings::minimal();
388
389        // Esc should quit (not just interrupt)
390        let esc = KeyCombo::key(KeyCode::Esc);
391        assert!(bindings.quit.contains(&esc));
392
393        // No Emacs bindings
394        let ctrl_p = KeyCombo::ctrl('p');
395        assert!(!bindings.move_up.contains(&ctrl_p));
396    }
397
398    #[test]
399    fn test_builder_with_methods() {
400        // Start with minimal and override some bindings
401        let bindings = KeyBindings::minimal()
402            .with_quit(vec![KeyCombo::ctrl('w')])
403            .with_submit(vec![KeyCombo::key(KeyCode::Enter), KeyCombo::ctrl('m')]);
404
405        // quit should be replaced with Ctrl+W
406        assert_eq!(bindings.quit.len(), 1);
407        assert!(bindings.quit.contains(&KeyCombo::ctrl('w')));
408
409        // submit should have both Enter and Ctrl+M
410        assert_eq!(bindings.submit.len(), 2);
411        assert!(bindings.submit.contains(&KeyCombo::key(KeyCode::Enter)));
412        assert!(bindings.submit.contains(&KeyCombo::ctrl('m')));
413    }
414
415    #[test]
416    fn test_builder_without_methods() {
417        // Start with emacs and disable some features
418        let bindings = KeyBindings::emacs()
419            .without_exit_mode()
420            .without_kill_line();
421
422        // Exit mode should be empty
423        assert!(bindings.enter_exit_mode.is_empty());
424
425        // Kill line should be empty
426        assert!(bindings.kill_line.is_empty());
427
428        // Other bindings should still exist
429        assert!(!bindings.move_up.is_empty());
430        assert!(!bindings.submit.is_empty());
431    }
432
433    #[test]
434    fn test_builder_add_methods() {
435        // Start with minimal and add extra bindings
436        let bindings = KeyBindings::minimal()
437            .add_quit(KeyCombo::ctrl('c'))
438            .add_submit(KeyCombo::ctrl('s'));
439
440        // quit should have both Esc (original) and Ctrl+C (added)
441        assert!(bindings.quit.contains(&KeyCombo::key(KeyCode::Esc)));
442        assert!(bindings.quit.contains(&KeyCombo::ctrl('c')));
443
444        // submit should have both Enter (original) and Ctrl+S (added)
445        assert!(bindings.submit.contains(&KeyCombo::key(KeyCode::Enter)));
446        assert!(bindings.submit.contains(&KeyCombo::ctrl('s')));
447    }
448
449    #[test]
450    fn test_builder_chaining() {
451        // Test a complex chain of builder methods
452        let bindings = KeyBindings::minimal()
453            .with_move_up(vec![KeyCombo::key(KeyCode::Up), KeyCombo::ctrl('p')])
454            .with_move_down(vec![KeyCombo::key(KeyCode::Down), KeyCombo::ctrl('n')])
455            .without_quit()
456            .with_enter_exit_mode(vec![KeyCombo::ctrl('d')])
457            .with_exit_timeout_secs(5)
458            .add_force_quit(KeyCombo::ctrl('c'));
459
460        // Verify all customizations
461        assert!(bindings.move_up.contains(&KeyCombo::ctrl('p')));
462        assert!(bindings.move_down.contains(&KeyCombo::ctrl('n')));
463        assert!(bindings.quit.is_empty());
464        assert!(bindings.enter_exit_mode.contains(&KeyCombo::ctrl('d')));
465        assert_eq!(bindings.exit_timeout_secs, 5);
466        assert!(bindings.force_quit.contains(&KeyCombo::ctrl('c')));
467    }
468}