Skip to main content

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