Keyboard & mouse
Key events
Every keyboard input becomes an Event.Key(KeyEvent) where:
KeyEvent { code: KeyCode, modifiers: Modifiers, kind: KeyEventKind }
kind is Press by default on most terminals. Kitty keyboard protocol
also delivers Release and Repeat; if you depend on release events,
check caps.has_kitty_keyboard first.
Matching
match event {
Event.Key(ke) => match ke.code {
KeyCode.Char('q') if ke.modifiers.empty() => quit(),
KeyCode.Char('s') if ke.modifiers.contains(Modifiers.CTRL) => save(),
KeyCode.F(5) => refresh(),
KeyCode.Esc | KeyCode.Char('q') => quit(),
_ => {}
},
_ => {}
}
Common patterns
- Vim-like navigation —
KeyCode.Char('j') | KeyCode.Downin one arm. - Global hotkeys — intercept before per-mode dispatch in
handle_event:if let Event.Key(ke) = &event {if ke.is_ctrl_c() { return Some(Msg.Quit); }} - Mode switches — store a
Modein your model and match on it first.
Mouse events
SGR Extended 1006 is auto-enabled if supported:
Event.Mouse(MouseEvent { kind, column, row, modifiers })
Variants of MouseEventKind:
Down(button)/Up(button)/Drag(button)withbutton: Left | Right | MiddleMoved— pointer over terminal without buttonScrollUp/ScrollDown/ScrollLeft/ScrollRight
Hit testing
let r = some_widget_area;
if r.contains(me.column, me.row) {
// clicked inside widget
}
Drag handling
Keep a dragging: Bool flag in state and use Drag(button) while it's
set. The Split widget's handle_mouse demonstrates the pattern.
Paste events
If bracketed paste is enabled (default in the app framework), the whole
pasted payload arrives as a single Event.Paste(Text) — never
interleaved with keystrokes. This matters for password prompts and any
input that could be exploited by paste-spoofing.
Focus events
Event.FocusGained / Event.FocusLost on terminals supporting mode 1004.
Useful for:
- pausing animations / timers when the window is in the background
- writing a "currently editing" marker for shell prompts
- refreshing data that may have changed while unfocused
Default dispatchers
Widgets with built-in key handling expose handle_key(ke) returning
Bool:
| Widget | Bindings |
|---|---|
TextInput | Emacs-style (see reference) |
TextArea | Emacs-lite + Enter-inserts-newline |
Dropdown | Up/Down/PgUp/PgDn/Home/End/Enter/Esc/Backspace for search |
Split via SplitState.handle_resize_key | Ctrl+Arrow resize |
Your handle_event should:
- Dispatch global hotkeys first.
- Route to the focused widget's
handle_key. - Fall back to app-level actions.
fn handle_event(&self, event: Event) -> Maybe<Msg> {
let Event.Key(ke) = event else { return None; };
// Global
if ke.is_ctrl_c() { return Some(Msg.Quit); }
// Per-focused-widget
match self.focus {
FocusSearch => Some(Msg.SearchKey(ke)),
FocusList => match ke.code {
KeyCode.Up => Some(Msg.ListUp),
KeyCode.Down => Some(Msg.ListDown),
_ => None,
},
}
}
Then in update:
Msg.SearchKey(ke) => {
let _ = self.search.handle_key(ke);
Command.none()
}