Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 input
  • Mouse: Mouse input (requires tui::mouse <- true)
  • Paste: Bracketed paste content
  • Resize: 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 handlers
  • Continue: 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, &paragraph(&"[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, &paragraph(&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, &paragraph(&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, &paragraph(&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, &paragraph(&"[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 },
  &paragraph(&"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))
    ]
  )
)

See Also

  • style - Styling text and widgets
  • layout - Arranging widgets with focus support