import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { loadBashResolvedInputrc } from "./inputrc/bash-provider.js"; import { loadPortableInputrc } from "./inputrc/portable-provider.js"; import { loadBundledReadlineProfile } from "./inputrc/profile-loader.js"; import { ReadlineProfileResolver } from "./inputrc/profile-resolver.js"; import { resolveUserInputrcPath } from "./inputrc/user-inputrc.js"; import { ReadlineViEditor } from "./readline-vi-editor.js"; const DEFAULT_PROFILE = "bash-5-readline-8"; /** Install the modal editor and its resolved default inputrc profile in TUI mode. */ export default function registerReadlineViExtension(pi: ExtensionAPI): void { let activeEditor: ReadlineViEditor | undefined; let reload: (() => Promise) | undefined; let source = "bundled bash-5-readline-8"; pi.registerCommand("readline-vi-reload", { description: "Reload $INPUTRC or ~/.inputrc while retaining last-known-good bindings", handler: async (_args, ctx) => { if (reload === undefined) { ctx.ui.notify("pi-vi is not active in this session", "warning"); return; } source = await reload(); ctx.ui.notify(`pi-vi bindings reloaded from ${source}`, "info"); }, }); pi.registerCommand("readline-vi-diagnostics", { description: "Show the active Readline profile without prompt or environment data", handler: (args, ctx) => { const binding = args.length === 0 || activeEditor === undefined ? "" : `; binding=${activeEditor.describeInputrcBinding(args)}`; const unsupported = activeEditor?.unsupportedReadlineVariables() ?? []; const variables = unsupported.length === 0 ? "" : `; unsupported-variables=${unsupported.join(",")}`; ctx.ui.notify( `pi-vi: profile=${DEFAULT_PROFILE}; source=${source}; bind-x=disabled${binding}${variables}`, "info", ); return Promise.resolve(); }, }); pi.on("session_start", async (_event, ctx) => { if (ctx.mode !== "tui") return; const loadResolver = async (): Promise<{ resolver: ReadlineProfileResolver; source: string; }> => { const bundled = await loadBundledReadlineProfile(DEFAULT_PROFILE); const inputrcPath = await resolveUserInputrcPath(); if (inputrcPath === undefined) return { resolver: new ReadlineProfileResolver(bundled), source: `bundled ${DEFAULT_PROFILE}`, }; try { const profile = await loadBashResolvedInputrc(bundled, inputrcPath); return { resolver: new ReadlineProfileResolver(profile), source: "user inputrc (Bash-resolved)", }; } catch (bashError) { try { const profile = await loadPortableInputrc(bundled, inputrcPath); ctx.ui.notify( "pi-vi loaded inputrc with the portable fallback", "warning", ); return { resolver: new ReadlineProfileResolver(profile), source: "user inputrc (portable fallback)", }; } catch (portableError) { const bashReason = bashError instanceof Error ? bashError.message : "unknown error"; const portableReason = portableError instanceof Error ? portableError.message : "unknown error"; ctx.ui.notify( `pi-vi could not load user inputrc; using bundled bindings (Bash: ${bashReason}; portable: ${portableReason})`, "warning", ); return { resolver: new ReadlineProfileResolver(bundled), source: `bundled ${DEFAULT_PROFILE} (inputrc failed)`, }; } } }; const initial = await loadResolver(); source = initial.source; let resolver = initial.resolver; reload = async () => { const loaded = await loadResolver(); resolver = loaded.resolver; activeEditor?.setProfileResolver(resolver); return loaded.source; }; ctx.ui.setEditorComponent((tui, theme, keybindings) => { const editor = new ReadlineViEditor(tui, theme, keybindings); editor.setModeThemeProvider(() => ctx.ui.theme); editor.setProfileResolver(resolver); activeEditor = editor; return editor; }); }); }