Input Pipeline
The input pipeline turns a KeyEvent into one of: an action to run, plain text
for a field, a "waiting for the next key" state, or a cancellation. You rarely
touch it directly — handle_key runs it for you — but you do need to know the
binding syntax and how text input and chord sequences work.
Binding syntax
Bindings are plain strings. Modifiers are joined with +; a multi-key sequence
is space-separated.
| Write | Means |
|---|---|
tab, enter, esc, space | named keys |
up down left right home end pageup pagedown | navigation keys |
backspace delete insert | editing keys |
f1 … f12 | function keys |
a, :, ? | single characters |
ctrl+s | Ctrl + S |
alt+x | Alt + X |
shift+tab | Shift + Tab (normalized to BackTab) |
ctrl+shift+p | combined modifiers |
g h | press g, then h (a sequence) |
Modifier names are case-insensitive and have short forms: ctrl/control/c,
alt/meta/m, shift/s. So ctrl+s and c+s are the same binding.
This is the string format —
ctrl+, not theC-you may have seen elsewhere. The examples all use this form.
Binding keys to actions
On the builder, .bind(mode, binding, action) registers a key in a mode:
#![allow(unused)] fn main() { TuiPages::builder(View::Home) .bind(modes::GENERAL, "tab", Action::FocusNext) .bind(modes::GENERAL, "enter", Action::Select) .bind(modes::GENERAL, "g h", Action::GotoHome) // sequence .bind(modes::GLOBAL, "ctrl+c", Action::Quit) // works everywhere }
A key only fires if its mode is in the current page's PageSpec::modes.
GLOBAL is always active.
Multi-key sequences
Bind a space-separated sequence and the runtime waits for the rest after the first key:
#![allow(unused)] fn main() { .bind(modes::GENERAL, "g h", Action::GotoHome) .bind(modes::GENERAL, "g n", Action::GotoNotes) .bind(modes::GENERAL, "ctrl+x ctrl+c", Action::ForceQuit) }
While waiting, handle_key returns TuiPagesStatus::Waiting(hints) — those
hints are the candidate continuations, which you can show in a status bar. If
the next key doesn't match, or the timeout (input_timeout_ms, default 1000ms)
elapses, the sequence cancels.
Text input
By default every key is looked up as a binding. When a page sets
accepts_text_input(true), plain character keys that aren't bound instead flow
through as text:
#![allow(unused)] fn main() { PageSpec::new() .accepts_text_input(true) .modes(vec![modes::INSERT]) }
Now handle_key returns TuiPagesStatus::TextHandled for ordinary typing, and
your handler's handle_text(chord, ctx, state) gets the KeyChord. Bound keys
(like esc to leave the field) still fire as actions.
What handle_key returns
handle_key returns a TuiPagesOutput whose status is one of:
#![allow(unused)] fn main() { pub enum TuiPagesStatus<A> { ActionHandled, // a binding matched; your handler ran TextHandled, // text flowed to handle_text Waiting(Vec<InputHint<A>>), // mid-sequence; show the hints Cancelled, // sequence expired or didn't match CommandIncomplete(_), // from submit_command — see Commands CommandUnknown, CommandEmpty, } }
and a quit_requested: bool set when an effect asked to quit. Most loops only
check quit_requested and stash Waiting hints for display:
#![allow(unused)] fn main() { let output = tui.handle_key(key, state)?; match output.status { TuiPagesStatus::Waiting(hints) => waiting = hints, _ => waiting.clear(), } if output.quit_requested { return Ok(()); } }
Parsing bindings yourself
If you load keybindings from a config file, the parse functions are public:
#![allow(unused)] fn main() { use tui_pages::{parse_binding, try_parse_binding, parse_key, try_parse_key}; // Lenient: drops tokens it can't parse. Good for trusted, in-code strings. let chords: Vec<KeyChord> = parse_binding("ctrl+x z"); // Strict: reports the first bad token. Use for user-editable config. match try_parse_binding("ctrl+shft+x") { Ok(chords) => { /* ... */ } Err(e) => eprintln!("bad binding: {e}"), // UnknownKey("ctrl+shft+x") } }
parse_binding returns a sequence (Vec<KeyChord>); parse_key parses a
single chord. To build a chord from a live event, use
KeyChord::from_event(&key_event).