crossterm-actions

Crates.iocrossterm-actions
lib.rscrossterm-actions
version1.0.1
created_at2026-01-10 17:05:37.859636+00
updated_at2026-01-10 17:52:42.303148+00
descriptionHierarchical TUI action/keybinding management for crossterm applications
homepage
repositoryhttps://github.com/JonathanTroyer/crossterm-actions
max_upload_size
id2034441
size153,170
Jonathan Troyer (JonathanTroyer)

documentation

README

crossterm-actions

Hierarchical TUI action/keybinding management for crossterm applications.

Features

  • Hierarchical actions: Namespaced events like TuiEvent::Navigation(NavigationEvent::Left)
  • Fluent builder API: Define bindings with ActionBinding::builder().action(event).key(key.with_ctrl()).build()
  • Pre-built defaults: Emacs and Vi keybinding presets
  • Event dispatcher: Map crossterm KeyEvent to TuiEvent
  • inputrc support: Optional GNU readline configuration compatibility

Usage

use crossterm_actions::{
    ActionBinding, ActionConfig, EditingMode, EventDispatcher,
    TuiEvent, AppEvent, NavigationEvent, keys,
};

// Create a dispatcher with emacs defaults
let dispatcher = EventDispatcher::default();

// Or build custom bindings
let mut config = ActionConfig::new(EditingMode::Emacs);

config.bind(
    ActionBinding::builder()
        .action(TuiEvent::App(AppEvent::Quit))
        .key(keys::char('q'))
        .key(keys::char('c').with_ctrl())
        .description("Exit the application")
        .build(),
);

config.bind(
    ActionBinding::builder()
        .action(TuiEvent::Navigation(NavigationEvent::Left))
        .key(keys::LEFT)
        .key(keys::char('h'))
        .build(),
);

config.compile(); // Build lookup table

let dispatcher = EventDispatcher::new(config);

Using the bind_action! Macro

For more concise binding definitions, use the bind_action! macro:

use crossterm_actions::{
    bind_action, ActionConfig, EditingMode, EventDispatcher,
    TuiEvent, AppEvent, NavigationEvent, keys,
};

let mut config = ActionConfig::new(EditingMode::Emacs);

// Single key
bind_action!(config, TuiEvent::App(AppEvent::Quit), keys::char('q'));

// Single key with description
bind_action!(config, TuiEvent::App(AppEvent::Quit), keys::char('c').with_ctrl(), "Force quit");

// Multiple keys
bind_action!(config, TuiEvent::Navigation(NavigationEvent::Left), [keys::LEFT, keys::char('h')]);

// Multiple keys with description
bind_action!(
    config,
    TuiEvent::App(AppEvent::Help),
    [keys::char('?'), keys::f(1)],
    "Show help"
);

config.compile();
let dispatcher = EventDispatcher::new(config);

Dispatching Events

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use crossterm_actions::{EventDispatcher, TuiEvent, AppEvent};

let dispatcher = EventDispatcher::default();

// Create a key event (normally from crossterm::event::read())
let event = KeyEvent {
    code: KeyCode::Char('q'),
    modifiers: KeyModifiers::NONE,
    kind: KeyEventKind::Press,
    state: KeyEventState::NONE,
};

match dispatcher.dispatch(&event) {
    Some(TuiEvent::App(AppEvent::Quit)) => {
        println!("User wants to quit");
    }
    Some(other) => {
        println!("Got action: {:?}", other);
    }
    None => {
        // Key not bound to any action
    }
}

Action Hierarchy

use crossterm_actions::{TuiEvent, NavigationEvent, InputEvent, SelectionEvent, AppEvent};

// All available events
let events: Vec<TuiEvent> = vec![
    // Navigation
    TuiEvent::Navigation(NavigationEvent::Left),
    TuiEvent::Navigation(NavigationEvent::Right),
    TuiEvent::Navigation(NavigationEvent::Up),
    TuiEvent::Navigation(NavigationEvent::Down),
    TuiEvent::Navigation(NavigationEvent::Home),
    TuiEvent::Navigation(NavigationEvent::End),
    TuiEvent::Navigation(NavigationEvent::PageUp),
    TuiEvent::Navigation(NavigationEvent::PageDown),

    // Input/Editing
    TuiEvent::Input(InputEvent::Confirm),
    TuiEvent::Input(InputEvent::Cancel),
    TuiEvent::Input(InputEvent::Delete),
    TuiEvent::Input(InputEvent::Backspace),

    // Selection
    TuiEvent::Selection(SelectionEvent::Next),
    TuiEvent::Selection(SelectionEvent::Prev),
    TuiEvent::Selection(SelectionEvent::Toggle),

    // Application
    TuiEvent::App(AppEvent::Quit),
    TuiEvent::App(AppEvent::Help),
    TuiEvent::App(AppEvent::Refresh),
    TuiEvent::App(AppEvent::Search),
];

Default Bindings

Emacs Mode

Action Keys
Left , Ctrl+B
Right , Ctrl+F
Up , Ctrl+P
Down , Ctrl+N
Home Home, Ctrl+A
End End, Ctrl+E
Quit q, Ctrl+C
Help ?, F1

Vi Mode

Action Keys
Left , h
Right , l
Up , k
Down , j
Home Home, 0, ^
End End, $
Quit q

Custom Actions

Use your own action types by implementing Clone, Eq, and Hash:

use crossterm_actions::{ActionBinding, ActionConfig, EditingMode, EventDispatcher, keys};

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum MyAction {
    Save,
    Undo,
    Redo,
}

let mut config = ActionConfig::new(EditingMode::Emacs);

config.bind(
    ActionBinding::builder()
        .action(MyAction::Save)
        .key(keys::char('s').with_ctrl())
        .build(),
);

config.bind(
    ActionBinding::builder()
        .action(MyAction::Undo)
        .key(keys::char('z').with_ctrl())
        .build(),
);

config.compile();
let dispatcher = EventDispatcher::new(config);

Extending Defaults with Custom Actions

Use map_actions to wrap the built-in defaults in your own action type:

use crossterm_actions::{ActionBinding, emacs_defaults, TuiEvent, AppEvent, keys};

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum AppAction {
    Tui(TuiEvent),
    CodePreview,
    ToggleDarkMode,
}

// Transform emacs defaults from ActionConfig<TuiEvent> to ActionConfig<AppAction>
let mut config = emacs_defaults().map_actions(AppAction::Tui);

// Add custom bindings alongside the defaults
config.bind(
    ActionBinding::builder()
        .action(AppAction::CodePreview)
        .key(keys::char('p'))
        .build(),
);

config.compile();

// 'q' still triggers quit (wrapped), 'p' triggers your custom action
assert_eq!(config.get(&keys::char('q')), Some(AppAction::Tui(TuiEvent::App(AppEvent::Quit))));
assert_eq!(config.get(&keys::char('p')), Some(AppAction::CodePreview));

inputrc Support

Enable the inputrc feature (on by default) to load keybindings from ~/.inputrc:

use crossterm_actions::{defaults, load_inputrc};

let mut config = defaults::emacs_defaults();

// Apply user's inputrc customizations
if let Err(e) = load_inputrc(&mut config) {
    eprintln!("Warning: failed to load inputrc: {}", e);
}

License

MIT

Commit count: 22

cargo fmt