Input Handling
The input handling system allows TUI applications to respond to keyboard, mouse, and other terminal events. Input handlers wrap widgets and intercept events before they propagate further.
The Event Type
All terminal events are represented by the Event type:
type Event = [
`FocusGained,
`FocusLost,
`Key(KeyEvent),
`Mouse(MouseEvent),
`Paste(string),
`Resize(i64, i64)
];
FocusGained/FocusLost: Terminal window focus changes (enabled while the display runs, independently of mouse input)Key: Keyboard inputMouse: Mouse input (requirestui::mouse <- true)Paste: Bracketed paste contentResize: Terminal was resized to (columns, rows)
Keyboard Events
Keyboard events contain detailed information about key presses:
type KeyEvent = {
code: KeyCode,
kind: KeyEventKind,
modifiers: Array<KeyModifier>,
state: Array<KeyEventState>
};
type KeyEventState = [
`Keypad,
`CapsLock,
`NumLock
];
Key Codes
type KeyCode = [
`Backspace,
`Enter,
`Left,
`Right,
`Up,
`Down,
`Home,
`End,
`PageUp,
`PageDown,
`Tab,
`BackTab,
`Delete,
`Insert,
`F(i64),
`Char(string),
`Null,
`Esc,
`CapsLock,
`ScrollLock,
`NumLock,
`PrintScreen,
`Pause,
`Menu,
`KeypadBegin,
`Media(MediaKeyCode),
`Modifier(ModifierKeyCode)
];
type MediaKeyCode = [
`Play,
`Pause,
`PlayPause,
`Reverse,
`Stop,
`FastForward,
`Rewind,
`TrackNext,
`TrackPrevious,
`Record,
`LowerVolume,
`RaiseVolume,
`MuteVolume
];
type ModifierKeyCode = [
`LeftShift,
`LeftControl,
`LeftAlt,
`LeftSuper,
`LeftHyper,
`LeftMeta,
`RightShift,
`RightControl,
`RightAlt,
`RightSuper,
`RightHyper,
`RightMeta,
`IsoLevel3Shift,
`IsoLevel5Shift
];
Character keys use Char(string), e.g., Char("a") or Char("A").
Function keys use F(i64), e.g., F(1) for F1.
Key Event Kind
type KeyEventKind = [
`Press,
`Repeat,
`Release
];
Most applications only care about Press events, and must say so: a
terminal that reports releases (the Windows console does) delivers two
events per keystroke, and a handler that matches `Key(k) alone
runs twice. on_press(e, f) calls f with the KeyEvent when e is a
press and continues on everything else. It returns what a handler
returns, so it is usually the handler’s whole body:
let handle = |e: Event| on_press(e, |k| select k.code {
`Up => { sel <- k ~ max(0, sel - 1); `Stop },
`Down => { sel <- k ~ sel + 1; `Stop },
_ => `Continue
});
input_handler(#handle: &handle, &view)
A handler that wants mouse, paste, resize or a release selects on the
Event itself. A modal one that lets nothing through wraps the call
and answers every event, which means sampling the answer on the event
(a bare `Stop is a constant: it answers the first event and no
other):
let modal = |e: Event| { on_press(e, keys); e ~ `Stop };
Key Modifiers
type KeyModifier = [
`Shift,
`Control,
`Alt,
`Super,
`Hyper,
`Meta
];
The modifiers array contains all modifiers held during the key event.
Ending the Program
A TUI program runs until Ctrl-C, or until it calls tui::exit, which
ends it the same way: the terminal is restored and the runtime stops.
Fire it from the key that quits, sampled with the event:
`Char("q") => { tui::exit(k ~ null); `Stop }
Handing the Terminal to a Program
Some steps belong on the plain terminal — a sudo password prompt, an
editor. tui::suspend takes a level: while its input is true the
display has released the terminal (leaving the alternate screen and
raw mode, and dropping its input reader), and when the input goes
false the display takes it back with a full repaint. Its result is the
display’s actual state, so start the program on the release and clear
the level with the program’s exit:
let suspended = false;
{
catch(e) { println("could not run: [e]"); suspended <- e ~ false };
suspended <- kk ~ true;
let released = tui::suspend(suspended)?;
let go = select released { true => released, false => never() };
let child = sys::process::spawn(sys::process::options(#args: ["-v"], go ~ "sudo"))?;
let status = sys::process::wait(child.proc)?;
suspended <- status ~ false
}
Mouse Events
Mouse events require enabling mouse support first:
tui::mouse <- true
Mouse events contain position and action information:
type MouseButton = [
`Left,
`Right,
`Middle
];
type MouseEventKind = [
`Down(MouseButton),
`Up(MouseButton),
`Drag(MouseButton),
`Moved,
`ScrollDown,
`ScrollUp,
`ScrollLeft,
`ScrollRight
];
type MouseEvent = {
column: i64,
kind: MouseEventKind,
modifiers: Array<KeyModifier>,
row: i64
};
Creating Input Handlers
Use tui::input_handler::input_handler to wrap a widget with event handling:
/// Route terminal events through `handle` before `child` sees them, one
/// at a time: the child gets an event the handler answers `Continue.
/// An event the handler does not answer in the cycle it was given (its
/// call produced nothing) goes nowhere. The handler is called from the
/// display, outside any `catch`, so it may not raise. While `#enabled`
/// is false, events are dropped: neither the handler nor `child` (nor
/// any handler under it) sees them.
val input_handler: fn(
?#enabled: &[bool, null],
#handle: &fn(e: Event) -> [`Stop, `Continue],
child: &Tui
) -> Tui;
/// Call `f` with the key event when `e` is a key press, and continue
/// on everything else: releases and repeats (a terminal that reports
/// releases would otherwise run every key twice), mouse, paste, focus
/// and resize. It returns what a handler returns, so it is a handler's
/// body, `|e: Event| on_press(e, |k| select k.code { .. })`, or a part
/// of one.
val on_press: fn(e: Event, f: fn(k: KeyEvent) -> [`Stop, `Continue] throws 'e) -> [`Stop, `Continue] throws 'e;
The handler function receives events and returns:
Stop: The event was handled; don’t propagate to other handlersContinue: Pass the event to the next handler
The handler may not raise an error: it is called from the display,
outside any catch, so handle errors in its body ($, or a catch
inside it). on_press is the usual body, and it may throw what its
callback throws.
Basic Example
use tui::{
input_handler::{Event, input_handler, on_press},
paragraph::paragraph
};
let count = 0;
let handle = |e: Event| on_press(e, |k| select k.code {
q@ `Char("q") => {
tui::exit(q ~ null);
`Stop
},
u@ `Up => {
count <- u ~ count + 1;
`Stop
},
d@ `Down => {
count <- d ~ count - 1;
`Stop
},
_ => `Continue
});
input_handler(#handle: &handle, ¶graph(&"[count]"))
The Up and Down arms sample the key (u ~ count + 1). A write
inside a select arm fires when its right-hand side fires: count <- count + 1 would fire again on its own update and loop, and a constant
such as mode <- `Insert fires only at the arm’s first entry, since a
later entry resumes the arm (the check warns of it). Sample the event
that selects the arm instead, k@ `Char("i") => mode <- k ~ `Insert;
the select chapter’s “Writing From an Arm” spells out why.
Conditional Handling
The #enabled parameter turns a handler on and off. A disabled handler
drops every event: neither it nor anything it wraps, nested handlers
included, sees one. So it gates a whole subtree, and whatever enables it
must sit outside it:
use tui::{
input_handler::{Event, input_handler, on_press},
paragraph::paragraph
};
let editing = false;
let typed = "";
// always enabled: 'e' starts editing
let start = |e: Event| on_press(e, |k| select k.code {
i@ `Char("e") if editing == false => {
editing <- i ~ true;
`Stop
},
_ => `Continue
});
// enabled only while editing: Esc stops, characters are typed
let edit = |e: Event| on_press(e, |k| select k.code {
x@ `Esc => {
editing <- x ~ false;
`Stop
},
c@ `Char(s) => {
typed <- c ~ "[typed][s]";
`Stop
},
_ => `Continue
});
let status = select editing {
true => "editing: [typed]",
false => "press e to edit: [typed]"
};
input_handler(
#handle: &start,
&input_handler(#enabled: &editing, #handle: &edit, ¶graph(&status))
)
Event Propagation
Input handlers form a stack. Events flow from the outermost handler inward:
use tui::{
input_handler::{Event, input_handler, on_press},
paragraph::paragraph
};
let last = "nothing yet";
// the outer handler sees each event first and takes only 'a'
let outer = |e: Event| on_press(e, |k| select k.code {
a@ `Char("a") => {
last <- a ~ "outer took a";
`Stop
},
_ => `Continue
});
// the inner handler sees what the outer one passed on
let inner = |e: Event| on_press(e, |k| select k.code {
c@ `Char(s) => {
last <- c ~ "inner took [s]";
`Stop
},
_ => `Continue
});
input_handler(
#handle: &outer,
&input_handler(#handle: &inner, ¶graph(&last))
)
If the outer handler returns Continue, the event reaches the inner one. If
either returns Stop, propagation stops.
Containers route an event to one child: a block passes it to its child,
a layout only to its #focused child (the first when #focused is not
set), tabs only to the selected tab, and an overlay only to its top
layer. A handler in a pane that is not focused, or in a tab that is not
selected, sees no event at all.
The Raw Event Stream
For advanced use cases, you can access the raw event stream directly via
tui::event:
use tui::paragraph::paragraph;
let last = "no key yet";
last <- select tui::event {
`Key(k) => "[k.code]",
_ => never()
};
paragraph(&last)
However, using input_handler is preferred as it integrates properly with the
widget tree and supports the enable/disable pattern.
Text Entry
tui::line_edit is a single-line editor: line_edit::state(initial)
makes its state, line_edit::handle(&mut st, e) applies a key (typing,
paste, Backspace, Delete, Left, Right, Home, End) and answers Stop or
Continue like a handler, and line_edit::view(&st) renders it as a
Line with the cursor shown (#mask hides a secret):
use tui::{
line,
line_edit,
block::block,
input_handler::{Event, input_handler, on_press},
layout::{child, layout},
text::text
};
let ed = line_edit::state("");
let saved = "";
// Enter saves the text and clears the editor; every other key edits
let handle = |e: Event| on_press(e, |k| select k.code {
kk@ `Enter => {
saved <- kk ~ ed.value;
ed <- kk ~ line_edit::state("");
`Stop
},
_ => line_edit::handle(&mut ed, e)
});
input_handler(
#handle: &handle,
&block(
#border: &`All,
#title: &line("type, then Enter"),
&layout(
#direction: &`Vertical,
&[
child(#constraint: `Length(1), text(&[line_edit::view(&ed)])),
child(#constraint: `Length(1), text(&"saved: [saved]"))
]
)
)
)
tui::form::form is a modal form of several line editors: Tab and Down
move to the next field, BackTab and Up to the previous, Enter calls
#on_submit with every field’s text, Esc calls #on_cancel. Render its
view in an input_handler running its handle, on an overlay layer:
use tui::{
block::block,
form::form,
input_handler::{Event, input_handler, on_press},
overlay::{Layer, layer, overlay},
paragraph::paragraph
};
let filling = false;
let greeting = "press f to fill in the form";
let name_form = form(
#title: "Your name",
#on_submit: |v| {
greeting <- "hello [v[0]$] [v[1]$]";
filling <- v ~ false;
null
},
#on_cancel: |e| {
filling <- e ~ false;
null
},
[{ label: "first", value: "" }, { label: "last", value: "" }]
);
let handle_base = |e: Event| on_press(e, |k| select k.code {
f@ `Char("f") => {
filling <- f ~ true;
`Stop
},
_ => `Continue
});
let base = input_handler(
#handle: &handle_base,
&block(#border: &`All, ¶graph(&greeting))
);
let layers: Array<Layer> = select filling {
true => [
layer(
#width: &`Percentage(60),
#height: &`Length(8),
input_handler(#handle: &name_form.handle, &name_form.view)
)
],
false => []
};
overlay(#layers: &layers, base)
Common Patterns
Vim-style Mode Switching
use tui::{
input_handler::{Event, input_handler, on_press},
paragraph::paragraph
};
type Mode = [`Normal, `Insert];
let mode: Mode = `Normal;
let text = "";
let handle = |e: Event| on_press(e, |k| select (k ~ mode, k.code) {
(`Normal, `Char("i")) => {
mode <- k ~ `Insert;
`Stop
},
(`Insert, `Esc) => {
mode <- k ~ `Normal;
`Stop
},
(`Insert, `Char(c)) => {
text <- k ~ "[text][c]";
`Stop
},
_ => `Continue
});
input_handler(#handle: &handle, ¶graph(&"[mode]: [text]"))
Focus Management
use tui::{
block::block,
input_handler::{Event, input_handler, on_press},
layout::{child, layout},
paragraph::paragraph
};
let focused = 0;
let num_widgets = 3;
let handle = |e: Event| on_press(e, |k| select k.code {
t@ `Tab => {
focused <- ((t ~ focused) + 1) % num_widgets;
`Stop
},
t@ `BackTab => {
focused <- ((t ~ focused) - 1 + num_widgets) % num_widgets;
`Stop
},
_ => `Continue
});
let pane = |i| block(
#border: &select focused { n if n == i => `All, _ => `None },
¶graph(&"pane [i]")
);
input_handler(
#handle: &handle,
&layout(
#direction: &`Horizontal,
#focused: &focused,
&[
child(#constraint: `Fill(1), pane(0)),
child(#constraint: `Fill(1), pane(1)),
child(#constraint: `Fill(1), pane(2))
]
)
)