import { copyToClipboard, getAgentDir, type ExtensionAPI, type ExtensionContext, } from "@earendil-works/pi-coding-agent"; import type { KeyId } from "@earendil-works/pi-tui"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; /** * Keybinding id used inside ~/.pi/agent/keybindings.json to remap this * shortcut. Set it to a string or an array of strings to bind multiple keys: * * { "pi-toolbox.copyEditor": "ctrl+shift+c" } * { "pi-toolbox.copyEditor": ["alt+c", "ctrl+shift+c"] } * * Why keybindings.json and not an id passed straight to registerShortcut: * pi's registerShortcut() binds a concrete key sequence (e.g. "alt+c"), not a * keybinding id — there is no extension API to register a custom id into pi's * native keybinding table. So instead we read the user's keybindings.json and * bind the key sequence they configured there. The id is ours; the file is pi's. * * Like pi's own KeybindingsManager.create(), this reads only the global * ~/.pi/agent/keybindings.json (not a project-local .pi/keybindings.json). */ const KEYBINDING_ID = "pi-toolbox.copyEditor"; /** * Default hotkey. * * Alt+C is chosen because it is two keys (not three), intuitive (Alt+C = Copy), * and free: pi already binds alt+left, alt+right, alt+b, alt+d, alt+f, * alt+backspace, alt+delete, alt+y, alt+enter, alt+up and alt+v (Windows paste * image), but not alt+c. It works on Windows Terminal, iTerm2, WezTerm, and * Ghostty. */ const DEFAULT_KEY: KeyId = "alt+c"; /** * Read the bound key(s) for this shortcut from the global keybindings.json. * Returns the default when the file is missing, malformed, or leaves the id * unset. Supports both a single string and an array of strings (multiple keys). */ function loadShortcutKeys(): KeyId[] { const path = join(getAgentDir(), "keybindings.json"); if (!existsSync(path)) return [DEFAULT_KEY]; try { const parsed = JSON.parse( readFileSync(path, "utf-8"), ) as Record; const value = parsed[KEYBINDING_ID]; if (typeof value === "string") { const key = value.trim(); return key && !/\s/.test(key) ? [key as KeyId] : [DEFAULT_KEY]; } if (Array.isArray(value) && value.every((v) => typeof v === "string")) { const keys = value .map((v) => v.trim()) .filter((v) => v && !/\s/.test(v)) as KeyId[]; return keys.length > 0 ? keys : [DEFAULT_KEY]; } } catch { // Malformed keybindings.json — fall back to the default. } return [DEFAULT_KEY]; } /** * Copy the entire input editor contents to the system clipboard. * Shared by every bound key sequence. */ async function copyEditorToClipboard(ctx: ExtensionContext): Promise { const text = ctx.ui.getEditorText(); if (text.length === 0) { ctx.ui.notify("Editor is empty — nothing to copy", "warning"); return; } try { await copyToClipboard(text); ctx.ui.notify( `Copied ${text.length} character${text.length === 1 ? "" : "s"} to clipboard`, "info", ); } catch (err) { ctx.ui.notify( `Copy failed: ${err instanceof Error ? err.message : String(err)}`, "error", ); } } export default function (pi: ExtensionAPI) { // Resolve at load time, before setupExtensionShortcuts runs. Editing // keybindings.json followed by /reload re-runs this factory and rebinds. for (const key of loadShortcutKeys()) { pi.registerShortcut(key, { description: "Copy all editor text to clipboard (pi-toolbox)", handler: copyEditorToClipboard, }); } }