Key Bindings
Key bindings define how keyboard input maps to actions in the TUI. The KeyBindings structure holds mappings for navigation, editing, and application control. Agent Air provides three built-in presets—bare_minimum, minimal, and emacs—and a builder API for customization.
The binding system is designed for flexibility. Each action can have multiple key combinations, allowing users familiar with different editors to use their preferred shortcuts. You can start with a preset and customize specific bindings, or build a completely custom configuration from scratch.
KeyBindings Structure
The KeyBindings struct contains vectors of key combinations for each action. Having multiple combinations per action allows alternative shortcuts for the same operation.
pub struct KeyBindings {
// Navigation
pub move_up: Vec<KeyCombo>,
pub move_down: Vec<KeyCombo>,
pub move_left: Vec<KeyCombo>,
pub move_right: Vec<KeyCombo>,
pub move_line_start: Vec<KeyCombo>,
pub move_line_end: Vec<KeyCombo>,
// Editing
pub delete_char_before: Vec<KeyCombo>,
pub delete_char_at: Vec<KeyCombo>,
pub kill_line: Vec<KeyCombo>,
pub insert_newline: Vec<KeyCombo>,
// Application
pub submit: Vec<KeyCombo>,
pub interrupt: Vec<KeyCombo>,
pub quit: Vec<KeyCombo>,
pub force_quit: Vec<KeyCombo>,
pub enter_exit_mode: Vec<KeyCombo>,
pub exit_timeout_secs: u64,
// Widget navigation
pub select: Vec<KeyCombo>,
pub cancel: Vec<KeyCombo>,
}
KeyCombo
Key combinations are represented by KeyCombo, which pairs a key code with modifier keys. The struct provides helper constructors for common patterns.
pub struct KeyCombo {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
Constructors
// Plain key
KeyCombo::key(KeyCode::Enter)
// Ctrl+key
KeyCombo::ctrl('p')
// Alt+key
KeyCombo::alt('x')
// Shift+key
KeyCombo::shift(KeyCode::Tab)
// Ctrl+Alt+key
KeyCombo::ctrl_alt('a')
// Ctrl+Shift+key
KeyCombo::ctrl_shift('z')
Built-in Presets
Agent Air provides three presets that cover common use cases. Each preset balances simplicity with functionality differently.
bare_minimum
The default preset when no bindings are specified. Provides only basic functionality with arrow keys and standard keys. This is the most intuitive for users unfamiliar with terminal conventions.
let bindings = KeyBindings::bare_minimum();
| Action | Keys |
|---|---|
| Move up | Up arrow |
| Move down | Down arrow |
| Move left | Left arrow |
| Move right | Right arrow |
| Line start | Home |
| Line end | End |
| Delete before | Backspace |
| Delete at | Delete |
| Kill line | (none) |
| Insert newline | (none) |
| Submit | Enter |
| Interrupt | (none) |
| Quit | Esc (when input empty) |
| Force quit | Ctrl+Q |
| Exit mode | (none) |
| Select | Enter, Space |
| Cancel | Esc |
minimal
Similar to bare_minimum but adds Ctrl+J for inserting newlines, useful for multi-line input:
let bindings = KeyBindings::minimal();
| Action | Keys |
|---|---|
| Move up | Up arrow |
| Move down | Down arrow |
| Move left | Left arrow |
| Move right | Right arrow |
| Line start | Home |
| Line end | End |
| Delete before | Backspace |
| Delete at | Delete |
| Kill line | (none) |
| Insert newline | Ctrl+J |
| Submit | Enter |
| Interrupt | (none) |
| Quit | Esc (when input empty and no modal) |
| Force quit | Ctrl+Q |
| Exit mode | (none) |
| Select | Enter, Space |
| Cancel | Esc |
emacs
Full Emacs-style bindings for power users. Includes Ctrl+P/N/B/F for navigation, Ctrl+A/E for line movement, and Ctrl+K for kill line. Uses a two-key exit sequence (Ctrl+D twice) instead of Esc to quit.
let bindings = KeyBindings::emacs();
| Action | Keys |
|---|---|
| Move up | Up, Ctrl+P |
| Move down | Down, Ctrl+N |
| Move left | Left, Ctrl+B |
| Move right | Right, Ctrl+F |
| Line start | Home, Ctrl+A |
| Line end | End, Ctrl+E |
| Delete before | Backspace |
| Delete at | Delete |
| Kill line | Ctrl+K |
| Insert newline | Ctrl+J |
| Submit | Enter |
| Interrupt | Esc |
| Quit | (none, use exit mode) |
| Force quit | Ctrl+Q |
| Exit mode | Ctrl+D |
| Select | Enter, Space |
| Cancel | Esc |
Exit Behavior Comparison
The presets differ significantly in how they handle exiting the application. Understanding these differences helps you choose the right preset for your users.
bare_minimum and minimal
- Esc quits immediately when input is empty and no modal is blocking
- Ctrl+Q force quits regardless of state
- Simple and intuitive for casual users
emacs
- Esc interrupts the current request (does not quit)
- Ctrl+D enters exit confirmation mode (press twice within timeout to quit)
- Ctrl+Q force quits regardless of state
- Prevents accidental exits, preferred by power users
Exit Timeout
The exit_timeout_secs field determines how long the exit confirmation window stays open. After pressing the exit key once, the user must press it again within this timeout to confirm.
pub const DEFAULT_EXIT_TIMEOUT_SECS: u64 = 2;
If the user does not press the exit key again within the timeout, exit mode is cancelled and normal operation resumes.
Preset Summary
| Feature | bare_minimum | minimal | emacs |
|---|---|---|---|
| Emacs navigation | No | No | Yes (Ctrl+P/N/B/F) |
| Emacs editing | No | No | Yes (Ctrl+A/E/K) |
| Exit key | Esc | Esc | Ctrl+D (twice) |
| Interrupt key | None | None | Esc |
| Insert newline | None | Ctrl+J | Ctrl+J |
| Kill line | None | None | Ctrl+K |
Customizing Bindings
KeyBindings provides builder methods to customize bindings. There are three categories of methods:
- with_* - Replace a binding entirely
- without_* - Disable a binding
- add_* - Append to existing bindings
All methods return Self, enabling method chaining.
Replacing Bindings
Use with_* methods to replace bindings entirely:
use agent_air::tui::keys::{KeyBindings, KeyCombo};
use crossterm::event::KeyCode;
let bindings = KeyBindings::minimal()
.with_quit(vec![KeyCombo::ctrl('w')])
.with_submit(vec![
KeyCombo::key(KeyCode::Enter),
KeyCombo::ctrl('m'),
]);
Available with_* methods:
| Method | Action |
|---|---|
with_move_up | Set move up bindings |
with_move_down | Set move down bindings |
with_move_left | Set move left bindings |
with_move_right | Set move right bindings |
with_move_line_start | Set line start bindings |
with_move_line_end | Set line end bindings |
with_delete_char_before | Set backspace bindings |
with_delete_char_at | Set delete bindings |
with_kill_line | Set kill line bindings |
with_insert_newline | Set newline bindings |
with_submit | Set submit bindings |
with_interrupt | Set interrupt bindings |
with_quit | Set quit bindings |
with_force_quit | Set force quit bindings |
with_enter_exit_mode | Set exit mode bindings |
with_exit_timeout_secs | Set exit confirmation timeout |
with_select | Set widget select bindings |
with_cancel | Set widget cancel bindings |
Disabling Bindings
Use without_* methods to clear bindings:
let bindings = KeyBindings::emacs()
.without_exit_mode() // Disable Ctrl+D exit
.without_kill_line() // Disable Ctrl+K
.without_quit(); // Disable normal quit
Available without_* methods:
| Method | Effect |
|---|---|
without_exit_mode | Disable exit confirmation mode |
without_quit | Disable normal quit binding |
without_force_quit | Disable force quit binding |
without_interrupt | Disable interrupt binding |
without_kill_line | Disable kill line binding |
without_insert_newline | Disable newline binding |
Adding Bindings
Use add_* methods to append without removing existing bindings:
let bindings = KeyBindings::minimal()
.add_quit(KeyCombo::ctrl('c')) // Add Ctrl+C to quit
.add_submit(KeyCombo::ctrl('s')); // Add Ctrl+S to submit
Available add_* methods:
| Method | Action |
|---|---|
add_move_up | Add move up key |
add_move_down | Add move down key |
add_move_left | Add move left key |
add_move_right | Add move right key |
add_quit | Add quit key |
add_submit | Add submit key |
add_interrupt | Add interrupt key |
add_enter_exit_mode | Add exit mode key |
add_force_quit | Add force quit key |
add_select | Add widget select key |
add_cancel | Add widget cancel key |
Chaining Example
Combine multiple customizations to create your ideal configuration:
use agent_air::tui::keys::{KeyBindings, KeyCombo};
use crossterm::event::KeyCode;
let bindings = KeyBindings::bare_minimum()
// Add Emacs-style navigation
.with_move_up(vec![KeyCombo::key(KeyCode::Up), KeyCombo::ctrl('p')])
.with_move_down(vec![KeyCombo::key(KeyCode::Down), KeyCombo::ctrl('n')])
// Disable direct quit, use exit mode instead
.without_quit()
.with_enter_exit_mode(vec![KeyCombo::ctrl('d')])
.with_exit_timeout_secs(5)
// Add force quit with Ctrl+C
.add_force_quit(KeyCombo::ctrl('c'));
Using with DefaultKeyHandler
Pass your configured bindings to DefaultKeyHandler:
use agent_air::tui::keys::{DefaultKeyHandler, KeyBindings};
// Use emacs preset
let handler = DefaultKeyHandler::new(KeyBindings::emacs());
// Use minimal preset
let handler = DefaultKeyHandler::new(KeyBindings::minimal());
// Use bare_minimum (default)
let handler = DefaultKeyHandler::default();
// Use custom bindings
let bindings = KeyBindings::minimal()
.without_quit()
.with_enter_exit_mode(vec![KeyCombo::ctrl('d')]);
let handler = DefaultKeyHandler::new(bindings);
Then pass the handler to your agent:
let agent = AgentAir::builder()
.config(my_config)
.key_handler(handler)
.build()?;
Complete Example
use agent_air::AgentAir;
use agent_air::tui::keys::{DefaultKeyHandler, KeyBindings, KeyCombo};
use crossterm::event::KeyCode;
// Start with emacs and customize
let bindings = KeyBindings::emacs()
// Remove kill line (Ctrl+K) - conflicts with our usage
.without_kill_line()
// Longer exit confirmation window
.with_exit_timeout_secs(3)
// Add Ctrl+C as additional interrupt
.add_interrupt(KeyCombo::ctrl('c'))
// Add Ctrl+S as additional submit
.add_submit(KeyCombo::ctrl('s'));
let handler = DefaultKeyHandler::new(bindings);
let agent = AgentAir::builder()
.config(my_config)
.key_handler(handler)
.build()?; 