diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 4c82e594c3e..c73fa000c94 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -51,6 +51,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex useKeyboard(async (evt) => { if (!store.leader && result.match("leader", evt)) { + evt.preventDefault() leader(true) return } @@ -73,10 +74,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: keyof KeybindsConfig, evt: ParsedKey) { diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts index 69fef28f0d9..28a11245908 100644 --- a/packages/opencode/src/util/keybind.ts +++ b/packages/opencode/src/util/keybind.ts @@ -10,6 +10,13 @@ export namespace Keybind { leader: boolean // our custom field } + const normalizeKey = (key: ParsedKey): ParsedKey => { + if (key.name === "\x00") return { ...key, name: "space", ctrl: true } + if (key.name === "\x1F") return { ...key, name: "_", ctrl: true } + if (key.name === " ") return { ...key, name: "space" } + return key + } + export function match(a: Info, b: Info): boolean { // Normalize super field (undefined and false are equivalent) const normalizedA = { ...a, super: a.super ?? false } @@ -22,12 +29,13 @@ export namespace Keybind { * This helper ensures all required fields are present and avoids manual object creation. */ export function fromParsedKey(key: ParsedKey, leader = false): Info { + const normalized = normalizeKey(key) return { - name: key.name, - ctrl: key.ctrl, - meta: key.meta, - shift: key.shift, - super: key.super ?? false, + name: normalized.name ?? "", + ctrl: normalized.ctrl, + meta: normalized.meta, + shift: normalized.shift, + super: normalized.super ?? false, leader, } } @@ -39,9 +47,10 @@ export namespace Keybind { if (info.meta) parts.push("alt") if (info.super) parts.push("super") if (info.shift) parts.push("shift") - if (info.name) { - if (info.name === "delete") parts.push("del") - else parts.push(info.name) + const name = info.name === " " ? "space" : info.name + if (name) { + if (name === "delete") parts.push("del") + if (name !== "delete") parts.push(name) } let result = parts.join("+") diff --git a/packages/opencode/test/keybind.test.ts b/packages/opencode/test/keybind.test.ts index 4ca1f1697e2..aaea035304c 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,21 @@ describe("Keybind.match", () => { }) }) +describe("Keybind.fromParsedKey", () => { + test("should normalize ctrl+space NUL", () => { + const key = { name: "\x00", ctrl: false, meta: false, shift: false, super: false } as ParsedKey + const result = Keybind.fromParsedKey(key) + 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")