Keyboard input

OpenTUI parses terminal input and provides structured key events. The renderer.keyInput EventEmitter emits keypress events plus raw paste events with optional metadata.

Basic key handling

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

const renderer = await createCliRenderer()
const keyHandler = renderer.keyInput

keyHandler.on("keypress", (key: KeyEvent) => {
  console.log("Key name:", key.name)
  console.log("Input sequence:", key.sequence)
  console.log("Raw input:", key.raw)
  console.log("Ctrl pressed:", key.ctrl)
  console.log("Shift pressed:", key.shift)
  console.log("Alt pressed:", key.meta)
  console.log("Option pressed:", key.option)
})

KeyEvent properties

Each KeyEvent contains the parsed key identity, the generated input sequence, and the original terminal input:

PropertyTypeDescription
namestringCanonical key identity, such as "a", "space", "return", "escape", or "f1"
sequencestringDecoded input sequence/text for the key. For printable keys this is the text, such as "a" or " "
rawstringExact terminal input sequence for this key event, decoded as a string
source"raw" | "kitty"Parser path that produced the event
ctrlbooleanWhether Ctrl was held
shiftbooleanWhether Shift was held
metabooleanWhether Alt/Meta was held
optionbooleanWhether Option was held (macOS Alt/Option path)
superboolean?Whether Super/Cmd/Windows was held, when reported
hyperboolean?Whether Hyper was held, when reported
eventType"press" | "repeat" | "release"Key event type. Kitty repeat events are emitted as "press" with repeated: true
repeatedboolean?true for Kitty repeat events
numberbooleantrue when the parsed key is a digit from legacy input
codestring?Terminal key code for recognized function-style escape sequences
capsLockboolean?Kitty Caps Lock modifier state, when reported
numLockboolean?Kitty Num Lock modifier state, when reported
baseCodenumber?Kitty base-layout codepoint used by layout-stable shortcut matching

sequence is not always the raw terminal bytes. For example, Kitty Ctrl+Space arrives as raw "\x1b[32;5u", but emits name: "space", ctrl: true, and sequence: " ". Use raw when you need the exact terminal sequence.

Keybinding aliases

Some core renderables and the built-in console support aliases for configured keybindings. These aliases are applied when building a component’s keybinding lookup map; they do not rewrite emitted KeyEvent.name values.

The default keybinding aliases are:

// enter -> return
// esc   -> escape
//
// Numpad keys are aliased to their main-keyboard equivalents:
// kp0..kp9   -> "0".."9"
// kpenter    -> enter
// kpleft/kpright/kpup/kpdown -> arrow keys
// kphome/kpend/kppageup/kppagedown/kpinsert/kpdelete -> navigation
// kpdecimal/kpdivide/kpmultiply/kpminus/kpplus/kpequal/kpseparator -> symbols

Aliases are one-step mappings from configured binding names to additional lookup names. For example, a component binding configured as { name: "enter", action: "submit" } also matches an emitted KeyEvent whose name is "return". This aliasing is component-specific; direct renderer.keyInput handlers receive the parser’s canonical names and should compare against names such as "return", "escape", and "space".

Editable components that accept a keyAliasMap option (InputRenderable, TextareaRenderable, SelectRenderable, etc.) merge these defaults with your own map:

import { InputRenderable } from "@opentui/core"

const input = new InputRenderable(renderer, {
  id: "field",
  keyAliasMap: {
    // Treat the numpad decimal key as forward-delete
    kpdecimal: "delete",
  },
})

Common key patterns

Single keys

keyHandler.on("keypress", (key: KeyEvent) => {
  if (key.name === "escape") {
    console.log("Escape pressed!")
  }

  if (key.name === "return") {
    console.log("Enter pressed!")
  }

  if (key.name === "space") {
    console.log("Space pressed!")
  }
})

Modifier combinations

