mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-15 17:13:12 +00:00
flatten to keybind compatible config (#26421)
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import type { KeyEvent, Renderable } from "@opentui/core"
|
||||
import type { Binding } from "@opentui/keymap"
|
||||
import { resolveBindingSections, type BindingSectionsConfig } from "@opentui/keymap/extras"
|
||||
import { createBindingLookup } from "@opentui/keymap/extras"
|
||||
import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2"
|
||||
import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
|
||||
import { formatBindings } from "@/cli/cmd/run/keymap.shared"
|
||||
import { KeymapSectionNames, keymapBindingDefaults, type KeymapSection } from "@/cli/cmd/tui/config/tui-schema"
|
||||
import { ConfigKeybinds } from "@/config/keybinds"
|
||||
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
|
||||
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot"
|
||||
|
||||
type RunBinding = Binding<Renderable, KeyEvent>
|
||||
@@ -82,34 +81,24 @@ function config(input?: {
|
||||
}>
|
||||
}): Resolved {
|
||||
const bind = input?.bindings
|
||||
const sections = {
|
||||
global: Object.fromEntries([
|
||||
...(bind?.commandList ? [["command.palette.show", bind.commandList] as const] : []),
|
||||
...(bind?.variantCycle ? [["variant.cycle", bind.variantCycle] as const] : []),
|
||||
]),
|
||||
prompt: Object.fromEntries([
|
||||
...(bind?.interrupt ? [["session.interrupt", bind.interrupt] as const] : []),
|
||||
...(bind?.historyPrevious ? [["prompt.history.previous", bind.historyPrevious] as const] : []),
|
||||
...(bind?.historyNext ? [["prompt.history.next", bind.historyNext] as const] : []),
|
||||
...(bind?.inputClear ? [["prompt.clear", bind.inputClear] as const] : []),
|
||||
]),
|
||||
input: Object.fromEntries([
|
||||
...(bind?.inputSubmit ? [["input.submit", bind.inputSubmit] as const] : []),
|
||||
...(bind?.inputNewline ? [["input.newline", bind.inputNewline] as const] : []),
|
||||
]),
|
||||
} satisfies BindingSectionsConfig<Renderable, KeyEvent>
|
||||
|
||||
const keybinds = TuiKeybind.Keybinds.parse({
|
||||
...(input?.leader && { leader: input.leader }),
|
||||
...(bind?.commandList && { command_list: bind.commandList }),
|
||||
...(bind?.variantCycle && { variant_cycle: bind.variantCycle }),
|
||||
...(bind?.interrupt && { session_interrupt: bind.interrupt }),
|
||||
...(bind?.historyPrevious && { history_previous: bind.historyPrevious }),
|
||||
...(bind?.historyNext && { history_next: bind.historyNext }),
|
||||
...(bind?.inputClear && { input_clear: bind.inputClear }),
|
||||
...(bind?.inputSubmit && { input_submit: bind.inputSubmit }),
|
||||
...(bind?.inputNewline && { input_newline: bind.inputNewline }),
|
||||
})
|
||||
return {
|
||||
diff_style: input?.diff_style,
|
||||
keybinds: ConfigKeybinds.Keybinds.parse({}),
|
||||
keymap: {
|
||||
leader: input?.leader ?? "ctrl+x",
|
||||
leader_timeout: input?.leaderTimeout ?? 2000,
|
||||
...resolveBindingSections<Renderable, KeyEvent, typeof sections, KeymapSection>(sections, {
|
||||
sections: KeymapSectionNames,
|
||||
bindingDefaults: keymapBindingDefaults,
|
||||
}),
|
||||
},
|
||||
keybinds: createBindingLookup(TuiKeybind.toBindingConfig(keybinds), {
|
||||
commandMap: TuiKeybind.CommandMap,
|
||||
bindingDefaults: TuiKeybind.bindingDefaults(),
|
||||
}),
|
||||
leader_timeout: input?.leaderTimeout ?? 2000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +107,7 @@ describe("run runtime boot", () => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
test("reads footer keybinds from resolved keymap config", async () => {
|
||||
test("reads footer keybinds from resolved keybind config", async () => {
|
||||
spyOn(TuiConfig, "get").mockResolvedValue(
|
||||
config({
|
||||
leader: "ctrl+g",
|
||||
|
||||
@@ -81,7 +81,7 @@ async function load(): Promise<Data> {
|
||||
|
||||
await Bun.write(
|
||||
localPluginPath,
|
||||
`import { resolveBindingSections } from "@opentui/keymap/extras"
|
||||
`import { createBindingLookup } from "@opentui/keymap/extras"
|
||||
import { useBindings } from "@opentui/keymap/solid"
|
||||
|
||||
export const ignored = async (_input, options) => {
|
||||
@@ -97,20 +97,18 @@ export default {
|
||||
const cfg_diff = api.tuiConfig.diff_style
|
||||
const cfg_speed = api.tuiConfig.scroll_speed
|
||||
const cfg_accel = api.tuiConfig.scroll_acceleration?.enabled
|
||||
const cfg_submit = api.tuiConfig.keybinds?.input_submit
|
||||
const has_keys = typeof api.keys.formatBindings === "function"
|
||||
const keymap = resolveBindingSections(options.keymap?.sections ?? {
|
||||
main: {
|
||||
"plugin.loader.local": "ctrl+shift+m",
|
||||
"plugin.loader.close": "escape",
|
||||
},
|
||||
}, { sections: ["main"] }).sections
|
||||
const key_modal = keymap.main.find((item) => item.cmd === "plugin.loader.local")?.key
|
||||
const key_close = keymap.main.find((item) => item.cmd === "plugin.loader.close")?.key
|
||||
const keybinds = createBindingLookup(options.keybinds ?? {
|
||||
"plugin.loader.local": "ctrl+shift+m",
|
||||
"plugin.loader.close": "escape",
|
||||
})
|
||||
const bindings = keybinds.gather("plugin.loader", ["plugin.loader.local", "plugin.loader.close"])
|
||||
const key_modal = bindings.find((item) => item.cmd === "plugin.loader.local")?.key
|
||||
const key_close = bindings.find((item) => item.cmd === "plugin.loader.close")?.key
|
||||
const key_unknown = "ctrl+k"
|
||||
const off = api.keymap.registerLayer({
|
||||
commands: [{ name: "plugin.loader.local", run() {} }, { name: "plugin.loader.close", run() {} }],
|
||||
bindings: keymap.main,
|
||||
bindings,
|
||||
})
|
||||
off()
|
||||
const kv_before = api.kv.get(options.kv_key, "missing")
|
||||
@@ -153,7 +151,7 @@ export default {
|
||||
key_unknown,
|
||||
has_keys,
|
||||
has_keymap: typeof api.keymap.registerLayer === "function",
|
||||
has_resolve_binding_sections: typeof resolveBindingSections === "function",
|
||||
has_create_binding_lookup: typeof createBindingLookup === "function",
|
||||
has_keymap_solid: typeof useBindings === "function",
|
||||
kv_before,
|
||||
kv_after,
|
||||
@@ -176,7 +174,6 @@ export default {
|
||||
cfg_diff,
|
||||
cfg_speed,
|
||||
cfg_accel,
|
||||
cfg_submit,
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -356,13 +353,9 @@ export default {
|
||||
theme_name: tmp.extra.localThemeName,
|
||||
kv_key: "plugin_state_key",
|
||||
session_id: "ses_test",
|
||||
keymap: {
|
||||
sections: {
|
||||
main: {
|
||||
"plugin.loader.local": "ctrl+alt+m",
|
||||
"plugin.loader.close": "q",
|
||||
},
|
||||
},
|
||||
keybinds: {
|
||||
"plugin.loader.local": "ctrl+alt+m",
|
||||
"plugin.loader.close": "q",
|
||||
},
|
||||
}
|
||||
const invalidOpts = {
|
||||
@@ -408,9 +401,6 @@ export default {
|
||||
diff_style: "stacked",
|
||||
scroll_speed: 1.5,
|
||||
scroll_acceleration: { enabled: true },
|
||||
keybinds: {
|
||||
input_submit: "ctrl+enter",
|
||||
},
|
||||
},
|
||||
state: {
|
||||
session: {
|
||||
@@ -670,7 +660,7 @@ describe("tui.plugin.loader", () => {
|
||||
expect(data.local.key_unknown).toBe("ctrl+k")
|
||||
expect(data.local.has_keys).toBe(true)
|
||||
expect(data.local.has_keymap).toBe(true)
|
||||
expect(data.local.has_resolve_binding_sections).toBe(true)
|
||||
expect(data.local.has_create_binding_lookup).toBe(true)
|
||||
expect(data.local.has_keymap_solid).toBe(true)
|
||||
expect(data.local.kv_before).toBe("missing")
|
||||
expect(data.local.kv_after).toBe("stored")
|
||||
@@ -693,7 +683,6 @@ describe("tui.plugin.loader", () => {
|
||||
expect(data.local.cfg_diff).toBe("stacked")
|
||||
expect(data.local.cfg_speed).toBe(1.5)
|
||||
expect(data.local.cfg_accel).toBe(true)
|
||||
expect(data.local.cfg_submit).toBe("ctrl+enter")
|
||||
})
|
||||
|
||||
test("installs themes in the correct scope and remains resilient", () => {
|
||||
|
||||
@@ -171,26 +171,26 @@ test("loads disabled-by-default internal plugin inactive and activates on demand
|
||||
enabled: true,
|
||||
active: true,
|
||||
})
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "tui-which-key")).toEqual({
|
||||
id: "tui-which-key",
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "which-key")).toEqual({
|
||||
id: "which-key",
|
||||
source: "internal",
|
||||
spec: "tui-which-key",
|
||||
target: "tui-which-key",
|
||||
spec: "which-key",
|
||||
target: "which-key",
|
||||
enabled: false,
|
||||
active: false,
|
||||
})
|
||||
|
||||
await expect(TuiPluginRuntime.activatePlugin("tui-which-key")).resolves.toBe(true)
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "tui-which-key")).toEqual({
|
||||
id: "tui-which-key",
|
||||
await expect(TuiPluginRuntime.activatePlugin("which-key")).resolves.toBe(true)
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "which-key")).toEqual({
|
||||
id: "which-key",
|
||||
source: "internal",
|
||||
spec: "tui-which-key",
|
||||
target: "tui-which-key",
|
||||
spec: "which-key",
|
||||
target: "which-key",
|
||||
enabled: true,
|
||||
active: true,
|
||||
})
|
||||
expect(api.kv.get("plugin_enabled", {})).toEqual({
|
||||
"tui-which-key": true,
|
||||
"which-key": true,
|
||||
})
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
|
||||
@@ -163,7 +163,7 @@ test("migrates tui-specific keys from opencode.json when tui.json does not exist
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(5)
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
expect(config.keybinds.get("app.exit")?.[0]?.key).toBe("ctrl+q")
|
||||
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
||||
expect(JSON.parse(text)).toMatchObject({
|
||||
theme: "migrated-theme",
|
||||
@@ -398,83 +398,64 @@ test("merges keybind overrides across precedence layers", async () => {
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
expect(config.keybinds?.theme_list).toBe("ctrl+k")
|
||||
expect(config.keybinds.get("app.exit")?.[0]?.key).toBe("ctrl+q")
|
||||
expect(config.keybinds.get("theme.switch")?.[0]?.key).toBe("ctrl+k")
|
||||
})
|
||||
|
||||
test("resolves semantic keymap sections", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
keybinds: { command_list: "ctrl+z" },
|
||||
keymap: {
|
||||
sections: {
|
||||
global: { "command.palette.show": "alt+p" },
|
||||
which_key: { "tui-which-key.toggle": "alt+k" },
|
||||
prompt: { "prompt.editor": "ctrl+e" },
|
||||
autocomplete: { "prompt.autocomplete.next": "ctrl+j" },
|
||||
dialog_actions: { "dialog.action.toggle": "ctrl+t" },
|
||||
model: { "model.dialog.favorite": "ctrl+f" },
|
||||
plugins: { "plugin.dialog.install": "shift+i" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keymap.sections.global.find((binding) => binding.cmd === "command.palette.show")?.key).toBe("alt+p")
|
||||
expect(config.keymap.sections.global.find((binding) => binding.cmd === "session.new")?.key).toBe("<leader>n")
|
||||
expect(config.keymap.sections.which_key.find((binding) => binding.cmd === "tui-which-key.toggle")?.key).toBe("alt+k")
|
||||
expect(config.keymap.sections.which_key.find((binding) => binding.cmd === "tui-which-key.layout.toggle")?.key).toBe(
|
||||
"ctrl+alt+shift+k",
|
||||
)
|
||||
expect(config.keymap.sections.which_key.find((binding) => binding.cmd === "tui-which-key.pending.toggle")?.key).toBe(
|
||||
"ctrl+alt+shift+p",
|
||||
)
|
||||
expect(config.keymap.sections.which_key.find((binding) => binding.cmd === "tui-which-key.group.next")?.key).toBe(
|
||||
"ctrl+alt+right,ctrl+alt+]",
|
||||
)
|
||||
expect(
|
||||
(
|
||||
config.keymap.sections.which_key.find((binding) => binding.cmd === "tui-which-key.toggle") as
|
||||
| { group?: unknown }
|
||||
| undefined
|
||||
)?.group,
|
||||
).toBe("System")
|
||||
expect(config.keymap.sections.prompt.find((binding) => binding.cmd === "prompt.editor")?.key).toBe("ctrl+e")
|
||||
expect(config.keymap.sections.autocomplete.find((binding) => binding.cmd === "prompt.autocomplete.next")?.key).toBe(
|
||||
"ctrl+j",
|
||||
)
|
||||
expect(config.keymap.sections.dialog_actions.find((binding) => binding.cmd === "dialog.action.toggle")?.key).toBe(
|
||||
"ctrl+t",
|
||||
)
|
||||
expect(config.keymap.sections.model.find((binding) => binding.cmd === "model.dialog.favorite")?.key).toBe("ctrl+f")
|
||||
expect(config.keymap.sections.plugins.find((binding) => binding.cmd === "plugin.dialog.install")?.key).toBe("shift+i")
|
||||
expect(config.keymap.pick("plugins", ["plugin.dialog.install"]).map((binding) => binding.cmd)).toEqual([
|
||||
"plugin.dialog.install",
|
||||
])
|
||||
expect((config.keymap.pick("plugins", ["plugin.dialog.install"])[0] as { group?: unknown } | undefined)?.group).toBe(
|
||||
"Plugins",
|
||||
)
|
||||
expect(config.keymap.omit("plugins", ["plugin.dialog.install"]).map((binding) => binding.cmd)).toEqual([])
|
||||
})
|
||||
|
||||
test("legacy keybinds transform into semantic keymap sections", async () => {
|
||||
test("resolves keybind lookup from canonical keybinds", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
keybinds: {
|
||||
leader: { key: { name: "g", ctrl: true } },
|
||||
command_list: "alt+p",
|
||||
which_key_toggle: "alt+k",
|
||||
editor_open: "ctrl+e",
|
||||
"prompt.autocomplete.next": "ctrl+j",
|
||||
"dialog.mcp.toggle": "ctrl+t",
|
||||
model_favorite_toggle: "ctrl+f",
|
||||
"dialog.plugins.install": "shift+i",
|
||||
},
|
||||
leader_timeout: 1234,
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds.get("leader")?.[0]?.key).toEqual({ name: "g", ctrl: true })
|
||||
expect(config.leader_timeout).toBe(1234)
|
||||
expect(config.keybinds.get("command.palette.show")?.[0]?.key).toBe("alt+p")
|
||||
expect(config.keybinds.get("session.new")?.[0]?.key).toBe("<leader>n")
|
||||
expect(config.keybinds.get("which-key.toggle")?.[0]?.key).toBe("alt+k")
|
||||
expect(config.keybinds.get("which-key.layout.toggle")?.[0]?.key).toBe("ctrl+alt+shift+k")
|
||||
expect(config.keybinds.get("which-key.pending.toggle")?.[0]?.key).toBe("ctrl+alt+shift+p")
|
||||
expect(config.keybinds.get("which-key.group.next")?.[0]?.key).toBe("ctrl+alt+right,ctrl+alt+]")
|
||||
expect((config.keybinds.get("which-key.toggle")?.[0] as { desc?: unknown } | undefined)?.desc).toBe(
|
||||
"Toggle which-key panel",
|
||||
)
|
||||
expect(config.keybinds.get("prompt.editor")?.[0]?.key).toBe("ctrl+e")
|
||||
expect(config.keybinds.get("prompt.autocomplete.next")?.[0]?.key).toBe("ctrl+j")
|
||||
expect(config.keybinds.get("dialog.mcp.toggle")?.[0]?.key).toBe("ctrl+t")
|
||||
expect(config.keybinds.get("model.dialog.favorite")?.[0]?.key).toBe("ctrl+f")
|
||||
expect(config.keybinds.get("dialog.plugins.install")?.[0]?.key).toBe("shift+i")
|
||||
expect(config.keybinds.gather("plugins.dialog", ["dialog.plugins.install"]).map((binding) => binding.cmd)).toEqual([
|
||||
"dialog.plugins.install",
|
||||
])
|
||||
})
|
||||
|
||||
test("keybinds accept OpenTUI binding specs", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
keybinds: {
|
||||
command_list: [{ key: "alt+p", preventDefault: false }],
|
||||
editor_open: { key: { name: "e", ctrl: true }, group: "Explicit" },
|
||||
"prompt.autocomplete.next": false,
|
||||
plugin_manager: "ctrl+shift+p",
|
||||
},
|
||||
}),
|
||||
@@ -483,52 +464,23 @@ test("legacy keybinds transform into semantic keymap sections", async () => {
|
||||
})
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(Object.keys(config.keymap.sections)).toEqual([
|
||||
"global",
|
||||
"which_key",
|
||||
"session",
|
||||
"prompt",
|
||||
"autocomplete",
|
||||
"input",
|
||||
"dialog_select",
|
||||
"dialog_actions",
|
||||
"model",
|
||||
"permission",
|
||||
"question",
|
||||
"plugins",
|
||||
"home_tips",
|
||||
])
|
||||
expect(config.keymap.sections.global.find((binding) => binding.cmd === "command.palette.show")?.key).toBe("alt+p")
|
||||
expect(config.keymap.sections.which_key.find((binding) => binding.cmd === "tui-which-key.toggle")?.key).toBe(
|
||||
"ctrl+alt+k",
|
||||
)
|
||||
expect(config.keymap.sections.prompt.find((binding) => binding.cmd === "prompt.editor")?.key).toBe("ctrl+e")
|
||||
expect(config.keymap.sections.autocomplete.find((binding) => binding.cmd === "prompt.autocomplete.next")?.key).toBe(
|
||||
"ctrl+j",
|
||||
)
|
||||
expect(config.keymap.sections.dialog_actions.find((binding) => binding.cmd === "dialog.action.toggle")?.key).toBe(
|
||||
"ctrl+t",
|
||||
)
|
||||
expect(config.keymap.sections.model.find((binding) => binding.cmd === "model.dialog.provider")?.key).toBe("ctrl+a")
|
||||
expect(config.keymap.sections.model.find((binding) => binding.cmd === "model.dialog.favorite")?.key).toBe("ctrl+f")
|
||||
expect(config.keymap.sections.plugins.find((binding) => binding.cmd === "plugin.dialog.install")?.key).toBe("shift+i")
|
||||
expect(config.keymap.sections.plugins.find((binding) => binding.cmd === "plugins.list")?.key).toBe("ctrl+shift+p")
|
||||
expect(config.keymap.pick("plugins", ["plugin.dialog.install"]).map((binding) => binding.cmd)).toEqual([
|
||||
"plugin.dialog.install",
|
||||
])
|
||||
expect((config.keymap.omit("plugins", ["plugin.dialog.install"])[0] as { group?: unknown } | undefined)?.group).toBe(
|
||||
"Plugins",
|
||||
)
|
||||
expect(config.keymap.omit("plugins", ["plugin.dialog.install"]).map((binding) => binding.cmd)).toEqual([
|
||||
"plugins.list",
|
||||
expect(config.keybinds.get("command.palette.show")).toEqual([
|
||||
{ key: "alt+p", cmd: "command.palette.show", preventDefault: false, desc: "List available commands" },
|
||||
])
|
||||
expect(config.keybinds.get("prompt.editor")?.[0]).toMatchObject({
|
||||
key: { name: "e", ctrl: true },
|
||||
cmd: "prompt.editor",
|
||||
group: "Explicit",
|
||||
})
|
||||
expect(config.keybinds.get("prompt.autocomplete.next")).toEqual([])
|
||||
expect(config.keybinds.get("plugins.list")?.[0]?.key).toBe("ctrl+shift+p")
|
||||
})
|
||||
|
||||
wintest("defaults Ctrl+Z to input undo on Windows", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
expect(config.keybinds.get("terminal.suspend")).toEqual([])
|
||||
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
|
||||
})
|
||||
|
||||
wintest("keeps explicit input undo overrides on Windows", async () => {
|
||||
@@ -538,8 +490,8 @@ wintest("keeps explicit input undo overrides on Windows", async () => {
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+y")
|
||||
expect(config.keybinds.get("terminal.suspend")).toEqual([])
|
||||
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+y")
|
||||
})
|
||||
|
||||
wintest("ignores terminal suspend bindings on Windows", async () => {
|
||||
@@ -550,33 +502,29 @@ wintest("ignores terminal suspend bindings on Windows", async () => {
|
||||
})
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
expect(config.keybinds.get("terminal.suspend")).toEqual([])
|
||||
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
|
||||
})
|
||||
|
||||
test("applies Windows keymap defaults", async () => {
|
||||
test("applies Windows keybind defaults", async () => {
|
||||
await withPlatform("win32", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keymap.sections.global.find((binding) => binding.cmd === "terminal.suspend")).toBeUndefined()
|
||||
expect(config.keymap.sections.input.find((binding) => binding.cmd === "input.undo")?.key).toBe(
|
||||
"ctrl+z,ctrl+-,super+z",
|
||||
)
|
||||
expect(config.keybinds.get("terminal.suspend")).toEqual([])
|
||||
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps explicit configured keymap terminal suspend binding on Windows", async () => {
|
||||
test("ignores explicit keybind terminal suspend binding on Windows", async () => {
|
||||
await withPlatform("win32", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
keymap: {
|
||||
sections: {
|
||||
global: { "terminal.suspend": "alt+z" },
|
||||
},
|
||||
keybinds: {
|
||||
terminal_suspend: "alt+z",
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -584,21 +532,19 @@ test("keeps explicit configured keymap terminal suspend binding on Windows", asy
|
||||
})
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keymap.sections.global.find((binding) => binding.cmd === "terminal.suspend")?.key).toBe("alt+z")
|
||||
expect(config.keybinds.get("terminal.suspend")).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps explicit configured keymap input undo on Windows", async () => {
|
||||
test("keeps explicit configured keybind input undo on Windows", async () => {
|
||||
await withPlatform("win32", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
keymap: {
|
||||
sections: {
|
||||
input: { "input.undo": "ctrl+y" },
|
||||
},
|
||||
keybinds: {
|
||||
input_undo: "ctrl+y",
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -606,7 +552,7 @@ test("keeps explicit configured keymap input undo on Windows", async () => {
|
||||
})
|
||||
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keymap.sections.input.find((binding) => binding.cmd === "input.undo")?.key).toBe("ctrl+y")
|
||||
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+y")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -655,7 +601,7 @@ test("applies env and file substitutions in tui.json", async () => {
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("env-theme")
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
expect(config.keybinds.get("app.exit")?.[0]?.key).toBe("ctrl+q")
|
||||
} finally {
|
||||
if (original === undefined) delete process.env.TUI_THEME_TEST
|
||||
else process.env.TUI_THEME_TEST = original
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { RGBA, type CliRenderer } from "@opentui/core"
|
||||
import type { HostPluginApi } from "../../src/cli/cmd/tui/plugin/slots"
|
||||
import { LegacyKeymapTransform } from "../../src/cli/cmd/tui/config/legacy-keymap-transform"
|
||||
import { ConfigKeybinds } from "../../src/config/keybinds"
|
||||
import { createTuiResolvedKeymap } from "./tui-runtime"
|
||||
import { createTuiResolvedConfig } from "./tui-runtime"
|
||||
|
||||
type Count = {
|
||||
event_add: number
|
||||
@@ -112,11 +110,9 @@ type Opts = {
|
||||
}
|
||||
|
||||
function tuiConfig(input?: Partial<HostPluginApi["tuiConfig"]>): HostPluginApi["tuiConfig"] {
|
||||
const keybinds = ConfigKeybinds.Keybinds.parse(input?.keybinds ?? {})
|
||||
return {
|
||||
...createTuiResolvedConfig(),
|
||||
...input,
|
||||
keybinds,
|
||||
keymap: input?.keymap ?? createTuiResolvedKeymap(LegacyKeymapTransform.create(input?.keybinds ?? {})),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
import { spyOn } from "bun:test"
|
||||
import path from "path"
|
||||
import type { KeyEvent, Renderable } from "@opentui/core"
|
||||
import { resolveBindingSections, type BindingSectionsConfig } from "@opentui/keymap/extras"
|
||||
import { createBindingLookup } from "@opentui/keymap/extras"
|
||||
import { TuiConfig } from "../../src/cli/cmd/tui/config/tui"
|
||||
import { LegacyKeymapTransform } from "../../src/cli/cmd/tui/config/legacy-keymap-transform"
|
||||
import { ConfigKeybinds } from "../../src/config/keybinds"
|
||||
import {
|
||||
KeymapConfig,
|
||||
KeymapSectionNames,
|
||||
keymapBindingDefaults,
|
||||
type KeymapConfigInput,
|
||||
type KeymapSection,
|
||||
} from "../../src/cli/cmd/tui/config/tui-schema"
|
||||
import { TuiKeybind } from "../../src/cli/cmd/tui/config/keybind"
|
||||
|
||||
type PluginSpec = string | [string, Record<string, unknown>]
|
||||
type ResolvedInput = Omit<TuiConfig.Resolved, "keybinds" | "keymap"> & {
|
||||
keybinds?: TuiConfig.Resolved["keybinds"]
|
||||
keymap?: TuiConfig.Resolved["keymap"]
|
||||
type ResolvedInput = Omit<TuiConfig.Resolved, "keybinds" | "leader_timeout"> & {
|
||||
keybinds?: Partial<TuiKeybind.Keybinds>
|
||||
leader_timeout?: number
|
||||
}
|
||||
|
||||
export function createTuiResolvedKeymap(input: KeymapConfigInput): TuiConfig.Resolved["keymap"] {
|
||||
const config = KeymapConfig.parse(input)
|
||||
return {
|
||||
leader: !config.leader || config.leader === "none" ? "ctrl+x" : config.leader,
|
||||
leader_timeout: config.leader_timeout,
|
||||
...resolveBindingSections<Renderable, KeyEvent, BindingSectionsConfig<Renderable, KeyEvent>, KeymapSection>(
|
||||
config.sections,
|
||||
{
|
||||
sections: KeymapSectionNames,
|
||||
bindingDefaults: keymapBindingDefaults,
|
||||
},
|
||||
),
|
||||
}
|
||||
export function createTuiResolvedKeybinds(input: Partial<TuiKeybind.Keybinds> = {}): TuiConfig.Resolved["keybinds"] {
|
||||
const keybinds = TuiKeybind.Keybinds.parse(input)
|
||||
return createBindingLookup(TuiKeybind.toBindingConfig(keybinds), {
|
||||
commandMap: TuiKeybind.CommandMap,
|
||||
bindingDefaults: TuiKeybind.bindingDefaults(),
|
||||
})
|
||||
}
|
||||
|
||||
export function createTuiResolvedConfig(input: ResolvedInput = {}): TuiConfig.Resolved {
|
||||
const keybinds = input.keybinds ?? ConfigKeybinds.Keybinds.parse({})
|
||||
const keybinds = TuiKeybind.Keybinds.parse(input.keybinds ?? {})
|
||||
return {
|
||||
...input,
|
||||
keybinds,
|
||||
keymap: input.keymap ?? createTuiResolvedKeymap(LegacyKeymapTransform.create(input.keybinds ?? {})),
|
||||
keybinds: createTuiResolvedKeybinds(keybinds),
|
||||
leader_timeout: input.leader_timeout ?? 2000,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user