Solid keymap integration
@opentui/keymap/solid puts a pre-created OpenTUI keymap in Solid context and connects keymap state to Solid signals.
These bindings require Keymap<Renderable, KeyEvent> from @opentui/keymap/opentui.
They do not create or wrap the HTML adapter for browser Solid apps.
Read Solid bindings for OpenTUI Solid setup. Read Keymap hosts for keymap construction.
Exports
The entry point exports these runtime values:
| Export | Purpose |
|---|---|
KeymapProvider | Put an existing OpenTUI keymap in Solid context. |
useKeymap() | Read that keymap. |
useKeymapSelector(selector) | Derive an accessor from keymap state. |
useBindings(createLayer) | Register a layer for the current reactive scope. |
reactiveMatcherFromSignal(accessor, predicate?) | Adapt a Solid accessor to ReactiveMatcher. |
It also exports these types:
| Type | Purpose |
|---|---|
KeymapProviderProps | Provider props with keymap and required children. |
UseBindingsTarget<TRenderable> | Target accessor that can return a renderable, null, or undefined. |
UseBindingsLayer<TRenderable> | Layer shape with a Solid target accessor. |
Basic setup
import { createCliRenderer } from "@opentui/core"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { KeymapProvider, useBindings } from "@opentui/keymap/solid"
import { render } from "@opentui/solid"
const renderer = await createCliRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
function App() {
useBindings(() => ({
commands: [
{
name: "app.quit",
run() {
renderer.destroy()
},
},
],
bindings: [{ key: "q", cmd: "app.quit" }],
}))
return <text>Press q to quit</text>
}
await render(
() => (
<KeymapProvider keymap={keymap}>
<App />
</KeymapProvider>
),
renderer,
)The default OpenTUI factory installs the parser and standard field addons. The provider does not add features and does not own renderer cleanup.
Provider behavior
| Prop | Type | Required |
|---|---|---|
keymap | Keymap<Renderable, KeyEvent> | yes |
children | JSX.Element | yes |
useKeymap() returns the exact provider value. It throws this error outside a provider:
Keymap not found. Wrap the tree in <KeymapProvider>.Keep the provider's keymap instance stable for its owner lifetime.
renderer.destroy() disposes the Solid root and runs hook cleanup.
See Lifecycle and cleanup.
useBindings()
useBindings(createLayer) runs the layer factory in createEffect().
Signals and memos read by the factory become dependencies.
When a dependency changes, the effect runs again and the hook replaces the layer with the new result. If the factory reads no reactive value, the layer remains registered until its owner is disposed.
The hook replaces its layer in one update, so state listeners see one change. The layer keeps the position of its
hook: among layers of the same priority, precedence follows mount order, also while the layer is empty or waits for
a target. Earlier versions moved a re-registered layer to the top.
If the new layer is plain data (plain objects, arrays, primitives, and functions without own properties; no class
instances, Maps, getters, symbol keys, or frozen or sealed objects) and differs only in its functions, the hook
keeps the registered layer: nothing the keymap handed out changes, and commands, binding commands and enabled
checks call the new functions. A Solid store object reports getters; spread it to pass plain data. Object values of
command fields, and any attr or require value from a field compiler (a function too), are handed back or matched
by identity, so they must be the same as before; a new one, even an equal one, registers the layer anew. Function
command fields are forwarded like other functions. state is emitted only if an enabled result differs from the
old functions' result or from one read since the last state event, and a pending key sequence through a layer that
turned off is cleared. Commands of a kept layer are keymap-owned objects. Any other layer, and any layer with a
reactive matcher, is replaced as a new registration in the same position.
The layer can include Core fields such as priority, bindings, and commands.
It can also include fields from installed addons, such as enabled.
Solid replaces the Core target value with a target accessor:
| Shape | Required fields | Behavior |
|---|---|---|
| Global | No target or targetMode | Register a global layer. |
| Local descendants | target accessor | Default to targetMode: "focus-within". |
| Local exact focus | target accessor, targetMode: "focus" | Match only the exact focused renderable. |
If the target accessor returns null or undefined, the hook waits. A later accessor value causes the effect to
register the layer.
Passing targetMode without a target accessor throws:
useBindings local bindings need a target accessorOwner cleanup disposes the current layer. Layer disposal also unsubscribes reactive matchers.
useKeymapSelector()
useKeymapSelector(selector) returns Accessor<T>. Call the accessor to read the current value.
const activeKeys = useKeymapSelector((keymap) => keymap.getActiveKeys({ includeMetadata: true }))
const pendingSequence = useKeymapSelector((keymap) => keymap.getPendingSequence())
activeKeys()
pendingSequence()The hook subscribes to the batched state event on mount. It runs the selector again after each state update and
removes the subscription during owner cleanup.
If the host is destroyed during an update, the selector keeps its previous value for the exact host-destroyed error.
It throws if no previous value exists or if the previous value is undefined.
Use selectors for key hints, command lists, leader prompts, status text, and other derived views. The Core keymap API defines query results and state timing.
Signal matchers
reactiveMatcherFromSignal() converts a Solid accessor to ReactiveMatcher.
Without a predicate, it converts the accessor value to boolean.
useBindings(() => ({
enabled: reactiveMatcherFromSignal(mode, (value) => value === "normal"),
bindings: [{ key: "x", cmd: "editor.delete-line" }],
}))The matcher creates a disposable Solid root for each subscription. Disposing the keymap layer disposes that root and stops signal updates.
Test the integration
Use createTestRenderer() for framework rendering and input. Create the OpenTUI keymap from its renderer, then render
the provider and drive mockInput.
Use @opentui/keymap/testing for host-independent addon tests.
See Testing for renderer cleanup and input helpers.
Complete example: Solid keymap