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();
ActionKeys
Move upUp arrow
Move downDown arrow
Move leftLeft arrow
Move rightRight arrow
Line startHome
Line endEnd
Delete beforeBackspace
Delete atDelete
Kill line(none)
Insert newline(none)
SubmitEnter
Interrupt(none)
QuitEsc (when input empty)
Force quitCtrl+Q
Exit mode(none)
SelectEnter, Space
CancelEsc

minimal

Similar to bare_minimum but adds Ctrl+J for inserting newlines, useful for multi-line input:

let bindings = KeyBindings::minimal();
ActionKeys
Move upUp arrow
Move downDown arrow
Move leftLeft arrow
Move rightRight arrow
Line startHome
Line endEnd
Delete beforeBackspace
Delete atDelete
Kill line(none)
Insert newlineCtrl+J
SubmitEnter
Interrupt(none)
QuitEsc (when input empty and no modal)
Force quitCtrl+Q
Exit mode(none)
SelectEnter, Space
CancelEsc

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();
ActionKeys
Move upUp, Ctrl+P
Move downDown, Ctrl+N
Move leftLeft, Ctrl+B
Move rightRight, Ctrl+F
Line startHome, Ctrl+A
Line endEnd, Ctrl+E
Delete beforeBackspace
Delete atDelete
Kill lineCtrl+K
Insert newlineCtrl+J
SubmitEnter
InterruptEsc
Quit(none, use exit mode)
Force quitCtrl+Q
Exit modeCtrl+D
SelectEnter, Space
CancelEsc

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

Featurebare_minimumminimalemacs
Emacs navigationNoNoYes (Ctrl+P/N/B/F)
Emacs editingNoNoYes (Ctrl+A/E/K)
Exit keyEscEscCtrl+D (twice)
Interrupt keyNoneNoneEsc
Insert newlineNoneCtrl+JCtrl+J
Kill lineNoneNoneCtrl+K

Customizing Bindings

KeyBindings provides builder methods to customize bindings. There are three categories of methods:

  1. with_* - Replace a binding entirely
  2. without_* - Disable a binding
  3. 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:

MethodAction
with_move_upSet move up bindings
with_move_downSet move down bindings
with_move_leftSet move left bindings
with_move_rightSet move right bindings
with_move_line_startSet line start bindings
with_move_line_endSet line end bindings
with_delete_char_beforeSet backspace bindings
with_delete_char_atSet delete bindings
with_kill_lineSet kill line bindings
with_insert_newlineSet newline bindings
with_submitSet submit bindings
with_interruptSet interrupt bindings
with_quitSet quit bindings
with_force_quitSet force quit bindings
with_enter_exit_modeSet exit mode bindings
with_exit_timeout_secsSet exit confirmation timeout
with_selectSet widget select bindings
with_cancelSet 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:

MethodEffect
without_exit_modeDisable exit confirmation mode
without_quitDisable normal quit binding
without_force_quitDisable force quit binding
without_interruptDisable interrupt binding
without_kill_lineDisable kill line binding
without_insert_newlineDisable 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:

MethodAction
add_move_upAdd move up key
add_move_downAdd move down key
add_move_leftAdd move left key
add_move_rightAdd move right key
add_quitAdd quit key
add_submitAdd submit key
add_interruptAdd interrupt key
add_enter_exit_modeAdd exit mode key
add_force_quitAdd force quit key
add_selectAdd widget select key
add_cancelAdd 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()?;