Files
anomalyco__opentui/packages/core
Kit Langton aef16edbac fix(core): preserve keyboard layouts in embedded terminals (#1477)
## Why

In OpenCode's embedded terminal, Dvorak Ctrl+U and Ctrl+D can reach
Neovim as Ctrl+F and Ctrl+H when the child uses Kitty keyboard encoding.
Paging up instead pages down, and paging down moves the cursor left.

The incoming Kitty event correctly separates the active-layout character
from its base-layout alternative. `EmbeddedTerminalRenderable` currently
passes the alternative as the native encoder's unshifted character,
changing the key's identity.

## What Changes

Keep the active-layout character as the primary key, and use the
base-layout alternative only to infer the physical key when an explicit
physical code is unavailable.

| Incoming Dvorak input | Before | After |
| --- | --- | --- |
| Plain u, base-layout F | u (117), byte `75` hex | Unchanged |
| Ctrl+U, base-layout F | Ctrl+F | Ctrl+U |
| Ctrl+D, base-layout H | Ctrl+H | Ctrl+D |
| Ctrl+Shift+U, base-layout F | Ctrl+Shift+F | Ctrl+Shift+U |

If the child requests alternate-key reporting, the original base-layout
alternative remains available separately. Legacy control-byte output
stays unchanged.

This explains why ordinary typing can work while control shortcuts fail:
in the reproduced Kitty mode, plain letters use the translated text,
while control shortcuts use the incorrectly selected key codepoint.
Ctrl+U becomes `ESC[102;5u` instead of `ESC[117;5u`; Ctrl+D becomes
`ESC[104;5u` instead of `ESC[100;5u`.

Regression coverage parses real Kitty sequences and exercises the native
encoder. One table covers plain Dvorak u/d, control shortcuts, Shift,
Cyrillic input, a QWERTY control case, legacy child mode, and
alternate-key reporting. A focused-delivery test covers
press/repeat/release events.

## Demo

Real Neovim 0.12.4 running in the existing `embedded-terminal-demo.ts`
example with the production `EmbeddedTerminalRenderable`. The inspector
shows:

1. The received key, active-layout and base-layout codepoints, and raw
input sequence.
2. The actual `onData` output forwarded unchanged to the PTY, in escaped
and hexadecimal form. The inspector does not re-encode the key.
3. Neovim's independently observed key via `vim.on_key`, plus its
resulting cursor position.

Each case starts at line 80, column 8, with `scroll=8`. Plain u is typed
in Insert mode; Ctrl+D/U are pressed in Normal mode. The clean Neovim
fixture explicitly enables Kitty mode with `CSI > 1 u`; Dvorak Kitty
input sequences are injected at the outer terminal boundary rather than
generated by an OS keyboard layout.

Before uses the unchanged `EmbeddedTerminal.ts` from `fe547ebe`, loaded
through a test-only module override. After uses `03b84c9a`. The three
independent cases are reordered to show plain typing first, slowed to
0.1×, and held for four seconds each; setup and resets are omitted. Both
processes returned cleanly after the check.

**Before:** Plain u stays 117. Ctrl+D changes 100 → 104 and Neovim sees
`<C-H>`; Ctrl+U changes 117 → 102 and Neovim sees `<C-F>`.


https://github.com/user-attachments/assets/6e1b469f-e4d2-4298-a095-8f422e10a8d0

**After:** Plain u still works. Ctrl+D stays 100 and Neovim sees
`<C-D>`; Ctrl+U stays 117 and Neovim sees `<C-U>`.


https://github.com/user-attachments/assets/b825e1f0-b7e6-4f1b-a112-e780a9f34fe8

## Scope

This fixes OpenTUI's embedded-terminal key adapter, without
application-specific remapping or a public API change. OpenCode will
need to adopt an OpenTUI release containing the fix.

## Verification

```sh
cd packages/core
bun run test src/renderables/EmbeddedTerminal.test.ts
bun run typecheck
bun run test:js:node
bun run build:lib
bun run test
cd ../..
bun run fmt:check
bun run lint
```

- Focused suite: seven regressions fail with the baseline encoder, while
plain typing passes; all 31 tests pass afterward. Rechecked after
consolidating the repeated test setup.
- Full Bun suite after building the parser assets: 5,587 passed, 26
skipped, zero failures. The initial unbuilt-worktree run failed only
because `parser.worker.js` was absent.
- Node suite: 4,842 passed, seven skipped, zero failures.
- Typechecking, formatting, and lint pass.
- Real PTY before/after verification shown above.
2026-09-03 20:37:56 -04:00
..
2026-07-27 23:55:19 +02:00

OpenTUI logo

npm version Core build status

OpenTUI is a library to build terminal user interfaces.

  • It is written in Zig.
  • You write TypeScript directly or through React and Solid.
  • You arrange boxes and text with flexbox.
  • You add selects, inputs, and scroll boxes with keyboard and mouse controls.
  • You can play sounds, show images, and render 3D graphics.

OpenCode uses OpenTUI in production for millions of users.

Website | Documentation | Packages

OpenTUI Core

@opentui/core supplies the renderer and TypeScript API. You use imperative renderables and events directly. The TypeScript packages call native Zig through an internal foreign function interface (FFI) boundary.

import { TextRenderable, createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer({ exitOnCtrlC: true })

renderer.root.add(new TextRenderable(renderer, { content: "Hello, OpenTUI!" }))

exitOnCtrlC: true calls renderer.destroy() when the user presses Ctrl+C. The code that creates the renderer must call renderer.destroy() on every other shutdown path.

Install the package:

bun add @opentui/core

Then build your first app with the quickstart.

Runtime and platform support

@opentui/core runs on Bun 1.3.0 or later, or on Node.js 26.4.0 or later with ECMAScript modules (ESM) and --experimental-ffi.

The native ABI and the generated platform packages, such as @opentui/core-linux-x64, are internal distribution surfaces, not application APIs.

AI agent skill

Install the OpenTUI documentation as a skill for your AI coding assistant with npx skills:

npx skills add anomalyco/opentui --skill opentui

Add -g to install the skill globally.

License

OpenTUI is licensed under the MIT License.