Something went wrong. Try again.
Some keybindings for navigation in opencode
Something went wrong. Try again.
3.3 kB · 90 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091/** * opencode-vimnav v2 TUI entrypoint — the `{ id, setup(context) }` plugin * shape the OpenCode v2 TUI host loads from the package's `./tui` export * (or the `src/tui.ts` sibling when configured as a source directory). * * JUST one thing: a global-mode keymap layer of tab-navigation commands * with baked-in defaults — alt+h / alt+l switch tabs (dispatching the * built-ins `session.tab.previous` / `session.tab.next`, which cycle), * alt+shift+h / alt+shift+l move the focused tab (the plugact host * extension, feature-detected at layer construction; without it the move * pair stays palette-dispatchable, unbound, and explains itself with a * toast). Options can override or drop every binding; anything unknown * throws at setup. * * The layer is registered from the `app` claim's render body — keymap * layers are owned by the component whose render registers them (the * documented lifecycle-only pattern) — so it retires with the claim. * Setup returns a cleanup that disposes the claim. */
import { resolveConfig } from "../config/binds.ts";import { buildLayerCommands, type TabActions } from "./commands.ts";import { hasTabMoveBy, type TuiContext } from "./context.ts";
export const PLUGIN_ID = "opencode-vimnav";
/** Short, fixed: surfaced by the move commands when plugact is absent. */export const MOVE_UNAVAILABLE_TOAST = "tab move needs the plugact host extension";
function moveAction(context: TuiContext, direction: 1 | -1): () => void { return () => { // Layer construction already gated the binding on this same check; the // palette path can still reach run, so re-verify rather than crash. const moveBy = context.ui.tabs.moveBy; if (typeof moveBy !== "function") { context.ui.toast.show({ title: "vimnav", message: MOVE_UNAVAILABLE_TOAST, variant: "info" }); return; } // undefined sessionID = the active tab; false (edge, tabs off) is a quiet no-op moveBy.call(context.ui.tabs, undefined, direction); };}
function actions(context: TuiContext): TabActions { return { previous: () => context.keymap.dispatch("session.tab.previous"), next: () => context.keymap.dispatch("session.tab.next"), moveLeft: moveAction(context, -1), moveRight: moveAction(context, 1), };}
export function setup(context: TuiContext): (() => void) | void { return setupWithOptions(context, context.options);}
export function setupWithOptions( context: TuiContext, options: Readonly<Record<string, unknown>>,): (() => void) | void { const config = resolveConfig(options); if (!config.enabled) return;
// Register exactly once per claim generation (render can re-run); the // layer dies with the claim's component, and disposing the claim is the // setup cleanup. let registered = false; const dispose = context.ui.slot({ append: "app", render: () => { if (registered) return null; registered = true; context.keymap.layer(() => ({ // global: active everywhere, including while typing in the prompt mode: "global", commands: buildLayerCommands(config.binds, hasTabMoveBy(context.ui.tabs), actions(context)), })); return null; }, });
return dispose;}
const plugin = { id: PLUGIN_ID, setup,};
export default plugin;