introduce opentui keymap as sole key/cmd engine (#26053)
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
import type { ParsedKey } from "@opentui/core"
|
||||
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
||||
import type { useCommandDialog } from "@tui/component/dialog-command"
|
||||
import type { useEvent } from "@tui/context/event"
|
||||
import type { useKeybind } from "@tui/context/keybind"
|
||||
import type { useRoute } from "@tui/context/route"
|
||||
import type { useSDK } from "@tui/context/sdk"
|
||||
import type { useSync } from "@tui/context/sync"
|
||||
import type { useTheme } from "@tui/context/theme"
|
||||
import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog"
|
||||
import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import { createPluginKeybind } from "../context/plugin-keybinds"
|
||||
import type { useOpencodeKeymap } from "../keymap"
|
||||
import type { useKV } from "../context/kv"
|
||||
import { DialogAlert } from "../ui/dialog-alert"
|
||||
import { DialogConfirm } from "../ui/dialog-confirm"
|
||||
@@ -19,6 +16,7 @@ import { Prompt } from "../component/prompt"
|
||||
import { Slot as HostSlot } from "./slots"
|
||||
import type { useToast } from "../ui/toast"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import * as Keymap from "../keymap"
|
||||
|
||||
type RouteEntry = {
|
||||
key: symbol
|
||||
@@ -28,10 +26,9 @@ type RouteEntry = {
|
||||
export type RouteMap = Map<string, RouteEntry[]>
|
||||
|
||||
type Input = {
|
||||
command: ReturnType<typeof useCommandDialog>
|
||||
tuiConfig: TuiConfig.Info
|
||||
tuiConfig: TuiConfig.Resolved
|
||||
dialog: ReturnType<typeof useDialog>
|
||||
keybind: ReturnType<typeof useKeybind>
|
||||
keymap: ReturnType<typeof useOpencodeKeymap>
|
||||
kv: ReturnType<typeof useKV>
|
||||
route: ReturnType<typeof useRoute>
|
||||
routes: RouteMap
|
||||
@@ -201,20 +198,17 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
||||
return () => {}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
app: appApi(),
|
||||
command: {
|
||||
register(cb) {
|
||||
return input.command.register(() => cb())
|
||||
keys: {
|
||||
formatSequence(parts) {
|
||||
return Keymap.formatKeySequence(parts, input.tuiConfig)
|
||||
},
|
||||
trigger(value) {
|
||||
input.command.trigger(value)
|
||||
},
|
||||
show() {
|
||||
input.command.show()
|
||||
formatBindings(bindings) {
|
||||
return Keymap.formatKeyBindings(bindings, input.tuiConfig)
|
||||
},
|
||||
},
|
||||
keymap: input.keymap,
|
||||
route: {
|
||||
register(list) {
|
||||
return routeRegister(input.routes, list, input.bump)
|
||||
@@ -306,17 +300,6 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
||||
},
|
||||
},
|
||||
},
|
||||
keybind: {
|
||||
match(key, evt: ParsedKey) {
|
||||
return input.keybind.match(key, evt)
|
||||
},
|
||||
print(key) {
|
||||
return input.keybind.print(key)
|
||||
},
|
||||
create(defaults, overrides) {
|
||||
return createPluginKeybind(input.keybind, defaults, overrides)
|
||||
},
|
||||
},
|
||||
get tuiConfig() {
|
||||
return input.tuiConfig
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "@opentui/solid/runtime-plugin-support"
|
||||
import { runtimeModules as keymapRuntimeModules } from "@opentui/keymap/runtime-modules"
|
||||
import { ensureRuntimePluginSupport } from "@opentui/solid/runtime-plugin-support/configure"
|
||||
import {
|
||||
type TuiDispose,
|
||||
type TuiPlugin,
|
||||
@@ -39,6 +40,8 @@ import { setupSlots, Slot as View } from "./slots"
|
||||
import type { HostPluginApi, HostSlots } from "./slots"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
|
||||
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
||||
|
||||
type PluginLoad = {
|
||||
options: ConfigPlugin.Options | undefined
|
||||
spec: string
|
||||
@@ -70,6 +73,36 @@ type PluginEntry = {
|
||||
scope?: PluginScope
|
||||
}
|
||||
|
||||
const ScopedKeymapMethods = new Set<PropertyKey>([
|
||||
"acquireResource",
|
||||
"registerLayer",
|
||||
"registerLayerFields",
|
||||
"prependLayerBindingsTransformer",
|
||||
"appendLayerBindingsTransformer",
|
||||
"prependBindingTransformer",
|
||||
"appendBindingTransformer",
|
||||
"prependBindingParser",
|
||||
"appendBindingParser",
|
||||
"registerToken",
|
||||
"registerSequencePattern",
|
||||
"prependBindingExpander",
|
||||
"appendBindingExpander",
|
||||
"registerBindingFields",
|
||||
"registerCommandFields",
|
||||
"prependCommandTransformer",
|
||||
"appendCommandTransformer",
|
||||
"prependCommandResolver",
|
||||
"appendCommandResolver",
|
||||
"prependLayerAnalyzer",
|
||||
"appendLayerAnalyzer",
|
||||
"intercept",
|
||||
"on",
|
||||
"prependEventMatchResolver",
|
||||
"appendEventMatchResolver",
|
||||
"prependDisambiguationResolver",
|
||||
"appendDisambiguationResolver",
|
||||
])
|
||||
|
||||
type RuntimeState = {
|
||||
directory: string
|
||||
api: Api
|
||||
@@ -104,6 +137,25 @@ function warn(message: string, data: Record<string, unknown>) {
|
||||
console.warn(`[tui.plugin] ${message}`, data)
|
||||
}
|
||||
|
||||
function createScopedKeymap(keymap: TuiPluginApi["keymap"], scope: PluginScope): TuiPluginApi["keymap"] {
|
||||
const cache = new Map<PropertyKey, unknown>()
|
||||
return new Proxy(keymap, {
|
||||
get(target, prop) {
|
||||
const value = Reflect.get(target, prop, target)
|
||||
if (typeof value !== "function") return value
|
||||
if (cache.has(prop)) return cache.get(prop)
|
||||
const fn = ScopedKeymapMethods.has(prop)
|
||||
? (...args: unknown[]) => {
|
||||
const dispose = (value as (...args: unknown[]) => unknown).apply(target, args)
|
||||
return scope.track(typeof dispose === "function" ? (dispose as () => void) : undefined)
|
||||
}
|
||||
: (...args: unknown[]) => (value as (...args: unknown[]) => unknown).apply(target, args)
|
||||
cache.set(prop, fn)
|
||||
return fn
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type CleanupResult = { type: "ok" } | { type: "error"; error: unknown } | { type: "timeout" }
|
||||
|
||||
function runCleanup(fn: () => unknown, ms: number): Promise<CleanupResult> {
|
||||
@@ -327,14 +379,16 @@ function createPluginScope(load: PluginLoad, id: string) {
|
||||
|
||||
const track = (fn: (() => void) | undefined) => {
|
||||
if (!fn) return () => {}
|
||||
const off = onDispose(fn)
|
||||
let drop = false
|
||||
return () => {
|
||||
let off = () => {}
|
||||
const wrapped = () => {
|
||||
if (drop) return
|
||||
drop = true
|
||||
off()
|
||||
fn()
|
||||
}
|
||||
off = onDispose(wrapped)
|
||||
return wrapped
|
||||
}
|
||||
|
||||
const lifecycle: TuiPluginApi["lifecycle"] = {
|
||||
@@ -395,7 +449,7 @@ function readPluginEnabledMap(value: unknown) {
|
||||
)
|
||||
}
|
||||
|
||||
function pluginEnabledState(state: RuntimeState, config: TuiConfig.Info) {
|
||||
function pluginEnabledState(state: RuntimeState, config: TuiConfig.Resolved) {
|
||||
return {
|
||||
...readPluginEnabledMap(config.plugin_enabled),
|
||||
...readPluginEnabledMap(state.api.kv.get(KV_KEY, {})),
|
||||
@@ -484,17 +538,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||
const api = runtime.api
|
||||
const host = runtime.slots
|
||||
const load = plugin.load
|
||||
const command: TuiPluginApi["command"] = {
|
||||
register(cb) {
|
||||
return scope.track(api.command.register(cb))
|
||||
},
|
||||
trigger(value) {
|
||||
api.command.trigger(value)
|
||||
},
|
||||
show() {
|
||||
api.command.show()
|
||||
},
|
||||
}
|
||||
|
||||
const route: TuiPluginApi["route"] = {
|
||||
register(list) {
|
||||
@@ -518,6 +561,8 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||
},
|
||||
}
|
||||
|
||||
const keymap = createScopedKeymap(api.keymap, scope)
|
||||
|
||||
let count = 0
|
||||
|
||||
const slots: TuiPluginApi["slots"] = {
|
||||
@@ -531,10 +576,10 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||
|
||||
return {
|
||||
app: api.app,
|
||||
command,
|
||||
keys: api.keys,
|
||||
keymap,
|
||||
route,
|
||||
ui: api.ui,
|
||||
keybind: api.keybind,
|
||||
tuiConfig: api.tuiConfig,
|
||||
kv: api.kv,
|
||||
state: api.state,
|
||||
@@ -580,7 +625,7 @@ function addPluginEntry(state: RuntimeState, plugin: PluginEntry) {
|
||||
return true
|
||||
}
|
||||
|
||||
function applyInitialPluginEnabledState(state: RuntimeState, config: TuiConfig.Info) {
|
||||
function applyInitialPluginEnabledState(state: RuntimeState, config: TuiConfig.Resolved) {
|
||||
const map = pluginEnabledState(state, config)
|
||||
for (const plugin of state.plugins) {
|
||||
const enabled = map[plugin.id]
|
||||
@@ -923,7 +968,7 @@ let loaded: Promise<void> | undefined
|
||||
let runtime: RuntimeState | undefined
|
||||
export const Slot = View
|
||||
|
||||
export async function init(input: { api: HostPluginApi; config: TuiConfig.Info }) {
|
||||
export async function init(input: { api: HostPluginApi; config: TuiConfig.Resolved }) {
|
||||
const cwd = process.cwd()
|
||||
if (loaded) {
|
||||
if (dir !== cwd) {
|
||||
@@ -972,7 +1017,7 @@ export async function dispose() {
|
||||
}
|
||||
}
|
||||
|
||||
async function load(input: { api: Api; config: TuiConfig.Info }) {
|
||||
async function load(input: { api: Api; config: TuiConfig.Resolved }) {
|
||||
const { api, config } = input
|
||||
const cwd = process.cwd()
|
||||
const slots = setupSlots(api)
|
||||
|
||||
Reference in New Issue
Block a user