Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470import type { KeymapActive, KeymapCommand, KeymapLayer, KeymapPending } from "@opencode/plugin/tui/context"import { InputRenderable, TextareaRenderable, type KeyEvent, type Renderable } from "@opentui/core"import { stringifyKeyStroke, type Binding, type CommandContext } from "@opentui/keymap"import { registerBackspacePopsPendingSequence, registerBaseLayoutFallback, registerCommaBindings, registerEscapeClearsPendingSequence, registerManagedTextareaLayer, registerTimedLeader,} from "@opentui/keymap/addons/opentui"import { formatCommandBindings, formatKeySequence } from "@opentui/keymap/extras"import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"import { useRenderer } from "@opentui/solid"import { createComputed, createContext, createMemo, createSignal, onCleanup, useContext, type Accessor, type ParentProps,} from "solid-js"import { useConfig } from "../config"import { TuiKeybind } from "../config/keybind"import { resolveInteractivity, useInteractivity } from "./interactivity"
declare module "@opentui/keymap" { interface Command { opencode?: KeymapCommand slash?: { name: string aliases?: string[] arguments?: true } }}
const MODE = { key: "opencode.mode", base: "base" } as const
type OpenTuiKeymap = Parameters<typeof KeymapProvider>[0]["keymap"]type Mode = ReturnType<typeof createMode>type KeymapConfig = { readonly keybinds: { get(command: string): readonly Binding<Renderable, KeyEvent>[] } readonly leader?: { readonly timeout: number } readonly leader_timeout?: number}
export const COMMAND_PALETTE_COMMAND = "command.palette.show"
const Context = createContext<{ readonly keymap: OpenTuiKeymap readonly config: KeymapConfig readonly mode: Mode readonly dispatch: (id: string, input?: string) => void readonly input: (id: string) => string | undefined}>()
function Provider(props: ParentProps<{ config?: KeymapConfig }>) { const renderer = useRenderer() const config: KeymapConfig = props.config ?? useConfig().data const keymap = createDefaultOpenTuiKeymap(renderer) const mode = createMode(keymap) let invocation: { readonly id: string; readonly input?: string } | undefined const dispatch = (id: string, input?: string) => { const previous = invocation invocation = { id, input } try { keymap.dispatchCommand(id) } finally { invocation = previous } } const dispose = [ registerCommaBindings(keymap), keymap.appendBindingExpander((context) => { const key = Object.entries({ enter: "return", esc: "escape", pgdown: "pagedown", pgup: "pageup" }).reduce( (result, [alias, value]) => result.replace(new RegExp(`(^|[+,\\s>])${alias}(?=$|[+,\\s<])`, "gi"), `$1${value}`), context.input, ) if (key === context.input) return return [{ key, displays: context.displays }] }), registerBaseLayoutFallback(keymap), registerEscapeClearsPendingSequence(keymap), registerBackspacePopsPendingSequence(keymap), registerManagedTextareaLayer(keymap, renderer, { enabled: () => { const editor = renderer.currentFocusedEditor return editor instanceof TextareaRenderable && !(editor instanceof InputRenderable) }, bindings: [ "input.move.left", "input.move.right", "input.move.up", "input.move.down", "input.select.left", "input.select.right", "input.select.up", "input.select.down", "input.line.home", "input.line.end", "input.select.line.home", "input.select.line.end", "input.visual.line.home", "input.visual.line.end", "input.select.visual.line.home", "input.select.visual.line.end", "input.buffer.home", "input.buffer.end", "input.select.buffer.home", "input.select.buffer.end", "input.delete.line", "input.delete.to.line.end", "input.delete.to.line.start", "input.backspace", "input.delete", "input.newline", "input.undo", "input.redo", "input.word.forward", "input.word.backward", "input.select.word.forward", "input.select.word.backward", "input.delete.word.forward", "input.delete.word.backward", "input.select.all", "input.submit", ].flatMap((command) => config.keybinds.get(command)), }), ] const leader = config.keybinds.get("leader")?.[0]?.key if (leader) { dispose.push( registerTimedLeader(keymap, { trigger: leader, name: "leader", timeoutMs: config.leader?.timeout ?? ("leader_timeout" in config ? config.leader_timeout : undefined) ?? 2000, }), ) } onCleanup(() => { dispose.reverse().forEach((item) => item()) mode.dispose() }) return ( <KeymapProvider keymap={keymap}> <Context.Provider value={{ keymap, config, mode, dispatch, input: (id) => (invocation?.id === id ? invocation.input : undefined), }} > {props.children} </Context.Provider> </KeymapProvider> )}
export type { KeymapCommand, KeymapLayer } from "@opencode/plugin/tui/context"
export interface Keymap { /** Dispatches a reachable command by ID. */ dispatch(id: string, input?: string): void /** Controls mutually exclusive OpenCode input modes. */ readonly mode: { /** Returns the active mode. */ current(): string /** Pushes a mode until the returned cleanup is called. */ push(mode: string): () => void } /** Registers a low-level keymap interceptor. */ intercept: OpenTuiKeymap["intercept"] /** Returns whether an event matches the configured leader key. */ isLeader(event: KeyEvent): boolean}
function use(): Keymap { const value = useValue() const enabled = useInteractivity() const leader = value.config.keybinds.get("leader")?.[0]?.key const isLeader = leader ? value.keymap.createKeyMatcher(leader) : () => false return { dispatch(id, input) { value.dispatch(id, input) }, mode: { current: value.mode.current, push: (mode) => value.mode.push(mode, resolveInteractivity(enabled)), }, intercept: value.keymap.intercept.bind(value.keymap), isLeader, }}
function createLayer(input: () => KeymapLayer) { const value = useValue() const enabled = useInteractivity() useBindings(() => { const layer = input() const { commands, bindings, mode, ...options } = layer const grouped = (commands ?? []).reduce( (result, command) => { if (command.id !== undefined) { if (!command.id) throw new Error("Keymap command IDs cannot be empty") if (typeof command.bind === "string" && !command.bind) throw new Error("Keymap command bindings cannot be empty") result.named.push({ ...command, id: command.id }) return result } if (command.palette) throw new Error("Palette commands require an ID") if (command.slash) throw new Error("Slash commands require an ID") if (typeof command.bind !== "string") throw new Error("Inline keymap commands require bind") if (!command.bind) throw new Error("Keymap command bindings cannot be empty") result.inline.push({ ...command, id: undefined, bind: command.bind }) return result }, { named: [] as Array<KeymapCommand & { readonly id: string }>, inline: [] as Array<KeymapCommand & { readonly id?: undefined; readonly bind: string }>, }, ) return { ...options, enabled: enabled() ? options.enabled : false, ...(mode === "global" ? {} : { mode: mode ?? MODE.base }), commands: grouped.named.map((command) => { const { id, description, group, palette, bind, run, ...definition } = command return { ...definition, name: id, opencode: command, run: (context: CommandContext<Renderable, KeyEvent>) => run(value.input(id), context.event), ...(description === undefined ? {} : { desc: description }), ...(group === undefined ? {} : { category: group }), ...(palette === undefined ? {} : { namespace: "palette" }), } }), bindings: [ ...grouped.inline.map((command) => ({ key: command.bind, cmd: () => { if (command.enabled === false) return false if (typeof command.enabled === "function" && !command.enabled()) return false return command.run() }, ...(command.title === undefined && command.description === undefined ? {} : { desc: command.title ?? command.description }), ...(command.group === undefined ? {} : { group: command.group }), })), ...grouped.named.flatMap((command) => { if (command.bind === false) return [] const configured = value.config.keybinds.get(command.id) if (configured.length) return configured if (typeof command.bind !== "string") return [] return [{ key: command.bind, cmd: command.id }] }), ...(bindings ?? []).flatMap((id) => value.config.keybinds.get(id)), ], } })}
function useShortcuts() { const value = useValue() const shortcuts = useKeymapSelector((keymap) => { const commands = keymap.getCommands({ visibility: "registered" }).map((command) => command.name) const bindings = keymap.getCommandBindings({ visibility: "registered", commands }) return new Map( commands.map((id) => { const current = bindings.get(id) ?? [] return [ id, { first: formatKeySequence(current[0]?.sequence, formatOptions(value.config)), all: formatCommandBindings(current, formatOptions(value.config)), list: current .map((binding) => formatKeySequence(binding.sequence, formatOptions(value.config))) .filter((shortcut): shortcut is string => shortcut !== undefined), }, ] }), ) }) return { get(id: string) { return shortcuts().get(id)?.first }, all(id: string) { return shortcuts().get(id)?.all }, list(id: string) { return shortcuts().get(id)?.list ?? [] }, }}
function useShortcut(id: string) { const shortcuts = useShortcuts() return () => shortcuts.get(id)}
function useLeaderActive() { const pending = usePendingSequence() return () => pending()[0]?.tokenName === "leader"}
function useCommands(): Accessor<readonly KeymapCommand[]> { const value = useValue() return useKeymapSelector((keymap) => keymap .getCommandEntries({ visibility: "reachable", }) .map((entry) => { const command = entry.command.opencode ?? { id: entry.command.name, title: typeof entry.command.title === "string" ? entry.command.title : undefined, description: typeof entry.command.desc === "string" ? entry.command.desc : undefined, group: typeof entry.command.category === "string" ? entry.command.category : undefined, enabled: typeof entry.command.enabled === "boolean" || typeof entry.command.enabled === "function" ? (entry.command.enabled as boolean | (() => boolean)) : undefined, palette: entry.command.namespace === "palette" ? true : undefined, slash: entry.command.slash, suggested: typeof entry.command.suggested === "boolean" || typeof entry.command.suggested === "function" ? (entry.command.suggested as boolean | (() => boolean)) : undefined, } return { ...command, run: (input?: string) => { value.dispatch(entry.command.name, input) }, } }), )}
function usePendingSequence() { useValue() return useKeymapSelector((keymap) => keymap.getPendingSequence())}
function useActiveKeys() { useValue() return useKeymapSelector((keymap) => keymap.getActiveKeys({ includeMetadata: true }))}
function useState() { const value = useValue() const commands = useCommands() const pending = usePendingSequence() const active = useActiveKeys() return { commands, pending: (): readonly KeymapPending[] => pending().map((item) => ({ key: formatKeySequence([item], formatOptions(value.config)) ?? "", ...(item.tokenName ? { token: item.tokenName } : {}), })), active: (): readonly KeymapActive[] => active().map((item) => ({ key: formatKeySequence( [{ stroke: item.stroke, display: item.display, tokenName: item.tokenName }], formatOptions(value.config), ) ?? "", ...(typeof item.commandAttrs?.title === "string" ? { title: item.commandAttrs.title } : {}), ...(typeof item.bindingAttrs?.desc === "string" ? { description: item.bindingAttrs.desc } : typeof item.commandAttrs?.desc === "string" ? { description: item.commandAttrs.desc } : {}), ...(typeof item.commandAttrs?.category === "string" ? { group: item.commandAttrs.category } : typeof item.bindingAttrs?.group === "string" ? { group: item.bindingAttrs.group } : {}), continues: item.continues, })), }}
function useValue() { const value = useContext(Context) if (!value) throw new Error("Keymap.Provider is missing") return value}
export const Keymap = { Provider, use, createLayer, useShortcuts, useShortcut, useLeaderActive, useCommands, usePendingSequence, useActiveKeys, useState,} as const
function createMode(keymap: OpenTuiKeymap) { const [stack, setStack] = createSignal< { readonly id: symbol; readonly mode: string; readonly enabled: Accessor<boolean> }[] >([]) const current = createMemo(() => stack().findLast((item) => item.enabled())?.mode ?? MODE.base) // Publish mode changes before another command can be dispatched in the same callback. createComputed(() => keymap.setData(MODE.key, current())) const unregister = keymap.registerLayerFields({ mode(value, context) { context.require(MODE.key, value) }, }) let disposed = false
return { current, push(mode: string, enabled: Accessor<boolean>) { if (disposed) return () => {} const id = Symbol(mode) // Inactive scopes retain their stack position beneath any newer modes. setStack((items) => [...items, { id, mode, enabled }]) return () => { setStack((items) => items.filter((item) => item.id !== id)) } }, dispose() { if (disposed) return disposed = true setStack([]) unregister() keymap.setData(MODE.key, undefined) }, }}
function formatOptions(config: KeymapConfig) { const leader = config.keybinds.get("leader")?.[0]?.key return { tokenDisplay: { leader: leader ? (typeof leader === "string" ? leader : stringifyKeyStroke(leader)) : TuiKeybind.LeaderDefault, }, keyNameAliases: { up: "↑", down: "↓", left: "←", right: "→", pageup: "pgup", pagedown: "pgdn", delete: "del", }, modifierAliases: { meta: "alt", }, } as const}