Key bindings
Navigation, edit, accept and cancel keys are all configurable. A widget never asks for a fixed key - it asks for a semantic action (MoveUp, Accept, Toggle - the full set is listed below), and a key map binds each action to one or more keys. Key bindings describe the terminal, not the questionnaire, so you set them on the Tui facade with ->keys(...), mirroring ->theme(...):
$tui = (new Tui($form))->keys('vim'); // built-in vim navigation (h/j/k/l)
Two presets ship: default (every binding it declares is listed below) and vim, which adds h/j/k/l alongside the arrow keys - but only where a letter isn't typed input, so text and filter fields keep the arrows.
Actions
The actions are the fixed set of intents the widgets understand - the bindings behind them are configurable, the intents are not. Every Action case:
| Action | Meaning |
|---|---|
MoveUp / MoveDown / MoveLeft / MoveRight | Move the cursor or caret |
Accept | Commit the editor's value |
Cancel | Close the editor without committing |
Activate | Open the focused field or panel (panel browser) |
Back | Go up one panel level |
Quit | Leave the TUI |
ScrollUp / ScrollDown | Scroll the panel without moving the cursor |
Help | Open the help overlay |
DeleteBack | Delete the character before the caret |
InsertSpace | Type a space |
NewLine | Insert a newline (textarea) |
ExternalEdit | Hand off to the external editor (textarea) |
Complete | Accept the ghost-text completion (text) |
Increment / Decrement | Step a bounded number |
Toggle | Check an option, or flip a two-state switch |
SelectAll / SelectNone | Check or clear every visible option |
Grab | Pick up or drop the held item (reorder) |
Yes / No | Set a confirm directly |
Reveal | Cycle the password display, or show hidden file-picker entries |
The named keys a binding can use - every KeyName case: Up, Down, Left, Right, Enter, Escape, Interrupt (Ctrl-C), Space, Backspace, Delete, Tab, Home, End, PageUp, PageDown, MouseWheelUp, MouseWheelDown. Anything printable is bound as the character itself ('q', '?').
The default bindings
What default actually declares, scope by scope. Every widget inherits the base layer and overrides only what differs:
| Scope | Keys | Action |
|---|---|---|
| base (every widget) | ↑ / ↓ / ← / → | MoveUp / MoveDown / MoveLeft / MoveRight |
| base | Enter | Accept |
| base | Esc | Cancel |
| base | Backspace | DeleteBack |
| base | Space | InsertSpace |
| navigation | Enter | Activate |
| navigation | Esc | Back |
| navigation | q | Quit |
| navigation | ? | Help |
| navigation | mouse wheel | ScrollUp / ScrollDown |
| number | ↑ / ↓ | Increment / Decrement |
| text | Tab | Complete |
| textarea | Enter | NewLine |
| textarea | Tab | Accept |
| textarea | Ctrl-E | ExternalEdit |
| confirm | y/Y and n/N | Yes and No |
| confirm, toggle | ← / → / Space / ↑ / ↓ | Toggle |
| password | Tab | Reveal |
| pause | Enter / Space | Accept |
| select (multiple), search (multiple) | Space | Toggle |
| select (multiple), search (multiple) | → / ← | SelectAll / SelectNone |
| file picker (single and multiple) | Tab | Reveal |
| file picker (multiple) | Space | Toggle |
| reorder | Space | Grab |
vim inherits all of it and adds letters where typing can't swallow them: the panel browser and the single-choice select gain k/j for up/down, and the calendar gains k/j/h/l for week and day movement - each alongside the arrow keys, never replacing them.
Not everything on the keyboard routes through the map: a few widget keys are fixed. The calendar's month and edge jumps (PageUp/PageDown, Home/End) have no action behind them and always keep their keys.
Per-widget-type overrides
Bindings are layered by scope: a base layer shared by every widget, a navigation layer for the panel browser, and one layer per widget type that overrides the base only where it differs (Enter inserts a newline in a textarea, Space toggles a checkbox option). To retune individual bindings, pass overrides on top of a preset - each names a scope, an action and its keys. Later bindings win, so re-declaring a scope-and-action pair replaces the preset's keys for it:
use DrevOps\Tui\Model\FieldType;
use DrevOps\Tui\Input\Action;
use DrevOps\Tui\Input\Binding;
use DrevOps\Tui\Input\KeyName;
use DrevOps\Tui\Input\Scope;
$tui = (new Tui($form))->keys('default', [
// Quit with x as well as q.
new Binding(Scope::navigation(), Action::Quit, 'x'),
// In the single-choice list, Tab accepts too.
new Binding(Scope::field(FieldType::Select), Action::Accept, KeyName::Tab, KeyName::Enter),
]);
A binding's keys take three forms: a KeyName case for a named key, a single-character string for a printable one, or a Key for anything else - Key::char("\x05") is how the default preset binds Ctrl-E. A scope takes three forms too: Scope::base() (shared by every widget), Scope::navigation() (the panel browser), and Scope::field(FieldType::X) - with Scope::field(FieldType::X, multiple: TRUE) targeting the multiple-collecting variant of a choice or file-picker widget, which carries its own binding set (that's where Space-to-toggle lives).
The panel and editor hints are drawn from the live bindings, so they always reflect the active keys - remap quit to x and the footer says so.
Presets and validation
A preset is a class listing its bindings. Subclass DefaultKeyMap to ship your own, name it directly with ->keys('\App\MyKeyMap'), or register a short alias with KeyMapManager::register('mine', MyKeyMap::class) and then ->keys('mine').
Bindings are validated the moment they're set, so a bad key map is caught at configuration time, not mid-session:
- a key bound to two different actions in the same scope is a conflict;
- a printable character bound in the base scope, or in a scope whose widget consumes typed input (text, number, password, textarea, search, suggest, toggle, file picker, and the multiple select), would be un-typeable and is rejected - control characters like Ctrl-E are exempt, since they're command keys, never typed content;
- an unknown preset name, or a character binding that is not exactly one character, is rejected.
See playground/10-key-bindings-* for the default map, the vim preset and a custom override side by side.