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:
| Property | Type | Description |
|---|---|---|
name | string | Canonical key identity, such as "a", "space", "return", "escape", or "f1" |
sequence | string | Decoded input sequence/text for the key. For printable keys this is the text, such as "a" or " " |
raw | string | Exact terminal input sequence for this key event, decoded as a string |
source | "raw" | "kitty" | Parser path that produced the event |
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 Alt/Option path) |
super | boolean? | Whether Super/Cmd/Windows was held, when reported |
hyper | boolean? | Whether Hyper was held, when reported |
eventType | "press" | "repeat" | "release" | Key event type. Kitty repeat events are emitted as "press" with repeated: true |
repeated | boolean? | true for Kitty repeat events |
number | boolean | true when the parsed key is a digit from legacy input |
code | string? | Terminal key code for recognized function-style escape sequences |
capsLock | boolean? | Kitty Caps Lock modifier state, when reported |
numLock | boolean? | Kitty Num Lock modifier state, when reported |
baseCode | number? | 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 })
| 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.
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)