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