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/keymap/hosts.mdx

docs/keymap/hosts.mdxBrowse 89 files
View on GitHub
← Back to SKILL.md

title: Keymap hosts description: Connect Keymap to OpenTUI, HTML, or a custom input host

Keymap hosts

A host connects the shared Keymap engine to a runtime's targets, focus, keyboard events, metadata, and lifecycle. This page is the reference for KeymapHost<TTarget, TEvent> and the two built-in adapters.

Use @opentui/keymap/opentui for terminal apps built on CliRenderer and Renderable. Use @opentui/keymap/html for browser UIs rooted in an HTMLElement.

Start with Keymap if you need the register, dispatch, and query model first.

Host responsibilities

The engine treats targets and events as opaque host values. A host supplies these behaviors:

  • Identify the root and the current focused target.
  • Traverse from a focused target to its parents.
  • Report whether the host or a target is destroyed.
  • Send key press, key release, and focus events.
  • Report target destruction so local layers can unregister.
  • Supply platform and modifier capabilities.
  • Create a command event when a programmatic command does not supply one.
  • Optionally send raw input before key parsing.
  • Optionally report host destruction.

KeymapHost

MemberRequiredPurpose
metadatayesPlatform, primary shortcut modifier, and modifier capabilities
rootTargetyesRoot of the target hierarchy
isDestroyedyesCurrent host lifetime state
getFocusedTarget()yesReturn the focused target or null
getParentTarget(target)yesReturn the target's parent or null
isTargetDestroyed(target)yesTest target liveness
onKeyPress(listener)yesSubscribe to press events and return a disposer
onKeyRelease(listener)yesSubscribe to release events and return a disposer
onFocusChange(listener)yesSubscribe to focus changes and return a disposer
onTargetDestroy(target, listener)yesSubscribe to one target's destruction and return a disposer
createCommandEvent()yesCreate the default event for runCommand() and dispatchCommand()
onDestroy(listener)noSubscribe to host destruction and return a disposer
onRawInput(listener)noSend raw input before key parsing and return a disposer

Pass a custom implementation to new Keymap(host). The constructor throws if host.isDestroyed is already true.

Focus hierarchy

A targetless layer is global. A targeted layer defaults to targetMode: "focus-within". It is active when its target is on the focused target's parent path.

targetMode: "focus" requires an exact focused-target match. If no target has focus, the activation path starts at rootTarget. Layer precedence still comes from priority and registration order, not hierarchy depth.

Every focus change clears the pending key sequence. The engine also updates state subscribers after the change.

Target lifecycle

onTargetDestroy() lets the engine unregister a layer that owns that target. The returned subscription disposer must remove only that listener. isTargetDestroyed() is the fallback liveness check during registration and activation.

Host metadata

keymap.getHostMetadata() returns HostMetadata:

FieldValuesMeaning
platformmacos, windows, linux, unknownHost platform for shortcut policy
primaryModifiersuper, ctrl, unknownModifier for addon syntax such as mod+s
modifiersRecord<HostModifier, HostCapability>Capability for ctrl, shift, meta, super, and hyper

Each capability is supported, unsupported, or unknown. Use unknown when an event can represent a modifier but the runtime cannot prove that input will deliver it.

Key events

Host events must implement KeymapEvent:

MemberPurpose
nameNormalized key name
ctrl, shift, metaRequired modifier state
super, hyperOptional modifier state
preventDefault()Prevent the matched event's default host behavior
stopPropagation()Stop later host listeners and set propagationStopped
propagationStoppedReport whether propagation stopped

If a host supplies onRawInput(), it must call raw listeners before key parsing. It must stop when a listener returns true.

See Keyboard input for OpenTUI KeyEvent fields and terminal protocol behavior.

Host cleanup

The engine subscribes to host input and focus when you construct Keymap. If the host supplies onDestroy(), host destruction clears pending input, disposes acquired resources, detaches target listeners, and removes host listeners.

Keymap has no separate public destroy() method. The host owns the engine lifetime. See Lifecycle and cleanup for renderer ownership and shutdown paths.

Built-in helper summary

PackageHost factoryBare keymap factoryDefault keymap factory
@opentui/keymap/opentuicreateOpenTuiKeymapHost(renderer)createOpenTuiKeymap(renderer)createDefaultOpenTuiKeymap(renderer)
@opentui/keymap/htmlcreateHtmlKeymapHost(root)createHtmlKeymap(root)createDefaultHtmlKeymap(root)

A host factory returns KeymapHost. A bare keymap factory adds only the host. A default keymap factory also installs the small default addon set for that host.

OpenTUI adapter

@opentui/keymap/opentui exports exactly these runtime helpers:

  • createOpenTuiKeymapHost(renderer)
  • createOpenTuiKeymap(renderer)
  • createDefaultOpenTuiKeymap(renderer)

The entry point uses CliRenderer, Renderable, and KeyEvent from @opentui/core.

Create an OpenTUI keymap

