cloudaxe-opencode/packages/opencode/src/cli/cmd/tui/context/keybind.tsx

106 lines
3.1 KiB
TypeScript
Raw Normal View History

2025-10-31 19:07:36 +00:00
import { createMemo } from "solid-js"
import { Keybind } from "@/util"
2025-10-31 19:07:36 +00:00
import { pipe, mapValues } from "remeda"
2026-04-16 06:03:03 +00:00
import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
2025-10-31 19:07:36 +00:00
import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper"
2026-02-25 22:53:09 +00:00
import { useTuiConfig } from "./tui-config"
export type KeybindKey = keyof NonNullable<TuiConfig.Info["keybinds"]> & string
2025-10-31 19:07:36 +00:00
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
name: "Keybind",
init: () => {
2026-02-25 22:53:09 +00:00
const config = useTuiConfig()
const keybinds = createMemo<Record<string, Keybind.Info[]>>(() => {
2025-10-31 19:07:36 +00:00
return pipe(
2026-02-25 22:53:09 +00:00
(config.keybinds ?? {}) as Record<string, string>,
2025-10-31 19:07:36 +00:00
mapValues((value) => Keybind.parse(value)),
)
})
const [store, setStore] = createStore({
leader: false,
})
const renderer = useRenderer()
let focus: Renderable | null
let timeout: NodeJS.Timeout
function leader(active: boolean) {
if (active) {
setStore("leader", true)
focus = renderer.currentFocusedRenderable
focus?.blur()
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
if (!store.leader) return
leader(false)
2026-01-29 05:17:34 +00:00
if (!focus || focus.isDestroyed) return
focus.focus()
2025-10-31 19:07:36 +00:00
}, 2000)
return
}
if (!active) {
if (focus && !renderer.currentFocusedRenderable) {
focus.focus()
}
setStore("leader", false)
}
}
useKeyboard(async (evt) => {
if (!store.leader && result.match("leader", evt)) {
leader(true)
return
}
if (store.leader && evt.name) {
setImmediate(() => {
if (focus && renderer.currentFocusedRenderable === focus) {
focus.focus()
}
leader(false)
})
}
})
const result = {
get all() {
return keybinds()
},
get leader() {
return store.leader
},
parse(evt: ParsedKey): Keybind.Info {
2025-12-11 18:18:27 +00:00
// Handle special case for Ctrl+Underscore (represented as \x1F)
if (evt.name === "\x1F") {
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
2025-10-31 19:07:36 +00:00
}
2025-12-11 18:18:27 +00:00
return Keybind.fromParsedKey(evt, store.leader)
2025-10-31 19:07:36 +00:00
},
2026-03-27 14:00:26 +00:00
match(key: string, evt: ParsedKey) {
const list = keybinds()[key] ?? Keybind.parse(key)
if (!list.length) return false
2025-10-31 19:07:36 +00:00
const parsed: Keybind.Info = result.parse(evt)
2026-03-27 14:00:26 +00:00
for (const item of list) {
if (Keybind.match(item, parsed)) {
2025-10-31 19:07:36 +00:00
return true
}
}
2026-03-27 14:00:26 +00:00
return false
2025-10-31 19:07:36 +00:00
},
2026-03-27 14:00:26 +00:00
print(key: string) {
const first = keybinds()[key]?.at(0) ?? Keybind.parse(key).at(0)
2025-10-31 19:07:36 +00:00
if (!first) return ""
2026-03-27 14:00:26 +00:00
const text = Keybind.toString(first)
const lead = keybinds().leader?.[0]
if (!lead) return text
return text.replace("<leader>", Keybind.toString(lead))
2025-10-31 19:07:36 +00:00
},
}
return result
},
})