Keybindings & keymap
This page covers the keymap.conf file (text widgets and global actions), TextArea newline configuration, and the public tui_lipan::input helpers for parsing, formatting, and matching shortcuts-including multi-key chords.
For how keyboard events flow through the component tree (on_key, bubbling), see focus.md.
keymap.conf (text input & app actions)
Text widgets and related handlers use crokey-style bindings loaded from a config file:
App::new()
.keymap_path("/path/to/keymap.conf") // Explicit path (highest priority)Environment fallback: TUI_LIPAN_KEYMAP=/path/to/keymap.conf
Default path: $XDG_CONFIG_HOME/tui-lipan/keymap.conf (or ~/.config/tui-lipan/keymap.conf)
Format - one action per line, action = key1, key2:
# Comments with #
copy = super-c, ctrl-insert
paste = super-v, ctrl-shift-v
paste_selection = shift-insert
cut = super-x, shift-delete
undo = ctrl-z, super-z
redo = ctrl-shift-z, ctrl-y
select_all = ctrl-a, super-a
move_left = left
select_word_right = shift-ctrl-right
delete_word_left = ctrl-backspace
insert_newline = enter
dismiss_overlay = esc
focus_next = tab
focus_prev = shift-tab
quit = ctrl-q
toggle_devtools = f12Available actions: copy, paste, paste_selection, cut, undo, redo, select_all, move_left, move_right, move_up, move_down, move_word_left, move_word_right, select_word_left, select_word_right, delete_word_left, delete_word_right, move_home, move_end, select_home, select_end, insert_newline, copy_image, paste_image, quit, dismiss_overlay, focus_next, focus_prev, toggle_devtools.
toggle_devtools is available when the devtools feature is enabled. F12 is the default binding, but you can remap or unbind it in keymap.conf. App code can also control the panel directly with ctx.show_devtools(), ctx.hide_devtools(), and ctx.toggle_devtools().
Clipboard actions are performable: copy/cut only consume when a selection exists, and paste only consumes when the focused widget can accept it. Ctrl+C also copies active mouse selections from Input, TextArea, DocumentView, and Terminal even when those widgets are not focusable.
For DocumentView, shared selections (shared_selection_id) copy as one concatenated payload per shared group within the same ScrollView.
Use none to unbind a key:
quit = none
focus_next = none
focus_prev = noneTo remap focus traversal instead of disabling it:
focus_next = ctrl-j
focus_prev = ctrl-kShift+Tab is normalized to the terminal's reverse-Tab event automatically.
Modifier names: ctrl, alt, shift, super (aliases: cmd, command, meta, win, windows). Use - between parts: ctrl-shift-z, super-c.
Built-in keymap matching (single key press)
Entries are parsed with the same KeyBinding rules as the public API (including chord syntax). The bundled Keymap runtime that drives keymap.conf actions, however, resolves one key event at a time. Single-step bindings work as before. Multi-step chord strings (e.g. ctrl+x b) parse correctly but do not trigger built-in actions until the runner wires chord state; use ChordMatcher (or matches_sequence) in your own on_key for app-level chords today.
KeyBinding / KeyBindings parsing
KeyBinding: one shortcut, optionally a chord (sequence of key steps).- Whitespace separates steps:
ctrl+x b→ Ctrl+X, thenb. - Each step is a single combination (
ctrl-shift-up,super-c, …).
- Whitespace separates steps:
KeyBindings: alternatives for the same logical shortcut.- Comma separates alternatives:
ctrl+d, ctrl+q→ either binding.
- Comma separates alternatives:
So ctrl+x b, ctrl+q means: (Ctrl+X then B) or Ctrl+Q.
Matching
KeyBinding::matches_sequence(&[KeyEvent])- true when the slice length equals the binding’s step count and each event matches the corresponding step (same normalization as the keymap: legacy raw ctrl characters, BackTab, etc.).KeyBinding::is_chord()/step_count()- inspect parsed chords.- There is no
KeyBinding::matches(&KeyEvent)on a single event; usematches_sequence(&[key])for a one-step binding, orChordMatcher(below) when several keys must be accumulated.
ChordMatcher (stateful chords)
ChordMatcher<T> holds a list of (KeyBinding, T) and implements incremental matching across key events: feed(&KeyEvent) -> ChordResult<&T>.
ChordResult::Matched- a full binding matched.ChordResult::Pending- prefix of at least one chord; more keys needed.ChordResult::None- no match (after reset behavior for failed continuations).
If one key is both a full single-step binding and a prefix of a longer chord, the matcher stays pending until the next key disambiguates.
Re-exported from the crate root and prelude (ChordMatcher, ChordResult).
Formatting helpers
use std::str::FromStr;
use tui_lipan::input::{
KeyBinding,
KeyBindings,
format_binding,
format_binding_lowercase,
format_bindings,
format_bindings_lowercase,
};
let one = KeyBinding::from_str("super+p")?;
assert_eq!(one.to_string(), "Cmd+P");
let many = KeyBindings::from_str("ctrl+d, ctrl+q")?;
assert_eq!(many.to_string(), "Ctrl+D / Ctrl+Q");
let chord = KeyBinding::from_str("ctrl+x b")?;
assert!(chord.is_chord());
assert_eq!(chord.to_string(), "Ctrl+X B");
assert_eq!(format_binding("control-shift-up")?, "Ctrl+Shift+Up");
assert_eq!(format_bindings("super-c, ctrl-insert")?, "Cmd+C / Ctrl+Insert");
assert_eq!(format_binding_lowercase("Esc")?, "esc");
assert_eq!(format_bindings_lowercase("ctrl+d, super+q")?, "ctrl+d / cmd+q");
assert_eq!(one.canonical_lowercase(), "cmd+p");
assert_eq!(many.canonical_lowercase(), "ctrl+d / ctrl+q");TextArea newline key
Configure Enter behavior for TextArea only (does not affect single-line Input):
App::new()
.text_area_newline_binding(TextAreaNewlineBinding::Enter) // default
// or:
.text_area_newline_binding(TextAreaNewlineBinding::ShiftEnter)
.text_area_newline_binding(TextAreaNewlineBinding::EnterOrShiftEnter)Per-widget override (takes priority over app setting):
TextArea::new(value).newline_binding(TextAreaNewlineBinding::ShiftEnter)