opentui

Build terminal UIs with OpenTUI. Covers Core, frameworks, components, application APIs, testing, extensions, integrations, deployment, and public API lookup.

Install
npx skills add 'https://github.com/anomalyco/opentui/tree/main/packages/web/src/content'
Download bundle ↓
main · ac753b4Scanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

docs/core-concepts/keyboard.mdx

docs/core-concepts/keyboard.mdxBrowse 89 files
View on GitHub
← Back to SKILL.md

title: Keyboard input description: Choose direct key events, component key bindings, or Keymap commands skill: entry: true intents: [keyboard, input, keybindings, paste, focus]

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

PropertyTypeMeaning
namestringCanonical key identity, such as "a", "space", "return", or "escape".
sequencestringDecoded text or sequence associated with the key.
rawstringThe original terminal sequence decoded as a string.
source"raw" | "kitty"The parser path.
ctrl, shift, meta, optionbooleanReported common modifiers. meta includes the Alt path.
super, hyperboolean | undefinedExtended modifiers when the terminal reports them.
eventType"press" | "repeat" | "release"Parsed event type. Kitty repeats use "press" with repeated: true.
repeatedboolean | undefinedWhether Kitty reported a repeat.
numberbooleanWhether legacy parsing identified a digit.
codestring | undefinedA recognized terminal key code.
capsLock, numLockboolean | undefinedKitty lock state when reported.
baseCodenumber | undefinedKitty 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.

Referenced from SKILL.md