import type { RpcClient } from "@neoinkjs/rpc"; import type { Nvim } from "./index"; import { subscribeNotification } from "./notify"; /** * The RPC method name every input keymap fires via `rpcnotify`, and the method * the host filters for. Single source of truth so the keymap `rhs` and the * subscriber can never drift apart. */ export const NEOINK_INPUT_METHOD = "neoink_input"; /** One forwardable key: its Neovim keymap `lhs` and the `keyspec` sent to the host. */ export interface InputKey { /** The keymap left-hand side, in Neovim `<...>` notation. */ lhs: string; /** The canonical string delivered to the host for this key. */ keyspec: string; } /** * The named (non-printable) keys neoink forwards, keyed by Neovim notation. * * The ``/``/``/`` and `` entries are Task * 7's kitty-analog additions: exactly the class of modified/special key a * naive terminal cannot disambiguate without the kitty keyboard protocol * (shift-modified arrows are frequently indistinguishable from plain arrows * over a legacy escape-sequence terminal; Alt/Meta+letter is ambiguous with a * bare `Esc` keypress immediately followed by that letter). nvim disambiguates * both natively — its keymap engine already treats `` and ``, or * `` and ``+`a`, as distinct keycodes regardless of what protocol * the underlying terminal speaks — so forwarding them here and parsing them * in `@neoinkjs/core`'s `parseKey` (`packages/core/src/input.ts`) is this row's * transformed-parity equivalent of Ink's kitty handling; see that module's * doc for the full rationale. */ const NAMED_KEYS: readonly InputKey[] = [ { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, { lhs: "", keyspec: "" }, ]; /** * The canonical set of named-key keyspec strings this module forwards (the * `keyspec` side of `NAMED_KEYS`, in Neovim `<...>` notation). This is the * single source of truth for "which named keyspecs neoink forwards to the * host" — `@neoinkjs/core`'s `parseKey` (packages/core/src/input.ts) keeps its * own keyspec → `Key` flag map rather than being restructured to read this * list directly, but guards against the two tables drifting apart with a * test that asserts every keyspec in this array parses to a recognized * result. Adding a key here without a matching parse mapping in core fails * that test instead of silently forwarding a key that parses to a dead, * flag-less event. */ export const NEOINK_NAMED_KEYSPECS: readonly string[] = NAMED_KEYS.map((key) => key.keyspec); const PRINTABLE_ASCII_START = 0x20; // space const PRINTABLE_ASCII_END = 0x7e; // tilde const CHAR_LT = "<"; const CHAR_SPACE = " "; /** Neovim keycode notation for a literal `<`, used both in `lhs` and inside a `rhs` string. */ const KEYCODE_LT = ""; /** * The keymap `lhs` for a single printable character. Most printables are their * own literal `lhs`; the two that collide with Neovim keymap notation get their * special forms: `<` becomes `` and space becomes ``. */ function lhsForPrintable(char: string): string { if (char === CHAR_LT) return KEYCODE_LT; if (char === CHAR_SPACE) return ""; return char; } /** Every printable ASCII char, built programmatically rather than hand-listed. */ function printableKeys(): InputKey[] { const keys: InputKey[] = []; for (let code = PRINTABLE_ASCII_START; code <= PRINTABLE_ASCII_END; code += 1) { const char = String.fromCharCode(code); keys.push({ lhs: lhsForPrintable(char), keyspec: char }); } return keys; } /** * The full set of keys neoink forwards to the host: every printable ASCII * character plus the named/navigation/control keys. Ordered printables-first * so the table reads predictably. */ export const NEOINK_INPUT_KEYS: readonly InputKey[] = [...printableKeys(), ...NAMED_KEYS]; /** Options applied to every input keymap. */ const KEYMAP_OPTS: Record = { nowait: true, silent: true, noremap: true }; /** * Escapes a keyspec so it survives being embedded, inside single quotes, in a * keymap `rhs` that Neovim runs through termcode replacement: * - `'` → `''` so it does not close the enclosing VimL single-quoted string; * - `<` → `` so termcode replacement does not treat a keyspec like `` * as an actual keycode instead of the literal characters `< C R >`. */ function escapeKeyspecForRhs(keyspec: string): string { return keyspec.replace(/'/g, "''").split(CHAR_LT).join(KEYCODE_LT); } /** * The keymap `rhs` that fires the input notification: a `` mapping (so it * runs without leaving the current mode or moving the cursor) invoking * `rpcnotify(, 'neoink_input', '')`. */ function buildKeymapRhs(channelId: number, keyspec: string): string { const escaped = escapeKeyspecForRhs(keyspec); return `call rpcnotify(${channelId}, '${NEOINK_INPUT_METHOD}', '${escaped}')`; } /** * Installs the neoink input keymap set on `buffer` (normal mode). Each keymap * fires `rpcnotify(channelId, 'neoink_input', '')`, so real keypresses * in that buffer arrive at the host as `neoink_input` notifications. Because the * mappings are `` mappings the buffer never leaves normal mode, so every * subsequent keypress stays captured too. */ export async function installInputKeymaps( nvim: Nvim, buffer: number, channelId: number, ): Promise { for (const key of NEOINK_INPUT_KEYS) { const rhs = buildKeymapRhs(channelId, key.keyspec); await nvim.setBufKeymap(buffer, "n", key.lhs, rhs, KEYMAP_OPTS); } } /** * Routes `neoink_input` notifications on `client` to `handler` with just the * keyspec string. Returns an unsubscribe function. Task 5 (`useInput`) consumes * this; here it proves keypresses reach the host end to end. */ export function subscribeInput(client: RpcClient, handler: (keyspec: string) => void): () => void { return subscribeNotification( client, NEOINK_INPUT_METHOD, (value): value is string => typeof value === "string", handler, ); }