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("Sequence:", key.sequence)
  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:

PropertyTypeDescription
namestringThe key name (e.g., “a”, “escape”, “f1”)
sequencestringThe raw escape sequence
ctrlbooleanWhether Ctrl was held
shiftbooleanWhether Shift was held
metabooleanWhether Alt/Meta was held
optionbooleanWhether Option was held (macOS)

Key aliases

OpenTUI normalizes several key names so your keybindings work the same across layouts and input paths. OpenTUI applies aliases after the parser reports a key, before emitting keypress.

The default aliases are:

import { defaultKeyAliases } from "@opentui/core"

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

Both key.name === "enter" and key.name === "kpenter" resolve to "return" when you match them with matchesKeyBinding. Editable components that accept a keyAliasMap option (InputRenderable, TextareaRenderable, SelectRenderable, etc.) merge these defaults with your own map:

import { mergeKeyAliases, defaultKeyAliases, InputRenderable } from "@opentui/core"

const input = new InputRenderable(renderer, {
  id: "field",
  keyAliasMap: mergeKeyAliases(defaultKeyAliases, {
    // 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.

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)