diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 8d3fe487d17c..0dc4fc7c0b22 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -52,6 +52,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex useKeyboard(async (evt) => { if (!store.leader && result.match("leader", evt)) { + evt.preventDefault() leader(true) return } @@ -74,10 +75,6 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex return store.leader }, parse(evt: ParsedKey): Keybind.Info { - // Handle special case for Ctrl+Underscore (represented as \x1F) - if (evt.name === "\x1F") { - return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader) - } return Keybind.fromParsedKey(evt, store.leader) }, match(key: string, evt: ParsedKey) { diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts index 83c7945ae19a..cd19a5136bcb 100644 --- a/packages/opencode/src/util/keybind.ts +++ b/packages/opencode/src/util/keybind.ts @@ -21,15 +21,22 @@ export namespace Keybind { * Convert OpenTUI's ParsedKey to our Keybind.Info format. * This helper ensures all required fields are present and avoids manual object creation. */ - export function fromParsedKey(key: ParsedKey, leader = false): Info { - return { - name: key.name === " " ? "space" : key.name, + export function fromParsedKey( + key: Pick, + leader = false, + ): Info { + const info = { + name: key.name, ctrl: key.ctrl, meta: key.meta, shift: key.shift, super: key.super ?? false, leader, } + if (key.name === "\x00") return { ...info, name: "space", ctrl: true } + if (key.name === "\x1F") return { ...info, name: "_", ctrl: true } + if (key.name === " ") return { ...info, name: "space" } + return info } export function toString(info: Info | undefined): string { diff --git a/packages/opencode/test/keybind.test.ts b/packages/opencode/test/keybind.test.ts index 4ca1f1697e2e..a7a9e7383f30 100644 --- a/packages/opencode/test/keybind.test.ts +++ b/packages/opencode/test/keybind.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect } from "bun:test" +import type { ParsedKey } from "@opentui/core" import { Keybind } from "../src/util/keybind" describe("Keybind.toString", () => { @@ -54,6 +55,17 @@ describe("Keybind.toString", () => { expect(Keybind.toString(info)).toBe("pgup") }) + test("should convert space key to string", () => { + const info: Keybind.Info = { + ctrl: false, + meta: false, + shift: false, + leader: false, + name: "space", + } + expect(Keybind.toString(info)).toBe("space") + }) + test("should handle empty name", () => { const info: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "" } expect(Keybind.toString(info)).toBe("ctrl") @@ -175,6 +187,44 @@ describe("Keybind.match", () => { }) }) +describe("Keybind.fromParsedKey", () => { + test("should parse ' ' as space", () => { + const result = Keybind.fromParsedKey({ name: " ", ctrl: false, meta: false, shift: false, super: false }) + expect(result).toEqual({ + ctrl: false, + meta: false, + shift: false, + super: false, + leader: false, + name: "space", + }) + }) + + test("should parse \x1F as ctrl+_", () => { + const result = Keybind.fromParsedKey({ name: "\x1F", ctrl: false, meta: false, shift: false, super: false }) + expect(result).toEqual({ + ctrl: true, + meta: false, + shift: false, + super: false, + leader: false, + name: "_", + }) + }) + + test("should parse \x00 as ctrl+space", () => { + const result = Keybind.fromParsedKey({ name: "\x00", ctrl: false, meta: false, shift: false, super: false }) + expect(result).toEqual({ + ctrl: true, + meta: false, + shift: false, + super: false, + leader: false, + name: "space", + }) + }) +}) + describe("Keybind.parse", () => { test("should parse simple key", () => { const result = Keybind.parse("f")