Keyboard input

Use direct renderer KeyEvent listeners for simple app-wide input. Use component key bindings for local behavior. Use @opentui/keymap for layers, commands, discovery, and key sequences.

These levels can coexist, but they have different ownership and cleanup rules.

Handle app-wide keys#

renderer.keyInput emits parsed keypress, keyrelease, and paste events.

import { type KeyEvent, createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer()

const onKeyPress = (key: KeyEvent) => {
  if (key.name === "escape") {
    renderer.destroy()
  }
}

renderer.keyInput.on("keypress", onKeyPress)

renderer.once("destroy", () => {
  renderer.keyInput.off("keypress", onKeyPress)
})

Direct listeners run before the focused renderable. They are suitable for a global exit key, a small shortcut set, or input diagnostics.

Use prependListener() when listener order matters. Remove listeners when their owner stops, as described in Lifecycle and cleanup.

Read KeyEvent#

Property Type Meaning
name string Canonical key identity, such as "a", "space", "return", or "escape".
sequence string Decoded text or sequence associated with the key.
raw string The original terminal sequence decoded as a string.
source "raw" | "kitty" The parser path.
ctrl, shift, meta, option boolean Reported common modifiers. meta includes the Alt path.
super, hyper boolean | undefined Extended modifiers when the terminal reports them.
eventType "press" | "repeat" | "release" Parsed event type. Kitty repeats use "press" with repeated: true.
repeated boolean | undefined Whether Kitty reported a repeat.
number boolean Whether legacy parsing identified a digit.
code string | undefined A recognized terminal key code.
capsLock, numLock boolean | undefined Kitty lock state when reported.
baseCode number | undefined Kitty base-layout code point for layout-stable matching.

sequence is not always equal to raw. For example, a Kitty control sequence can produce name: "space" and sequence: " ".

Compare direct events with canonical names such as "return", "escape", and "space". Component alias maps do not rewrite direct KeyEvent.name values.

Control propagation#

Global listeners run in registration order before the focused renderable’s handler.

key.stopPropagation() stops later global listeners and prevents delivery to the focused renderable. It does not set defaultPrevented.

key.preventDefault() lets later global listeners run, but it prevents focused renderable handlers from receiving the event.

A focused renderable’s onKeyDown runs before its built-in handleKeyPress(). Calling preventDefault() there skips the built-in action.

Component handleKeyPress() return values describe local handling, but they do not change the event’s propagation flags.

See Interaction, focus, and selection for focus ownership and the lack of automatic Tab traversal.

Configure component key bindings#

Input, Textarea, Select, and TabSelect expose keyBindings for local actions. These four components also accept keyAliasMap.

import { TextareaRenderable, createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer()
const editor = new TextareaRenderable(renderer, {
  width: 40,
  height: 8,
  keyBindings: [{ name: "s", ctrl: true, action: "submit" }],
  onSubmit() {
    console.log("Submit", editor.plainText)
  },
})

renderer.root.add(editor)
editor.focus()

A key binding contains name, optional ctrl, shift, meta, and super, plus a component-specific action.

Custom bindings replace default bindings with the same key and modifiers. Other default bindings remain active.

Core aliases configured binding names such as enter to return and esc to escape. It also maps keypad names to their main-keyboard equivalents.

Read Input and Textarea for their action sets. Other components document their own actions.

Use Keymap for commands#

Direct listeners become difficult to manage when shortcuts depend on focus, mode, priority, or a sequence prefix.

@opentui/keymap adds scoped layers, named commands, multi-stroke sequences, active-key queries, and command metadata. Its OpenTUI host reads the renderer’s focused renderable.

Keymap can consume a matched event before the focused component receives it. Read the Keymap guide for dispatch and disposal.

Paste events#

Paste is terminal input. It is not a read from the host clipboard.

import { type PasteEvent, createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer()
const decoder = new TextDecoder()

const onPaste = (event: PasteEvent) => {
  console.log(decoder.decode(event.bytes))
  console.log(event.metadata?.mimeType, event.metadata?.kind)
}

renderer.keyInput.on("paste", onPaste)

PasteEvent.bytes preserves the terminal payload. Optional metadata contains mimeType and kind, where kind is "text", "binary", or "unknown".

PasteEvent supports the same preventDefault() and stopPropagation() flow as KeyEvent. Focused Input and Textarea components decode, sanitize, and insert paste bytes.

Use Clipboard when the application must read or write host clipboard data.

Handle raw sequences#

addInputHandler(handler) and prependInputHandler(handler) receive a parsed input event’s raw terminal sequence before KeyEvent delivery.

Return true to consume that sequence. Return false to continue through remaining handlers and, for keys, KeyEvent delivery.

Raw terminal encodings vary. Prefer KeyEvent unless you must integrate an unsupported terminal protocol.

The Kitty keyboard protocol improves modifier and release reporting when supported. Read Terminal capabilities before depending on those fields.

Test keyboard input#

createTestRenderer() returns mockInput. It can press keys, type text, send modifiers, and emit bracketed paste.

Use Kitty test mode when you need release, repeat, super, hyper, or baseCode behavior. See Testing for the exact helpers.

Next#

Read Keymap when local listeners no longer describe your command model. Read Interaction, focus, and selection when a component does not receive input.