/** @jsxImportSource @opentui/solid */ import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" import { createBindingLookup } from "@opentui/keymap/extras" import { type TextareaRenderable } from "@opentui/core" import { testRender, useRenderer } from "@opentui/solid" import { expect, test } from "bun:test" import { onCleanup, onMount } from "solid-js" import { TuiKeybind } from "../src/config/keybind" import { formatKeySequence, getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap, } from "../src/keymap" function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) { const keybinds = TuiKeybind.parse(input) return { keybinds: createBindingLookup(TuiKeybind.toBindingConfig(keybinds), { commandMap: TuiKeybind.CommandMap, bindingDefaults: TuiKeybind.bindingDefaults(), }), leader_timeout: 2000, } } test("legacy page key aliases compile as page keys", async () => { const sequences: Record = {} function Harness() { const renderer = useRenderer() const keymap = createDefaultOpenTuiKeymap(renderer) const config = createResolvedKeymapConfig({ messages_page_up: "pgup", messages_page_down: "pgdown", }) const offKeymap = registerOpencodeKeymap(keymap, renderer, config) const offLayer = keymap.registerLayer({ bindings: ["session.page.up", "session.page.down"].flatMap((command) => config.keybinds.get(command)), }) const bindings = keymap.getCommandBindings({ visibility: "registered", commands: ["session.page.up", "session.page.down"], }) sequences.up = bindings.get("session.page.up")?.map((binding) => binding.sequence.map((part) => part.stroke.name)) ?? [] sequences.down = bindings.get("session.page.down")?.map((binding) => binding.sequence.map((part) => part.stroke.name)) ?? [] onCleanup(() => { offLayer() offKeymap() }) return ( ) } const app = await testRender(() => ) try { expect(sequences).toEqual({ up: [["pageup"]], down: [["pagedown"]], }) } finally { app.renderer.destroy() } }) test("formats navigation keys as arrows", async () => { const shortcuts: Record = {} function Harness() { const renderer = useRenderer() const keymap = createDefaultOpenTuiKeymap(renderer) const config = createResolvedKeymapConfig() const offKeymap = registerOpencodeKeymap(keymap, renderer, config) const commands = ["session.parent", "session.child.first", "session.child.previous", "session.child.next"] const offLayer = keymap.registerLayer({ bindings: commands.flatMap((command) => config.keybinds.get(command)), }) const bindings = keymap.getCommandBindings({ visibility: "registered", commands }) commands.forEach((command) => { shortcuts[command] = formatKeySequence(bindings.get(command)?.[0]?.sequence, config) }) onCleanup(() => { offLayer() offKeymap() }) return ( ) } const app = await testRender(() => ) try { expect(shortcuts).toEqual({ "session.parent": "↑", "session.child.first": "↓", "session.child.previous": "←", "session.child.next": "→", }) } finally { app.renderer.destroy() } }) test("dispatches message navigation while the composer is focused", async () => { for (const kittyKeyboard of [false, true]) { const counts = { "session.first": 0, "session.message.previous": 0, "session.message.next": 0, "session.messages_last_user": 0, } function Harness() { const renderer = useRenderer() const keymap = createDefaultOpenTuiKeymap(renderer) const config = createResolvedKeymapConfig() const offKeymap = registerOpencodeKeymap(keymap, renderer, config) const commands = Object.keys(counts) as (keyof typeof counts)[] const offLayer = keymap.registerLayer({ commands: commands.map((name) => ({ name, run() { counts[name]++ }, })), bindings: commands.flatMap((command) => config.keybinds.get(command)), }) let textarea: TextareaRenderable onMount(() => textarea.focus()) onCleanup(() => { offLayer() offKeymap() }) return (