import { createCliRenderer } from "@opentui/core"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"

const renderer = await createCliRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)

keymap.registerLayer({
  commands: [
    {
      name: "app.quit",
      run() {
        renderer.destroy()
      },
    },
  ],
  bindings: [{ key: "q", cmd: "app.quit" }],
})

Use createOpenTuiKeymap(renderer) when you want to install every parser and addon yourself. Both keymap factories throw if the renderer is already destroyed.

OpenTUI behavior

Host behaviorOpenTUI adapter
Root targetrenderer.root
Focused targetrenderer.currentFocusedRenderable when it is focused and not destroyed
Parent traversalRenderable.parent
Target destructionRenderableEvents.DESTROYED
Host destructionCliRenderEvents.DESTROY
Press and releaserenderer.keyInput events keypress and keyrelease
Focus changesCliRenderEvents.FOCUSED_RENDERABLE
Raw inputrenderer.prependInputHandler(...)
Synthetic command eventA new KeyEvent with name: "command"
MetadataRuntime platform and terminal keyboard capabilities

The adapter reports ctrl, shift, and meta as supported. It reports super and hyper as supported when the renderer detects Kitty keyboard support. Otherwise, it reports those capabilities as unknown.

Press and release events keep all OpenTUI-specific KeyEvent fields. Addons can therefore read fields such as baseCode.

OpenTUI default set

createDefaultOpenTuiKeymap(renderer) installs these addons in order:

  1. registerDefaultKeys()
  2. registerEnabledFields()
  3. registerMetadataFields()

It does not install leader keys, ex commands, sequence disambiguation, warning analyzers, base-layout fallback, or textarea integration.

@opentui/keymap/addons/opentui adds these OpenTUI integrations:

  • registerBaseLayoutFallback()
  • createTextareaBindings()
  • registerEditBufferCommands()
  • registerTextareaMappingSuspension()
  • registerManagedTextareaLayer()

See the complete OpenTUI keymap example.

HTML adapter

@opentui/keymap/html exports these runtime values and the HtmlKeymapEvent type:

  • normalizeHtmlKeyName(key)
  • createHtmlKeymapEvent(event?)
  • createHtmlKeymapHost(root)
  • htmlEventMatchResolver
  • createHtmlKeymap(root)
  • createDefaultHtmlKeymap(root)
  • HtmlKeymapEvent

HtmlKeymapEvent extends KeymapEvent and can include originalEvent: KeyboardEvent.

Live demo: HTML keymap demo

Create an HTML keymap

import { createDefaultHtmlKeymap } from "@opentui/keymap/html"

const root = document.getElementById("app")!
const keymap = createDefaultHtmlKeymap(root)

keymap.registerLayer({
  commands: [
    {
      name: "help.toggle",
      run() {
        document.body.classList.toggle("help-open")
      },
    },
  ],
  bindings: [{ key: "?", cmd: "help.toggle" }],
})

Use createHtmlKeymap(root) when you want to install parsers and event matchers yourself.

HTML key normalization

Browser inputKeymap valueNotes
ArrowLeftleftNavigation keys use shared names
EnterreturnStringifiers display the canonical stroke as enter
AaPrintable names become lowercase
F12f12Function keys become lowercase
altKeymetaKeymap uses meta for Alt or Option
metaKeysuperKeymap uses super for the platform Meta key

The HTML event matcher adds an unshifted candidate for shifted printable punctuation. A key binding such as "?" can therefore match without the spelling "shift+?".

HTML behavior

Host behaviorHTML adapter
Root targetThe HTMLElement passed to the factory
Focused targetdocument.activeElement when it is the root or a descendant
Parent traversalHTMLElement.parentElement
Target destructionMutationObserver on the root subtree when available
Host destructionNo explicit destroy event
Press and releaseCapture-phase keydown and keyup listeners on the root
Focus changesCapture-phase focusin and focusout, followed by a microtask read
Raw inputNot available
Synthetic command eventcreateHtmlKeymapEvent() without a DOM event
MetadataBrowser platform with hyper unsupported

When MutationObserver is available, removing a target from the root unregisters its local layers. Without it, the liveness check keeps the target inactive, but the layer remains until its disposer runs.

The HTML host has no explicit destroy signal. Dispose registrations when you remove a root that remains reachable.

HTML default set

createDefaultHtmlKeymap(root) installs these features in order:

  1. registerDefaultKeys()
  2. registerEnabledFields()
  3. registerMetadataFields()
  4. htmlEventMatchResolver through prependEventMatchResolver(...)

The prepend call makes the HTML candidates run before the shared canonical event matcher.

Custom hosts

Implement KeymapHost when neither built-in adapter matches your runtime:

import { Keymap, type KeymapHost } from "@opentui/keymap"

function createCustomKeymap(host: KeymapHost<object>) {
  return new Keymap(host)
}

Return a disposer from every subscription. Use conservative metadata and report unknown for capabilities that the host cannot prove.