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:
| Property | Type | Description |
|---|---|---|
name | string | The key name (e.g., “a”, “escape”, “f1”) |
sequence | string | The raw escape sequence |
ctrl | boolean | Whether Ctrl was held |
shift | boolean | Whether Shift was held |
meta | boolean | Whether Alt/Meta was held |
option | boolean | Whether 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 })
| Option | Default | Flag | Description |
|---|---|---|---|
disambiguate | true | DISAMBIGUATE_ESCAPE_CODES | Fixes ESC timing and alt+key ambiguity; reports ctrl+c as a proper key event |
alternateKeys | true | REPORT_ALTERNATE_KEYS | Reports shifted/base-layout keys for cross-layout shortcuts |
events | false | REPORT_EVENT_TYPES | Report press, repeat, and release events |
allKeysAsEscapes | false | REPORT_ALL_KEYS_AS_ESCAPE_CODES | Encode every key as an escape sequence |
reportText | false | REPORT_ASSOCIATED_TEXT | Include 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)