Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/opencode/src/cli/cmd/tui/context/keybind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down
25 changes: 17 additions & 8 deletions packages/opencode/src/util/keybind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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,
}
}
Expand All @@ -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("+")
Expand Down
27 changes: 27 additions & 0 deletions packages/opencode/test/keybind.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down