|
@@ -7,24 +7,100 @@ import {
|
|
|
} from "@opentui/keymap/extras"
|
|
} from "@opentui/keymap/extras"
|
|
|
import {
|
|
import {
|
|
|
KeymapProvider,
|
|
KeymapProvider,
|
|
|
- reactiveMatcherFromSignal,
|
|
|
|
|
useKeymap,
|
|
useKeymap,
|
|
|
useKeymapSelector,
|
|
useKeymapSelector,
|
|
|
useBindings,
|
|
useBindings,
|
|
|
} from "@opentui/keymap/solid"
|
|
} from "@opentui/keymap/solid"
|
|
|
-import type { Accessor } from "solid-js"
|
|
|
|
|
|
|
+import { createMemo, type Accessor } from "solid-js"
|
|
|
import type { TuiConfig } from "./config/tui"
|
|
import type { TuiConfig } from "./config/tui"
|
|
|
import { useTuiConfig } from "./context/tui-config"
|
|
import { useTuiConfig } from "./context/tui-config"
|
|
|
import { TuiKeybind } from "./config/keybind"
|
|
import { TuiKeybind } from "./config/keybind"
|
|
|
|
|
|
|
|
export const LEADER_TOKEN = "leader"
|
|
export const LEADER_TOKEN = "leader"
|
|
|
|
|
+export const OPENCODE_BASE_MODE = "base"
|
|
|
|
|
+export const COMMAND_PALETTE_COMMAND = "command.palette.show"
|
|
|
|
|
+
|
|
|
|
|
+const OPENCODE_MODE_KEY = "opencode.mode"
|
|
|
|
|
|
|
|
export const OpencodeKeymapProvider = KeymapProvider
|
|
export const OpencodeKeymapProvider = KeymapProvider
|
|
|
export const useOpencodeKeymap = useKeymap
|
|
export const useOpencodeKeymap = useKeymap
|
|
|
|
|
|
|
|
-export { reactiveMatcherFromSignal, useBindings, useKeymapSelector }
|
|
|
|
|
|
|
+export { useBindings, useKeymapSelector }
|
|
|
|
|
|
|
|
export type OpenTuiKeymap = ReturnType<typeof useKeymap>
|
|
export type OpenTuiKeymap = ReturnType<typeof useKeymap>
|
|
|
|
|
+type OpencodeModeStack = ReturnType<typeof createOpencodeModeStack>
|
|
|
|
|
+type CommandSlashEntry = {
|
|
|
|
|
+ display: string
|
|
|
|
|
+ description?: string
|
|
|
|
|
+ aliases?: string[]
|
|
|
|
|
+ onSelect: () => void
|
|
|
|
|
+}
|
|
|
|
|
+type Command = ReturnType<OpenTuiKeymap["getCommands"]>[number]
|
|
|
|
|
+
|
|
|
|
|
+const modeStacks = new WeakMap<OpenTuiKeymap, OpencodeModeStack>()
|
|
|
|
|
+
|
|
|
|
|
+function isVisiblePaletteCommand(command: Command) {
|
|
|
|
|
+ return command.hidden !== true && command.name !== COMMAND_PALETTE_COMMAND
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function createOpencodeModeStack(keymap: OpenTuiKeymap) {
|
|
|
|
|
+ keymap.setData(OPENCODE_MODE_KEY, OPENCODE_BASE_MODE)
|
|
|
|
|
+
|
|
|
|
|
+ const offFields = keymap.registerLayerFields({
|
|
|
|
|
+ mode(value, ctx) {
|
|
|
|
|
+ ctx.require(OPENCODE_MODE_KEY, value)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const stack: { id: symbol; mode: string }[] = []
|
|
|
|
|
+ let disposed = false
|
|
|
|
|
+
|
|
|
|
|
+ const update = () => {
|
|
|
|
|
+ keymap.setData(OPENCODE_MODE_KEY, stack.at(-1)?.mode ?? OPENCODE_BASE_MODE)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const stackApi = {
|
|
|
|
|
+ current() {
|
|
|
|
|
+ return stack.at(-1)?.mode ?? OPENCODE_BASE_MODE
|
|
|
|
|
+ },
|
|
|
|
|
+ push(mode: string) {
|
|
|
|
|
+ if (disposed) return () => {}
|
|
|
|
|
+ const id = Symbol(mode)
|
|
|
|
|
+ let active = true
|
|
|
|
|
+ stack.push({ id, mode })
|
|
|
|
|
+ update()
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ if (!active) return
|
|
|
|
|
+ active = false
|
|
|
|
|
+ const index = stack.findIndex((item) => item.id === id)
|
|
|
|
|
+ if (index !== -1) stack.splice(index, 1)
|
|
|
|
|
+ update()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ dispose() {
|
|
|
|
|
+ if (disposed) return
|
|
|
|
|
+ disposed = true
|
|
|
|
|
+ stack.length = 0
|
|
|
|
|
+ offFields()
|
|
|
|
|
+ keymap.setData(OPENCODE_MODE_KEY, undefined)
|
|
|
|
|
+ modeStacks.delete(keymap)
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ modeStacks.set(keymap, stackApi)
|
|
|
|
|
+ return stackApi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function useOpencodeModeStack() {
|
|
|
|
|
+ return getOpencodeModeStack(useOpencodeKeymap())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function getOpencodeModeStack(keymap: OpenTuiKeymap) {
|
|
|
|
|
+ const value = modeStacks.get(keymap)
|
|
|
|
|
+ if (!value) throw new Error("Opencode mode stack is not registered for this keymap")
|
|
|
|
|
+ return value
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
const KEY_ALIASES = {
|
|
const KEY_ALIASES = {
|
|
|
enter: "return",
|
|
enter: "return",
|
|
@@ -127,6 +203,7 @@ export function registerOpencodeKeymap(
|
|
|
renderer: CliRenderer,
|
|
renderer: CliRenderer,
|
|
|
config: Pick<TuiConfig.Resolved, "keybinds" | "leader_timeout">,
|
|
config: Pick<TuiConfig.Resolved, "keybinds" | "leader_timeout">,
|
|
|
) {
|
|
) {
|
|
|
|
|
+ const modeStack = createOpencodeModeStack(keymap)
|
|
|
const offCommaBindings = addons.registerCommaBindings(keymap)
|
|
const offCommaBindings = addons.registerCommaBindings(keymap)
|
|
|
const offAliasExpander = registerKeyAliases(keymap)
|
|
const offAliasExpander = registerKeyAliases(keymap)
|
|
|
const offBaseLayout = addons.registerBaseLayoutFallback(keymap)
|
|
const offBaseLayout = addons.registerBaseLayoutFallback(keymap)
|
|
@@ -150,6 +227,7 @@ export function registerOpencodeKeymap(
|
|
|
offAliasExpander()
|
|
offAliasExpander()
|
|
|
offBaseLayout()
|
|
offBaseLayout()
|
|
|
offCommaBindings()
|
|
offCommaBindings()
|
|
|
|
|
+ modeStack.dispose()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -166,3 +244,36 @@ export function useCommandShortcut(command: string): Accessor<string> {
|
|
|
export function useLeaderActive(): Accessor<boolean> {
|
|
export function useLeaderActive(): Accessor<boolean> {
|
|
|
return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN)
|
|
return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+export function useCommandSlashes(): Accessor<readonly CommandSlashEntry[]> {
|
|
|
|
|
+ const keymap = useOpencodeKeymap()
|
|
|
|
|
+ const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
|
|
|
|
|
+ keymap
|
|
|
|
|
+ .getCommandEntries({
|
|
|
|
|
+ visibility: "reachable",
|
|
|
|
|
+ namespace: "palette",
|
|
|
|
|
+ filter: isVisiblePaletteCommand,
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return createMemo<CommandSlashEntry[]>(() =>
|
|
|
|
|
+ entries().flatMap((entry) => {
|
|
|
|
|
+ const slashName = entry.command.slashName
|
|
|
|
|
+ if (typeof slashName !== "string" || !slashName) return []
|
|
|
|
|
+ const slashAliases = entry.command.slashAliases
|
|
|
|
|
+ return {
|
|
|
|
|
+ display: `/${slashName}`,
|
|
|
|
|
+ description:
|
|
|
|
|
+ typeof entry.command.desc === "string"
|
|
|
|
|
+ ? entry.command.desc
|
|
|
|
|
+ : typeof entry.command.title === "string"
|
|
|
|
|
+ ? entry.command.title
|
|
|
|
|
+ : undefined,
|
|
|
|
|
+ aliases: Array.isArray(slashAliases)
|
|
|
|
|
+ ? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`)
|
|
|
|
|
+ : undefined,
|
|
|
|
|
+ onSelect: () => keymap.dispatchCommand(entry.command.name),
|
|
|
|
|
+ }
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+}
|