Files
opencode/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx
2025-11-01 00:16:06 +00:00

108 lines
2.6 KiB
TypeScript

import { useDialog } from "@tui/ui/dialog"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import {
createContext,
createMemo,
createSignal,
onCleanup,
useContext,
type Accessor,
type ParentProps,
} from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { useKeybind } from "@tui/context/keybind"
import type { KeybindsConfig } from "@opencode-ai/sdk"
type Context = ReturnType<typeof init>
const ctx = createContext<Context>()
export type CommandOption = DialogSelectOption & {
keybind?: keyof KeybindsConfig
}
function init() {
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const dialog = useDialog()
const keybind = useKeybind()
const options = createMemo(() => {
return registrations().flatMap((x) => x())
})
let keybinds = true
useKeyboard((evt) => {
if (!keybinds) return
for (const option of options()) {
if (option.keybind && keybind.match(option.keybind, evt)) {
evt.preventDefault()
option.onSelect?.(dialog)
return
}
}
})
const result = {
trigger(name: string, source?: "prompt") {
for (const option of options()) {
if (option.value === name) {
option.onSelect?.(dialog, source)
return
}
}
},
keybinds(enabled: boolean) {
keybinds = enabled
},
show() {
dialog.replace(() => <DialogCommand options={options()} />)
},
register(cb: () => CommandOption[]) {
const results = createMemo(cb)
setRegistrations((arr) => [results, ...arr])
onCleanup(() => {
setRegistrations((arr) => arr.filter((x) => x !== results))
})
},
get options() {
return options()
},
}
return result
}
export function useCommandDialog() {
const value = useContext(ctx)
if (!value) {
throw new Error("useCommandDialog must be used within a CommandProvider")
}
return value
}
export function CommandProvider(props: ParentProps) {
const value = init()
const dialog = useDialog()
const keybind = useKeybind()
useKeyboard((evt) => {
if (keybind.match("command_list", evt) && dialog.stack.length === 0) {
evt.preventDefault()
dialog.replace(() => <DialogCommand options={value.options} />)
return
}
})
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
function DialogCommand(props: { options: CommandOption[] }) {
const keybind = useKeybind()
return (
<DialogSelect
title="Commands"
options={props.options.map((x) => ({
...x,
footer: x.keybind ? keybind.print(x.keybind) : undefined,
}))}
/>
)
}