keyHandler.on("keypress", (key: KeyEvent) => {
  // Ctrl+C
  if (key.ctrl && key.name === "c") {
    console.log("Ctrl+C pressed!")
  }

  // Ctrl+S
  if (key.ctrl && key.name === "s") {
    console.log("Save shortcut!")
  }

  // Shift+F1
  if (key.shift && key.name === "f1") {
    console.log("Shift+F1 pressed!")
  }

  // Alt+Enter
  if (key.meta && key.name === "return") {
    console.log("Alt+Enter pressed!")
  }
})

Function keys

keyHandler.on("keypress", (key: KeyEvent) => {
  // F1-F12
  if (key.name === "f1") {
    showHelp()
  }

  if (key.name === "f5") {
    refresh()
  }
})

Arrow keys

keyHandler.on("keypress", (key: KeyEvent) => {
  switch (key.name) {
    case "up":
      moveCursorUp()
      break
    case "down":
      moveCursorDown()
      break
    case "left":
      moveCursorLeft()
      break
    case "right":
      moveCursorRight()
      break
  }
})

Paste events

Handle pasted bytes separately from individual keypresses. Decode them explicitly when you expect text:

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

const textDecoder = new TextDecoder()

keyHandler.on("paste", (event: PasteEvent) => {
  console.log("Pasted bytes:", event.bytes)
  console.log("Decoded text:", textDecoder.decode(event.bytes))
  console.log("Metadata:", event.metadata)
})

Exit on Ctrl+C

Configure the renderer to automatically exit on Ctrl+C:

const renderer = await createCliRenderer({
  exitOnCtrlC: true, // Default behavior
})

// Or handle it manually
const renderer = await createCliRenderer({
  exitOnCtrlC: false,
})

renderer.keyInput.on("keypress", (key: KeyEvent) => {
  if (key.ctrl && key.name === "c") {
    // Custom cleanup before shutdown
    cleanup()
    renderer.destroy()
  }
})

Kitty keyboard protocol

OpenTUI opts into the kitty keyboard protocol when the terminal supports it. The protocol gives more reliable key names and modifier state than legacy input encoding, including release events, disambiguated escape, alt+key sequences, and alternate keys for cross-layout shortcuts. Configure it with the useKittyKeyboard option:

const renderer = await createCliRenderer({
  useKittyKeyboard: {
    disambiguate: true,
    alternateKeys: true,
    events: true, // press/repeat/release
    allKeysAsEscapes: false,
    reportText: false,
  },
})

// Disable entirely:
await createCliRenderer({ useKittyKeyboard: null })
OptionDefaultFlagDescription
disambiguatetrueDISAMBIGUATE_ESCAPE_CODESFixes ESC timing and alt+key ambiguity; reports ctrl+c as a proper key event
alternateKeystrueREPORT_ALTERNATE_KEYSReports shifted/base-layout keys for cross-layout shortcuts
eventsfalseREPORT_EVENT_TYPESReport press, repeat, and release events
allKeysAsEscapesfalseREPORT_ALL_KEYS_AS_ESCAPE_CODESEncode every key as an escape sequence
reportTextfalseREPORT_ASSOCIATED_TEXTInclude the text a key press generates

events: true enables the keyrelease event on renderer.keyInput as well as eventType on KeyEvent.

useKittyKeyboard: {} applies the defaults (disambiguate + alternateKeys). Passing null disables the protocol entirely.

Kitty escape sequences still emit the same canonical key names as legacy input where possible. For example, Kitty Ctrl+Space emits name: "space" with ctrl: true; the literal typed text remains available as sequence: " ", and the original escape sequence remains available as raw.

Focus and key routing

Focus components to receive keyboard input. OpenTUI routes events to the focused component:

import { InputRenderable } from "@opentui/core"

const input = new InputRenderable(renderer, {
  id: "my-input",
  placeholder: "Type here...",
})

// Focus the input to receive key events
input.focus()

// Or with constructs
import { Input } from "@opentui/core"

const inputNode = Input({ placeholder: "Type here..." })
inputNode.focus() // Queued for when instantiated

renderer.root.add(inputNode)