@termless/vt100-rust 0.9.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Rust vt100 backend for termless -- wraps the [vt100](https://crates.io/crates/vt100) Rust crate via napi-rs.
4
4
 
5
+ > Install from npm; a git install resolves the TypeScript source and runs only under Bun.
6
+
5
7
  ## Build
6
8
 
7
9
  Requires Rust toolchain:
package/dist/index.d.mts CHANGED
@@ -108,6 +108,7 @@ interface KeyDescriptor {
108
108
  }
109
109
  interface TerminalCapabilities {
110
110
  name: string;
111
+ /** `capabilities.version` is the upstream engine release pinned by `upstreamVersion` in `backends.json` for the default install (`"not-applicable"` without an upstream), not the wrapper release or proof of execution; non-default installs are outside this claim and a run's `runtimeIdentity` supplies execution evidence. */
111
112
  version: string;
112
113
  truecolor: boolean;
113
114
  kittyKeyboard: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/io/picture.ts","../../../src/terminal/types.ts","../src/backend.ts","../src/index.ts"],"mappings":";UAwBiB;EACf;EACA,IAAI;EACJ,IAAI;EACJ;EACA;EACA;EACA,WAAW;EACX,gBAAgB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;KAUU;;;;;;;KAQA;EAAU;EAAW;EAAW;EAAW;;UAItC;;EAEf;;EAEA;;EAEA;;EAEA,OAAO;;EAEP;;EAEA;;KAGU;;;;;;;KAUA;;;UC/EK;EACf;EACA;EACA;;;;;;;KAmBU,eAAe;UAIV;;;;;;;;;EASf;;;;;EAKA;;;;;;EAMA;;EAEA;;EAEA;;EAEA;;UAKe;EACf;EACA;EACA;EACA;EACA;;UAKe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YAAY;;;;;;;;;;;;;;;;;;;;UA6EG;;EAEf;;;;;;EAMA,aAAa,kBAAkB,kBAAkB,gBAAgB;;EAEjE,QAAQ,aAAa,cAAc;;EAEnC,OAAO,cAAc;;EAErB,WAAW;;EAEX,QAAQ,cAAc;;EAEtB,YAAY;EACZ,aAAa;EACb,QAAQ,MAAM;EACd;EACA,iBAAiB;;;;;;;;;;;;;UAkBF;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;;;;;;;;;EAUR;;;;;;;;;;;;;UAce,wBAAwB;WAC9B;EAGT,KAAK,MAAM;EACX;EAGA,KAAK,MAAM;EACX,OAAO,cAAc;EACrB;EAGA,UAAU,KAAK,gBAAgB;;;;;;;EAQ/B,cAAc,MAAM;EAGpB,eAAe;WAGN,cAAc;;;;;;;;;;;EAYvB,YAAY,OAAO,oBAAoB,QAAQ;;;;UCzPvC;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGQ;EACR;EACA;EACA;EACA;;UAGQ;EACR;EACA;EACA;;UAGQ;EACR,KAAK,MAAM;EACX,OAAO,cAAc;EACrB;EACA;EACA,aAAa,kBAAkB,kBAAkB,gBAAgB;EACjE,QAAQ,aAAa,cAAc;EACnC,QAAQ,cAAc;EACtB,aAAa;EACb,QAAQ;EACR;EACA,iBAAiB;EACjB,eAAe;;EAEf,iBAAiB;;UAGT;EACR,wBAAwB,cAAc,cAAc,6BAA6B;;;;;;iBAUnE,uBAAuB;;;;;;;;;;iBAkFvB,uBAAuB,OAAO,QAAQ,kBAAkB,SAAS,eAAe;;;;iBCtK1E,QAAQ,OAAO,QAAQ,mBAAmB,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/io/picture.ts","../../../src/terminal/types.ts","../src/backend.ts","../src/index.ts"],"mappings":";UAwBiB;EACf;EACA,IAAI;EACJ,IAAI;EACJ;EACA;EACA;EACA,WAAW;EACX,gBAAgB;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;KAUU;;;;;;;KAQA;EAAU;EAAW;EAAW;EAAW;;UAItC;;EAEf;;EAEA;;EAEA;;EAEA,OAAO;;EAEP;;EAEA;;KAGU;;;;;;;KAUA;;;UC/EK;EACf;EACA;EACA;;;;;;;KAmBU,eAAe;UAIV;;;;;;;;;EASf;;;;;EAKA;;;;;;EAMA;;EAEA;;EAEA;;EAEA;;UAKe;EACf;EACA;EACA;EACA;EACA;;UAKe;EACf;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YAAY;;;;;;;;;;;;;;;;;;;;UA6EG;;EAEf;;;;;;EAMA,aAAa,kBAAkB,kBAAkB,gBAAgB;;EAEjE,QAAQ,aAAa,cAAc;;EAEnC,OAAO,cAAc;;EAErB,WAAW;;EAEX,QAAQ,cAAc;;EAEtB,YAAY;EACZ,aAAa;EACb,QAAQ,MAAM;EACd;EACA,iBAAiB;;;;;;;;;;;;;UAkBF;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;;;;;;;;;EAUR;;;;;;;;;;;;;UAce,wBAAwB;WAC9B;EAGT,KAAK,MAAM;EACX;EAGA,KAAK,MAAM;EACX,OAAO,cAAc;EACrB;EAGA,UAAU,KAAK,gBAAgB;;;;;;;EAQ/B,cAAc,MAAM;EAGpB,eAAe;WAGN,cAAc;;;;;;;;;;;EAYvB,YAAY,OAAO,oBAAoB,QAAQ;;;;UC7PvC;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGQ;EACR;EACA;EACA;EACA;;UAGQ;EACR;EACA;EACA;;UAGQ;EACR,KAAK,MAAM;EACX,OAAO,cAAc;EACrB;EACA;EACA,aAAa,kBAAkB,kBAAkB,gBAAgB;EACjE,QAAQ,aAAa,cAAc;EACnC,QAAQ,cAAc;EACtB,aAAa;EACb,QAAQ;EACR;EACA,iBAAiB;EACjB,eAAe;;EAEf,iBAAiB;;UAGT;EACR,wBAAwB,cAAc,cAAc,6BAA6B;;;;;;iBAUnE,uBAAuB;;;;;;;;;;iBA+EvB,uBAAuB,OAAO,QAAQ,kBAAkB,SAAS,eAAe;;;;iBChK1E,QAAQ,OAAO,QAAQ,mBAAmB,QAAQ"}
package/dist/index.mjs CHANGED
@@ -1,7 +1,4 @@
1
1
  import { createRequire } from "node:module";
2
- //#region \0rolldown/runtime.js
3
- var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
4
- //#endregion
5
2
  //#region ../../src/terminal/key-mapping.ts
6
3
  const KEY_MAP = {
7
4
  ArrowUp: "\x1B[A",
@@ -69,9 +66,7 @@ function parseKey(key) {
69
66
  case "alt":
70
67
  result.alt = true;
71
68
  break;
72
- case "super":
73
- result.super = true;
74
- break;
69
+ case "super": result.super = true;
75
70
  }
76
71
  return result;
77
72
  }
@@ -140,6 +135,18 @@ function encodeKeyToAnsi(key) {
140
135
  const ansi = keyToAnsi(key);
141
136
  return encoder.encode(ansi);
142
137
  }
138
+ createRequire(import.meta.url);
139
+ /** Load a native addon from caller-relative candidates without static `.node` imports. */
140
+ function loadNativeAddon(options) {
141
+ const requireFromCaller = createRequire(options.callerUrl);
142
+ const errors = [];
143
+ for (const candidate of options.candidates) try {
144
+ return requireFromCaller(candidate);
145
+ } catch (error) {
146
+ errors.push(` - ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
147
+ }
148
+ throw new Error(`Failed to load ${options.packageName} native module on ${process.platform}/${process.arch}.\nPrebuilt scope: ${options.prebuiltScope}\nTried:\n${options.candidates.map((candidate) => ` - ${candidate}`).join("\n")}\nLoad attempts:\n${errors.join("\n")}`);
149
+ }
143
150
  //#endregion
144
151
  //#region src/backend.ts
145
152
  let nativeModule = null;
@@ -152,14 +159,15 @@ function loadVt100RustNative() {
152
159
  if (nativeModule) return nativeModule;
153
160
  if (loadError) throw loadError;
154
161
  try {
155
- const mod = __require("../termless-vt100-rust.node");
156
- nativeModule = mod;
157
- return mod;
158
- } catch (e) {
159
- loadError = /* @__PURE__ */ new Error(`Failed to load vt100-rust native module. Build it first:
160
- cd packages/vt100-rust/native && cargo build --release
161
- cp target/release/libtermless_vt100_rust_native.dylib ../termless-vt100-rust.node
162
- \nOriginal error: ${e instanceof Error ? e.message : String(e)}`);
162
+ nativeModule = loadNativeAddon({
163
+ callerUrl: import.meta.url,
164
+ packageName: "@termless/vt100-rust",
165
+ candidates: ["../termless-vt100-rust.node"],
166
+ prebuiltScope: "linux-x64-glibc only"
167
+ });
168
+ return nativeModule;
169
+ } catch (error) {
170
+ loadError = error;
163
171
  throw loadError;
164
172
  }
165
173
  }
@@ -342,7 +350,7 @@ function createVt100RustBackend(opts, native) {
342
350
  encodeKey: encodeKeyToAnsi,
343
351
  capabilities: {
344
352
  name: "vt100-rust",
345
- version: "0.1.0",
353
+ version: "0.16.2",
346
354
  truecolor: true,
347
355
  kittyKeyboard: false,
348
356
  kittyGraphics: false,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/terminal/key-mapping.ts","../../../src/terminal/key-encoding.ts","../src/backend.ts","../src/index.ts"],"sourcesContent":["/**\n * Key parsing and ANSI encoding for termless.\n *\n * Converts human-readable key descriptions (e.g., \"Ctrl+a\", \"ArrowUp\", \"F5\")\n * into KeyDescriptor objects and ANSI escape sequences suitable for PTY input.\n */\n\nimport type { KeyDescriptor } from \"./types.ts\"\n\n// ── Named key → ANSI sequence map ──\n\nconst KEY_MAP: Record<string, string | null> = {\n ArrowUp: \"\\x1b[A\",\n ArrowDown: \"\\x1b[B\",\n ArrowLeft: \"\\x1b[D\",\n ArrowRight: \"\\x1b[C\",\n Home: \"\\x1b[H\",\n End: \"\\x1b[F\",\n PageUp: \"\\x1b[5~\",\n PageDown: \"\\x1b[6~\",\n Enter: \"\\r\",\n Tab: \"\\t\",\n Backspace: \"\\x7f\",\n Delete: \"\\x1b[3~\",\n Escape: \"\\x1b\",\n Space: \" \",\n // Pure modifier keys produce no output on their own\n Control: null,\n Shift: null,\n Alt: null,\n Meta: null,\n}\n\n// ── Function keys F1-F12 ──\n\nconst FKEY_MAP: Record<string, string> = {\n F1: \"\\x1bOP\",\n F2: \"\\x1bOQ\",\n F3: \"\\x1bOR\",\n F4: \"\\x1bOS\",\n F5: \"\\x1b[15~\",\n F6: \"\\x1b[17~\",\n F7: \"\\x1b[18~\",\n F8: \"\\x1b[19~\",\n F9: \"\\x1b[20~\",\n F10: \"\\x1b[21~\",\n F11: \"\\x1b[23~\",\n F12: \"\\x1b[24~\",\n}\n\n// ── Modifier aliases ──\n\nconst MODIFIER_ALIASES: Record<string, string> = {\n ctrl: \"ctrl\",\n control: \"ctrl\",\n shift: \"shift\",\n alt: \"alt\",\n meta: \"super\",\n cmd: \"super\",\n option: \"alt\",\n super: \"super\",\n}\n\nfunction normalizeModifier(mod: string): string {\n return MODIFIER_ALIASES[mod.toLowerCase()] ?? mod.toLowerCase()\n}\n\n/**\n * Parse a key string like \"Ctrl+a\", \"ArrowUp\", \"Shift+Tab\" into a KeyDescriptor.\n *\n * Supports modifier prefixes: Ctrl, Control, Alt, Shift, Meta, Cmd, Option, Super.\n * Modifiers are case-insensitive and separated by \"+\".\n */\nexport function parseKey(key: string): KeyDescriptor {\n const parts = key.split(\"+\")\n const mainKey = parts.pop()!\n const result: KeyDescriptor = { key: mainKey }\n\n for (const part of parts) {\n const normalized = normalizeModifier(part)\n switch (normalized) {\n case \"ctrl\":\n result.ctrl = true\n break\n case \"shift\":\n result.shift = true\n break\n case \"alt\":\n result.alt = true\n break\n case \"super\":\n result.super = true\n break\n }\n }\n\n return result\n}\n\n/**\n * Convert a key descriptor or key string to its ANSI escape sequence.\n *\n * Handles:\n * - Single characters: returned as-is\n * - Named keys (ArrowUp, Enter, Tab, etc.): mapped to standard ANSI sequences\n * - Function keys (F1-F12): mapped to VT220/xterm sequences\n * - Ctrl+letter: ASCII control codes 1-26\n * - Ctrl+Enter: newline (\\n)\n * - Alt+key: ESC prefix + key\n */\nexport function keyToAnsi(key: KeyDescriptor | string): string {\n const desc = typeof key === \"string\" ? parseKey(key) : key\n const { key: mainKey, ctrl, alt, shift } = desc\n const hasSuperOrMeta = desc.super\n\n // Ctrl+letter -> control code (ASCII 1-26)\n if (ctrl && mainKey.length === 1) {\n const code = mainKey.toLowerCase().charCodeAt(0) - 96\n if (code >= 1 && code <= 26) return String.fromCharCode(code)\n }\n\n // Ctrl+Enter -> \\n (legacy terminal convention: \\r = Enter, \\n = Ctrl+Enter/Ctrl+J)\n if (ctrl && mainKey === \"Enter\") {\n return \"\\n\"\n }\n\n // Alt (or Super/Meta) + single character -> ESC prefix\n if ((alt || hasSuperOrMeta) && mainKey.length === 1) {\n return `\\x1b${mainKey}`\n }\n\n // Function keys\n const fkey = FKEY_MAP[mainKey]\n if (fkey !== undefined) {\n // With modifiers, use CSI sequences with modifier parameter\n if (ctrl || alt || shift || hasSuperOrMeta) {\n const mod = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0) + (hasSuperOrMeta ? 8 : 0)\n // F1-F4 use SS3 format, convert to CSI for modified keys\n const fkeyNum = parseInt(mainKey.slice(1))\n if (fkeyNum <= 4) {\n // F1=11, F2=12, F3=13, F4=14 in CSI ~ format\n const csiNum = fkeyNum + 10\n return `\\x1b[${csiNum};${mod}~`\n }\n // F5-F12 already use CSI ~ format: extract the number\n const match = fkey.match(/\\x1b\\[(\\d+)~/)\n if (match) {\n return `\\x1b[${match[1]};${mod}~`\n }\n }\n return fkey\n }\n\n // Named keys from KEY_MAP\n const mapped = KEY_MAP[mainKey]\n if (mapped !== undefined) {\n if (mapped === null) return \"\" // Pure modifier keys produce nothing\n\n // Arrow/navigation keys with modifiers use CSI 1;mod format\n if ((ctrl || alt || shift || hasSuperOrMeta) && mapped.startsWith(\"\\x1b[\")) {\n const mod = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0) + (hasSuperOrMeta ? 8 : 0)\n // CSI letter format (e.g., \\x1b[A) -> CSI 1;mod letter\n const letterMatch = mapped.match(/\\x1b\\[([A-H])$/)\n if (letterMatch) {\n return `\\x1b[1;${mod}${letterMatch[1]}`\n }\n // CSI num ~ format (e.g., \\x1b[5~) -> CSI num;mod ~\n const tildeMatch = mapped.match(/\\x1b\\[(\\d+)~$/)\n if (tildeMatch) {\n return `\\x1b[${tildeMatch[1]};${mod}~`\n }\n }\n\n // Shift+Tab -> reverse tab (CSI Z)\n if (shift && mainKey === \"Tab\") {\n return \"\\x1b[Z\"\n }\n\n return mapped\n }\n\n // Single character without modifiers\n if (mainKey.length === 1) {\n // Shift+letter -> uppercase (passthrough for terminals)\n if (shift && mainKey.match(/[a-z]/)) {\n return mainKey.toUpperCase()\n }\n return mainKey\n }\n\n // Unknown key: return as-is\n return mainKey\n}\n","/**\n * Shared ANSI key encoding for terminal backends.\n *\n * All backends need to convert KeyDescriptor → Uint8Array for PTY input.\n * This module delegates to keyToAnsi() as the single source of truth for\n * ANSI key encoding, converting the string result to bytes.\n */\n\nimport type { KeyDescriptor } from \"./types.ts\"\nimport { keyToAnsi } from \"./key-mapping.ts\"\n\n// Module-level TextEncoder for efficiency (avoids per-call allocation)\nconst encoder = new TextEncoder()\n\n/**\n * Encode a KeyDescriptor to an ANSI byte sequence for PTY input.\n *\n * Delegates to keyToAnsi() for the actual encoding, then converts to bytes.\n * This is the canonical encoder — backends that don't have their own\n * protocol (e.g., kitty keyboard) should use this.\n */\nexport function encodeKeyToAnsi(key: KeyDescriptor): Uint8Array {\n const ansi = keyToAnsi(key)\n return encoder.encode(ansi)\n}\n","/**\n * Rust vt100 backend for termless.\n *\n * Wraps the doy/vt100-rust crate via napi-rs native bindings to implement\n * the TerminalBackend interface -- same VT parser used by many Rust terminal\n * projects, but headless via native Node addon.\n *\n * Requires the native module to be built first:\n * cd packages/vt100-rust/native && cargo build --release\n * cp target/release/libtermless_vt100_rust_native.dylib ../termless-vt100-rust.node\n *\n * TODO: Set up @napi-rs/cli build pipeline for cross-platform prebuilds.\n */\n\nimport type {\n TerminalBackend,\n TerminalOptions,\n Cell,\n Cursor,\n TerminalMode,\n ScrollbackState,\n TerminalCapabilities,\n Color,\n} from \"../../../src/terminal/types.ts\"\nimport { encodeKeyToAnsi } from \"../../../src/terminal/key-encoding.ts\"\n\n// ===============================================================\n// Native module loading\n// ===============================================================\n\ninterface NapiCell {\n text: string\n fgR: number\n fgG: number\n fgB: number\n fgIsDefault: boolean\n bgR: number\n bgG: number\n bgB: number\n bgIsDefault: boolean\n bold: boolean\n faint: boolean\n italic: boolean\n underline: string\n strikethrough: boolean\n inverse: boolean\n wide: boolean\n}\n\ninterface NapiCursor {\n x: number\n y: number\n visible: boolean\n style: string\n}\n\ninterface NapiScrollback {\n viewportOffset: number\n totalLines: number\n screenLines: number\n}\n\ninterface Vt100RustTerminal {\n feed(data: Buffer): void\n resize(cols: number, rows: number): void\n reset(): void\n getText(): string\n getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string\n getCell(row: number, col: number): NapiCell\n getLine(row: number): NapiCell[]\n getCursor(): NapiCursor\n getMode(mode: string): boolean\n getTitle(): string\n getScrollback(): NapiScrollback\n scrollViewport(delta: number): void\n /** Read pending response data (DA1/DA2/DSR). Returns null if no responses pending or method not available. */\n readResponse?(): Buffer | null\n}\n\ninterface NativeModule {\n Vt100RustTerminal: new (cols: number, rows: number, scrollbackLimit?: number) => Vt100RustTerminal\n}\n\nlet nativeModule: NativeModule | null = null\nlet loadError: Error | null = null\n\n/**\n * Load the native vt100-rust module. Call once before creating backends.\n * Returns the native module, or throws if the .node file is missing.\n */\nexport function loadVt100RustNative(): NativeModule {\n if (nativeModule) return nativeModule\n if (loadError) throw loadError\n\n try {\n // Try loading the prebuilt native addon\n // The .node file should be at packages/vt100-rust/termless-vt100-rust.node\n const mod = require(\"../termless-vt100-rust.node\") as NativeModule\n nativeModule = mod\n return mod\n } catch (e) {\n loadError = new Error(\n \"Failed to load vt100-rust native module. Build it first:\\n\" +\n \" cd packages/vt100-rust/native && cargo build --release\\n\" +\n \" cp target/release/libtermless_vt100_rust_native.dylib ../termless-vt100-rust.node\\n\" +\n `\\nOriginal error: ${e instanceof Error ? e.message : String(e)}`,\n )\n throw loadError\n }\n}\n\n// ===============================================================\n// Cell conversion\n// ===============================================================\n\nfunction convertNapiCell(cell: NapiCell): Cell {\n const fg: Color | null = cell.fgIsDefault ? null : { r: cell.fgR, g: cell.fgG, b: cell.fgB }\n const bg: Color | null = cell.bgIsDefault ? null : { r: cell.bgR, g: cell.bgG, b: cell.bgB }\n\n return {\n char: cell.text,\n fg,\n bg,\n bold: cell.bold,\n dim: cell.faint,\n italic: cell.italic,\n underline: cell.underline === \"none\" ? false : (cell.underline as Cell[\"underline\"]),\n underlineColor: null,\n strikethrough: cell.strikethrough,\n inverse: cell.inverse,\n blink: false,\n hidden: false,\n wide: cell.wide,\n continuation: false,\n hyperlink: null,\n }\n}\n\nconst EMPTY_CELL: Cell = {\n char: \"\",\n fg: null,\n bg: null,\n bold: false,\n dim: false,\n italic: false,\n underline: false,\n underlineColor: null,\n strikethrough: false,\n inverse: false,\n blink: false,\n hidden: false,\n wide: false,\n continuation: false,\n hyperlink: null,\n}\n\n// ===============================================================\n// Backend factory\n// ===============================================================\n\nconst DEFAULT_COLS = 80\nconst DEFAULT_ROWS = 24\n\n/**\n * Create a vt100-rust backend for termless.\n *\n * Uses the doy/vt100-rust crate via napi-rs for headless terminal emulation.\n * The native module must be built and available before calling this function.\n *\n * @param opts - Optional terminal dimensions for eager initialization\n * @param native - Optional pre-loaded native module (for test isolation)\n */\nexport function createVt100RustBackend(opts?: Partial<TerminalOptions>, native?: NativeModule): TerminalBackend {\n let term: Vt100RustTerminal | null = null\n let nativeMod: NativeModule | null = native ?? null\n let cols = DEFAULT_COLS\n let rows = DEFAULT_ROWS\n\n function ensureNative(): NativeModule {\n if (!nativeMod) {\n nativeMod = loadVt100RustNative()\n }\n return nativeMod\n }\n\n function ensureTerm(): Vt100RustTerminal {\n if (!term) throw new Error(\"vt100-rust backend not initialized -- call init() first\")\n return term\n }\n\n function init(options: TerminalOptions): void {\n const mod = ensureNative()\n\n cols = options.cols\n rows = options.rows\n\n term = new mod.Vt100RustTerminal(cols, rows, options.scrollbackLimit ?? 1000)\n }\n\n // Eagerly init if opts provided\n if (opts) {\n init({\n cols: opts.cols ?? DEFAULT_COLS,\n rows: opts.rows ?? DEFAULT_ROWS,\n scrollbackLimit: opts.scrollbackLimit,\n })\n }\n\n function destroy(): void {\n term = null\n }\n\n /**\n * Standard cell metrics used for synthetic CSI 14t / 18t window-op\n * responses. The vt100-rust backend (doy/vt100 crate via napi-rs)\n * exposes only a cell grid — there's no native window-pixel concept,\n * so we synthesize 14t/18t from `rows × cols × typical cell metrics`.\n * silvery's `resolveMouseOption()` divides pixel coords by cell metrics\n * to recover cell coords for SGR-Pixels (1016) mouse hit-testing.\n * 8 × 17 is the typical Iosevka/JetBrains Mono cell at 12pt 96 DPI —\n * what matters is the ratio matches the backend's cell grid\n * (one cell = one cell, invariant).\n */\n const CELL_W_PX = 8\n const CELL_H_PX = 17\n\n // CSI 14t = text-area pixel-size query; reply CSI 4;h;w t\n // CSI 18t = text-area cell-size query; reply CSI 8;h;w t\n const CSI_14t_RE = /\\x1b\\[14t/g\n const CSI_18t_RE = /\\x1b\\[18t/g\n\n function feed(data: Uint8Array): void {\n const t = ensureTerm()\n t.feed(Buffer.from(data))\n\n // Drain DA1/DA2/DSR responses and forward to the terminal layer\n if (backend.onResponse && t.readResponse) {\n const response = t.readResponse()\n if (response && response.length > 0) {\n backend.onResponse(new Uint8Array(response))\n }\n }\n\n // Synthesize CSI 14t / 18t window-op probe responses from our\n // tracked cell grid (doy/vt100 has no window-pixel concept).\n if (backend.onResponse) {\n const text = new TextDecoder().decode(data)\n CSI_14t_RE.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = CSI_14t_RE.exec(text)) !== null) {\n const heightPx = rows * CELL_H_PX\n const widthPx = cols * CELL_W_PX\n backend.onResponse(new TextEncoder().encode(`\\x1b[4;${heightPx};${widthPx}t`))\n }\n CSI_18t_RE.lastIndex = 0\n while ((m = CSI_18t_RE.exec(text)) !== null) {\n backend.onResponse(new TextEncoder().encode(`\\x1b[8;${rows};${cols}t`))\n }\n }\n }\n\n function resize(newCols: number, newRows: number): void {\n const t = ensureTerm()\n t.resize(newCols, newRows)\n cols = newCols\n rows = newRows\n }\n\n function reset(): void {\n ensureTerm().reset()\n }\n\n function getText(): string {\n return ensureTerm().getText()\n }\n\n function getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string {\n return ensureTerm().getTextRange(startRow, startCol, endRow, endCol)\n }\n\n function getCell(row: number, col: number): Cell {\n const t = ensureTerm()\n const napiCell = t.getCell(row, col)\n return convertNapiCell(napiCell)\n }\n\n function getLine(row: number): Cell[] {\n const t = ensureTerm()\n const napiCells = t.getLine(row)\n return napiCells.map(convertNapiCell)\n }\n\n function getLines(): Cell[][] {\n const result: Cell[][] = []\n for (let row = 0; row < rows; row++) {\n result.push(getLine(row))\n }\n return result\n }\n\n function getCursor(): Cursor {\n const t = ensureTerm()\n const cursor = t.getCursor()\n return {\n col: cursor.x,\n row: cursor.y,\n x: cursor.x,\n y: cursor.y,\n visible: cursor.visible,\n style: (cursor.style as Cursor[\"style\"]) ?? \"block\",\n }\n }\n\n function getMode(mode: TerminalMode): boolean {\n return ensureTerm().getMode(mode)\n }\n\n function getTitle(): string {\n return ensureTerm().getTitle()\n }\n\n function getScrollback(): ScrollbackState {\n const t = ensureTerm()\n const sb = t.getScrollback()\n return {\n viewportTop: sb.viewportOffset,\n totalRows: sb.totalLines,\n screenRows: sb.screenLines,\n viewportOffset: sb.viewportOffset,\n totalLines: sb.totalLines,\n screenLines: sb.screenLines,\n }\n }\n\n function scrollViewport(delta: number): void {\n ensureTerm().scrollViewport(delta)\n }\n\n const capabilities: TerminalCapabilities = {\n name: \"vt100-rust\",\n version: \"0.1.0\",\n truecolor: true,\n kittyKeyboard: false,\n kittyGraphics: false,\n sixel: false,\n osc8Hyperlinks: false,\n semanticPrompts: false,\n unicode: \"13.0\",\n reflow: false,\n extensions: new Set(),\n }\n\n // TODO(native): Wire DA1/DA2/DSR response capture in lib.rs.\n // The doy/vt100 crate doesn't generate terminal responses (DA1/DA2/DSR).\n // It's a pure VT parser without a response generation mechanism.\n // If the crate adds response support in the future, steps would be:\n // 1. Capture output from the parser into a buffer\n // 2. Add #[napi] pub fn read_response(&self) -> Option<Buffer> that drains it\n // The TS side is already wired — it calls readResponse() after each feed().\n\n const backend: TerminalBackend = {\n name: \"vt100-rust\",\n init,\n destroy,\n feed,\n resize,\n reset,\n getText,\n getTextRange,\n getCell,\n getLine,\n getLines,\n getRow: getLine,\n getRows: getLines,\n getCursor,\n getMode,\n getTitle,\n getScrollback,\n scrollViewport,\n encodeKey: encodeKeyToAnsi,\n capabilities,\n }\n\n return backend\n}\n","export { createVt100RustBackend, loadVt100RustNative } from \"./backend.ts\"\n\nimport { createVt100RustBackend } from \"./backend.ts\"\nimport type { TerminalBackend, TerminalOptions } from \"../../../src/terminal/types.ts\"\n\n/** Resolve this backend for the registry. Loads native Rust bindings. */\nexport async function resolve(opts?: Partial<TerminalOptions>): Promise<TerminalBackend> {\n return createVt100RustBackend(opts)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/terminal/key-mapping.ts","../../../src/terminal/key-encoding.ts","../../../src/load-native.ts","../src/backend.ts","../src/index.ts"],"sourcesContent":["/**\n * Key parsing and ANSI encoding for termless.\n *\n * Converts human-readable key descriptions (e.g., \"Ctrl+a\", \"ArrowUp\", \"F5\")\n * into KeyDescriptor objects and ANSI escape sequences suitable for PTY input.\n */\n\nimport type { KeyDescriptor } from \"./types.ts\"\n\n// ── Named key → ANSI sequence map ──\n\nconst KEY_MAP: Record<string, string | null> = {\n ArrowUp: \"\\x1b[A\",\n ArrowDown: \"\\x1b[B\",\n ArrowLeft: \"\\x1b[D\",\n ArrowRight: \"\\x1b[C\",\n Home: \"\\x1b[H\",\n End: \"\\x1b[F\",\n PageUp: \"\\x1b[5~\",\n PageDown: \"\\x1b[6~\",\n Enter: \"\\r\",\n Tab: \"\\t\",\n Backspace: \"\\x7f\",\n Delete: \"\\x1b[3~\",\n Escape: \"\\x1b\",\n Space: \" \",\n // Pure modifier keys produce no output on their own\n Control: null,\n Shift: null,\n Alt: null,\n Meta: null,\n}\n\n// ── Function keys F1-F12 ──\n\nconst FKEY_MAP: Record<string, string> = {\n F1: \"\\x1bOP\",\n F2: \"\\x1bOQ\",\n F3: \"\\x1bOR\",\n F4: \"\\x1bOS\",\n F5: \"\\x1b[15~\",\n F6: \"\\x1b[17~\",\n F7: \"\\x1b[18~\",\n F8: \"\\x1b[19~\",\n F9: \"\\x1b[20~\",\n F10: \"\\x1b[21~\",\n F11: \"\\x1b[23~\",\n F12: \"\\x1b[24~\",\n}\n\n// ── Modifier aliases ──\n\nconst MODIFIER_ALIASES: Record<string, string> = {\n ctrl: \"ctrl\",\n control: \"ctrl\",\n shift: \"shift\",\n alt: \"alt\",\n meta: \"super\",\n cmd: \"super\",\n option: \"alt\",\n super: \"super\",\n}\n\nfunction normalizeModifier(mod: string): string {\n return MODIFIER_ALIASES[mod.toLowerCase()] ?? mod.toLowerCase()\n}\n\n/**\n * Parse a key string like \"Ctrl+a\", \"ArrowUp\", \"Shift+Tab\" into a KeyDescriptor.\n *\n * Supports modifier prefixes: Ctrl, Control, Alt, Shift, Meta, Cmd, Option, Super.\n * Modifiers are case-insensitive and separated by \"+\".\n */\nexport function parseKey(key: string): KeyDescriptor {\n const parts = key.split(\"+\")\n const mainKey = parts.pop()!\n const result: KeyDescriptor = { key: mainKey }\n\n for (const part of parts) {\n const normalized = normalizeModifier(part)\n switch (normalized) {\n case \"ctrl\":\n result.ctrl = true\n break\n case \"shift\":\n result.shift = true\n break\n case \"alt\":\n result.alt = true\n break\n case \"super\":\n result.super = true\n break\n }\n }\n\n return result\n}\n\n/**\n * Convert a key descriptor or key string to its ANSI escape sequence.\n *\n * Handles:\n * - Single characters: returned as-is\n * - Named keys (ArrowUp, Enter, Tab, etc.): mapped to standard ANSI sequences\n * - Function keys (F1-F12): mapped to VT220/xterm sequences\n * - Ctrl+letter: ASCII control codes 1-26\n * - Ctrl+Enter: newline (\\n)\n * - Alt+key: ESC prefix + key\n */\nexport function keyToAnsi(key: KeyDescriptor | string): string {\n const desc = typeof key === \"string\" ? parseKey(key) : key\n const { key: mainKey, ctrl, alt, shift } = desc\n const hasSuperOrMeta = desc.super\n\n // Ctrl+letter -> control code (ASCII 1-26)\n if (ctrl && mainKey.length === 1) {\n const code = mainKey.toLowerCase().charCodeAt(0) - 96\n if (code >= 1 && code <= 26) return String.fromCharCode(code)\n }\n\n // Ctrl+Enter -> \\n (legacy terminal convention: \\r = Enter, \\n = Ctrl+Enter/Ctrl+J)\n if (ctrl && mainKey === \"Enter\") {\n return \"\\n\"\n }\n\n // Alt (or Super/Meta) + single character -> ESC prefix\n if ((alt || hasSuperOrMeta) && mainKey.length === 1) {\n return `\\x1b${mainKey}`\n }\n\n // Function keys\n const fkey = FKEY_MAP[mainKey]\n if (fkey !== undefined) {\n // With modifiers, use CSI sequences with modifier parameter\n if (ctrl || alt || shift || hasSuperOrMeta) {\n const mod = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0) + (hasSuperOrMeta ? 8 : 0)\n // F1-F4 use SS3 format, convert to CSI for modified keys\n const fkeyNum = parseInt(mainKey.slice(1))\n if (fkeyNum <= 4) {\n // F1=11, F2=12, F3=13, F4=14 in CSI ~ format\n const csiNum = fkeyNum + 10\n return `\\x1b[${csiNum};${mod}~`\n }\n // F5-F12 already use CSI ~ format: extract the number\n const match = fkey.match(/\\x1b\\[(\\d+)~/)\n if (match) {\n return `\\x1b[${match[1]};${mod}~`\n }\n }\n return fkey\n }\n\n // Named keys from KEY_MAP\n const mapped = KEY_MAP[mainKey]\n if (mapped !== undefined) {\n if (mapped === null) return \"\" // Pure modifier keys produce nothing\n\n // Arrow/navigation keys with modifiers use CSI 1;mod format\n if ((ctrl || alt || shift || hasSuperOrMeta) && mapped.startsWith(\"\\x1b[\")) {\n const mod = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0) + (hasSuperOrMeta ? 8 : 0)\n // CSI letter format (e.g., \\x1b[A) -> CSI 1;mod letter\n const letterMatch = mapped.match(/\\x1b\\[([A-H])$/)\n if (letterMatch) {\n return `\\x1b[1;${mod}${letterMatch[1]}`\n }\n // CSI num ~ format (e.g., \\x1b[5~) -> CSI num;mod ~\n const tildeMatch = mapped.match(/\\x1b\\[(\\d+)~$/)\n if (tildeMatch) {\n return `\\x1b[${tildeMatch[1]};${mod}~`\n }\n }\n\n // Shift+Tab -> reverse tab (CSI Z)\n if (shift && mainKey === \"Tab\") {\n return \"\\x1b[Z\"\n }\n\n return mapped\n }\n\n // Single character without modifiers\n if (mainKey.length === 1) {\n // Shift+letter -> uppercase (passthrough for terminals)\n if (shift && mainKey.match(/[a-z]/)) {\n return mainKey.toUpperCase()\n }\n return mainKey\n }\n\n // Unknown key: return as-is\n return mainKey\n}\n","/**\n * Shared ANSI key encoding for terminal backends.\n *\n * All backends need to convert KeyDescriptor → Uint8Array for PTY input.\n * This module delegates to keyToAnsi() as the single source of truth for\n * ANSI key encoding, converting the string result to bytes.\n */\n\nimport type { KeyDescriptor } from \"./types.ts\"\nimport { keyToAnsi } from \"./key-mapping.ts\"\n\n// Module-level TextEncoder for efficiency (avoids per-call allocation)\nconst encoder = new TextEncoder()\n\n/**\n * Encode a KeyDescriptor to an ANSI byte sequence for PTY input.\n *\n * Delegates to keyToAnsi() for the actual encoding, then converts to bytes.\n * This is the canonical encoder — backends that don't have their own\n * protocol (e.g., kitty keyboard) should use this.\n */\nexport function encodeKeyToAnsi(key: KeyDescriptor): Uint8Array {\n const ansi = keyToAnsi(key)\n return encoder.encode(ansi)\n}\n","/**\n * The single native / CommonJS-addon load seam for @termless/core.\n *\n * `createRequire` is kept here DELIBERATELY — these are the cases the\n * ESM-only migration cannot convert, centralized so the package has exactly ONE\n * createRequire site (this file, the sole @termless/core entry on\n * check-no-createrequire.sh):\n *\n * 1. `loadNodePty()` — node-pty is a SYNCHRONOUS CommonJS native addon.\n * `spawnPortablePty()` is a sync API, so the load must be sync; `await\n * import()` is not an option and node-pty is not a node builtin (so\n * `process.getBuiltinModule` doesn't apply either).\n *\n * 2. `loadNativeAddon()` — tries caller-relative native candidates in order,\n * retaining each loader error for an actionable failure.\n *\n * 3. `resolveOptionalAsset()` — resolves an optional asset/module PATH (e.g.\n * `@twemoji/svg/<key>.svg`) via Node resolution. The ESM equivalent\n * `import.meta.resolve()` throws \"not supported\" under vitest's module\n * runner — the environment these render paths are exercised in — so\n * `require.resolve` is the only portable synchronous path resolver.\n *\n * Bead: createrequire-ban (wave 3 — native-loader centralization).\n */\nimport { createRequire } from \"node:module\"\n\nconst nativeRequire = createRequire(import.meta.url)\n\n/** Load a native addon from caller-relative candidates without static `.node` imports. */\nexport function loadNativeAddon<T>(options: {\n callerUrl: string\n packageName: string\n candidates: readonly string[]\n prebuiltScope: string\n}): T {\n const requireFromCaller = createRequire(options.callerUrl)\n const errors: string[] = []\n for (const candidate of options.candidates) {\n try {\n return requireFromCaller(candidate) as T\n } catch (error) {\n errors.push(` - ${candidate}: ${error instanceof Error ? error.message : String(error)}`)\n }\n }\n throw new Error(\n `Failed to load ${options.packageName} native module on ${process.platform}/${process.arch}.\\n` +\n `Prebuilt scope: ${options.prebuiltScope}\\n` +\n `Tried:\\n${options.candidates.map((candidate) => ` - ${candidate}`).join(\"\\n\")}\\n` +\n `Load attempts:\\n${errors.join(\"\\n\")}`,\n )\n}\n\n/** Minimal interface matching what we use from node-pty's IPty. */\nexport interface NodePtyInstance {\n write(data: string): void\n resize(cols: number, rows: number): void\n kill(signal?: string): void\n destroy(): void\n onData: (callback: (data: string) => void) => { dispose(): void }\n onExit: (callback: (e: { exitCode: number; signal?: number }) => void) => { dispose(): void }\n pid: number\n}\n\n/** Minimal interface for the node-pty module. */\nexport interface NodePtyModule {\n spawn(\n file: string,\n args: string[],\n options: {\n name?: string\n cols: number\n rows: number\n cwd?: string\n env?: Record<string, string>\n },\n ): NodePtyInstance\n}\n\n/**\n * Load node-pty synchronously. node-pty is a CommonJS native addon, so\n * createRequire is the correct way to load it from ESM and keep\n * `spawnPortablePty()` synchronous. Throws a clear, actionable error when the\n * optional peer is not installed.\n */\nexport function loadNodePty(): NodePtyModule {\n try {\n return nativeRequire(\"node-pty\") as NodePtyModule\n } catch {\n throw new Error(\n \"node-pty is required for PTY support on Node.js but was not found.\\n\" +\n \"Install it with: npm install node-pty\\n\" +\n \"Note: node-pty requires native compilation tools (Python, C++ compiler).\",\n )\n }\n}\n\n/**\n * Resolve an optional asset/module path via Node resolution\n * (`require.resolve`). Throws when the specifier is not installed — callers\n * catch to soft-fall-back (e.g. Twemoji asset → font rendering).\n */\nexport function resolveOptionalAsset(specifier: string): string {\n return nativeRequire.resolve(specifier)\n}\n","/**\n * Rust vt100 backend for termless.\n *\n * Wraps the doy/vt100-rust crate via napi-rs native bindings to implement\n * the TerminalBackend interface -- same VT parser used by many Rust terminal\n * projects, but headless via native Node addon.\n *\n * The prebuilt target is Linux x64 glibc.\n */\n\nimport type {\n TerminalBackend,\n TerminalOptions,\n Cell,\n Cursor,\n TerminalMode,\n ScrollbackState,\n TerminalCapabilities,\n Color,\n} from \"../../../src/terminal/types.ts\"\nimport { encodeKeyToAnsi } from \"../../../src/terminal/key-encoding.ts\"\nimport { loadNativeAddon } from \"../../../src/load-native.ts\"\n\n// ===============================================================\n// Native module loading\n// ===============================================================\n\ninterface NapiCell {\n text: string\n fgR: number\n fgG: number\n fgB: number\n fgIsDefault: boolean\n bgR: number\n bgG: number\n bgB: number\n bgIsDefault: boolean\n bold: boolean\n faint: boolean\n italic: boolean\n underline: string\n strikethrough: boolean\n inverse: boolean\n wide: boolean\n}\n\ninterface NapiCursor {\n x: number\n y: number\n visible: boolean\n style: string\n}\n\ninterface NapiScrollback {\n viewportOffset: number\n totalLines: number\n screenLines: number\n}\n\ninterface Vt100RustTerminal {\n feed(data: Buffer): void\n resize(cols: number, rows: number): void\n reset(): void\n getText(): string\n getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string\n getCell(row: number, col: number): NapiCell\n getLine(row: number): NapiCell[]\n getCursor(): NapiCursor\n getMode(mode: string): boolean\n getTitle(): string\n getScrollback(): NapiScrollback\n scrollViewport(delta: number): void\n /** Read pending response data (DA1/DA2/DSR). Returns null if no responses pending or method not available. */\n readResponse?(): Buffer | null\n}\n\ninterface NativeModule {\n Vt100RustTerminal: new (cols: number, rows: number, scrollbackLimit?: number) => Vt100RustTerminal\n}\n\nlet nativeModule: NativeModule | null = null\nlet loadError: Error | null = null\n\n/**\n * Load the native vt100-rust module. Call once before creating backends.\n * Returns the native module, or throws if the .node file is missing.\n */\nexport function loadVt100RustNative(): NativeModule {\n if (nativeModule) return nativeModule\n if (loadError) throw loadError\n\n try {\n nativeModule = loadNativeAddon<NativeModule>({\n callerUrl: import.meta.url,\n packageName: \"@termless/vt100-rust\",\n candidates: [\"../termless-vt100-rust.node\"],\n prebuiltScope: \"linux-x64-glibc only\",\n })\n return nativeModule\n } catch (error) {\n loadError = error as Error\n throw loadError\n }\n}\n\n// ===============================================================\n// Cell conversion\n// ===============================================================\n\nfunction convertNapiCell(cell: NapiCell): Cell {\n const fg: Color | null = cell.fgIsDefault ? null : { r: cell.fgR, g: cell.fgG, b: cell.fgB }\n const bg: Color | null = cell.bgIsDefault ? null : { r: cell.bgR, g: cell.bgG, b: cell.bgB }\n\n return {\n char: cell.text,\n fg,\n bg,\n bold: cell.bold,\n dim: cell.faint,\n italic: cell.italic,\n underline: cell.underline === \"none\" ? false : (cell.underline as Cell[\"underline\"]),\n underlineColor: null,\n strikethrough: cell.strikethrough,\n inverse: cell.inverse,\n blink: false,\n hidden: false,\n wide: cell.wide,\n continuation: false,\n hyperlink: null,\n }\n}\n\nconst EMPTY_CELL: Cell = {\n char: \"\",\n fg: null,\n bg: null,\n bold: false,\n dim: false,\n italic: false,\n underline: false,\n underlineColor: null,\n strikethrough: false,\n inverse: false,\n blink: false,\n hidden: false,\n wide: false,\n continuation: false,\n hyperlink: null,\n}\n\n// ===============================================================\n// Backend factory\n// ===============================================================\n\nconst DEFAULT_COLS = 80\nconst DEFAULT_ROWS = 24\n\n/**\n * Create a vt100-rust backend for termless.\n *\n * Uses the doy/vt100-rust crate via napi-rs for headless terminal emulation.\n * The native module must be built and available before calling this function.\n *\n * @param opts - Optional terminal dimensions for eager initialization\n * @param native - Optional pre-loaded native module (for test isolation)\n */\nexport function createVt100RustBackend(opts?: Partial<TerminalOptions>, native?: NativeModule): TerminalBackend {\n let term: Vt100RustTerminal | null = null\n let nativeMod: NativeModule | null = native ?? null\n let cols = DEFAULT_COLS\n let rows = DEFAULT_ROWS\n\n function ensureNative(): NativeModule {\n if (!nativeMod) {\n nativeMod = loadVt100RustNative()\n }\n return nativeMod\n }\n\n function ensureTerm(): Vt100RustTerminal {\n if (!term) throw new Error(\"vt100-rust backend not initialized -- call init() first\")\n return term\n }\n\n function init(options: TerminalOptions): void {\n const mod = ensureNative()\n\n cols = options.cols\n rows = options.rows\n\n term = new mod.Vt100RustTerminal(cols, rows, options.scrollbackLimit ?? 1000)\n }\n\n // Eagerly init if opts provided\n if (opts) {\n init({\n cols: opts.cols ?? DEFAULT_COLS,\n rows: opts.rows ?? DEFAULT_ROWS,\n scrollbackLimit: opts.scrollbackLimit,\n })\n }\n\n function destroy(): void {\n term = null\n }\n\n /**\n * Standard cell metrics used for synthetic CSI 14t / 18t window-op\n * responses. The vt100-rust backend (doy/vt100 crate via napi-rs)\n * exposes only a cell grid — there's no native window-pixel concept,\n * so we synthesize 14t/18t from `rows × cols × typical cell metrics`.\n * silvery's `resolveMouseOption()` divides pixel coords by cell metrics\n * to recover cell coords for SGR-Pixels (1016) mouse hit-testing.\n * 8 × 17 is the typical Iosevka/JetBrains Mono cell at 12pt 96 DPI —\n * what matters is the ratio matches the backend's cell grid\n * (one cell = one cell, invariant).\n */\n const CELL_W_PX = 8\n const CELL_H_PX = 17\n\n // CSI 14t = text-area pixel-size query; reply CSI 4;h;w t\n // CSI 18t = text-area cell-size query; reply CSI 8;h;w t\n const CSI_14t_RE = /\\x1b\\[14t/g\n const CSI_18t_RE = /\\x1b\\[18t/g\n\n function feed(data: Uint8Array): void {\n const t = ensureTerm()\n t.feed(Buffer.from(data))\n\n // Drain DA1/DA2/DSR responses and forward to the terminal layer\n if (backend.onResponse && t.readResponse) {\n const response = t.readResponse()\n if (response && response.length > 0) {\n backend.onResponse(new Uint8Array(response))\n }\n }\n\n // Synthesize CSI 14t / 18t window-op probe responses from our\n // tracked cell grid (doy/vt100 has no window-pixel concept).\n if (backend.onResponse) {\n const text = new TextDecoder().decode(data)\n CSI_14t_RE.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = CSI_14t_RE.exec(text)) !== null) {\n const heightPx = rows * CELL_H_PX\n const widthPx = cols * CELL_W_PX\n backend.onResponse(new TextEncoder().encode(`\\x1b[4;${heightPx};${widthPx}t`))\n }\n CSI_18t_RE.lastIndex = 0\n while ((m = CSI_18t_RE.exec(text)) !== null) {\n backend.onResponse(new TextEncoder().encode(`\\x1b[8;${rows};${cols}t`))\n }\n }\n }\n\n function resize(newCols: number, newRows: number): void {\n const t = ensureTerm()\n t.resize(newCols, newRows)\n cols = newCols\n rows = newRows\n }\n\n function reset(): void {\n ensureTerm().reset()\n }\n\n function getText(): string {\n return ensureTerm().getText()\n }\n\n function getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string {\n return ensureTerm().getTextRange(startRow, startCol, endRow, endCol)\n }\n\n function getCell(row: number, col: number): Cell {\n const t = ensureTerm()\n const napiCell = t.getCell(row, col)\n return convertNapiCell(napiCell)\n }\n\n function getLine(row: number): Cell[] {\n const t = ensureTerm()\n const napiCells = t.getLine(row)\n return napiCells.map(convertNapiCell)\n }\n\n function getLines(): Cell[][] {\n const result: Cell[][] = []\n for (let row = 0; row < rows; row++) {\n result.push(getLine(row))\n }\n return result\n }\n\n function getCursor(): Cursor {\n const t = ensureTerm()\n const cursor = t.getCursor()\n return {\n col: cursor.x,\n row: cursor.y,\n x: cursor.x,\n y: cursor.y,\n visible: cursor.visible,\n style: (cursor.style as Cursor[\"style\"]) ?? \"block\",\n }\n }\n\n function getMode(mode: TerminalMode): boolean {\n return ensureTerm().getMode(mode)\n }\n\n function getTitle(): string {\n return ensureTerm().getTitle()\n }\n\n function getScrollback(): ScrollbackState {\n const t = ensureTerm()\n const sb = t.getScrollback()\n return {\n viewportTop: sb.viewportOffset,\n totalRows: sb.totalLines,\n screenRows: sb.screenLines,\n viewportOffset: sb.viewportOffset,\n totalLines: sb.totalLines,\n screenLines: sb.screenLines,\n }\n }\n\n function scrollViewport(delta: number): void {\n ensureTerm().scrollViewport(delta)\n }\n\n const capabilities: TerminalCapabilities = {\n name: \"vt100-rust\",\n version: \"0.16.2\",\n truecolor: true,\n kittyKeyboard: false,\n kittyGraphics: false,\n sixel: false,\n osc8Hyperlinks: false,\n semanticPrompts: false,\n unicode: \"13.0\",\n reflow: false,\n extensions: new Set(),\n }\n\n // TODO(native): Wire DA1/DA2/DSR response capture in lib.rs.\n // The doy/vt100 crate doesn't generate terminal responses (DA1/DA2/DSR).\n // It's a pure VT parser without a response generation mechanism.\n // If the crate adds response support in the future, steps would be:\n // 1. Capture output from the parser into a buffer\n // 2. Add #[napi] pub fn read_response(&self) -> Option<Buffer> that drains it\n // The TS side is already wired — it calls readResponse() after each feed().\n\n const backend: TerminalBackend = {\n name: \"vt100-rust\",\n init,\n destroy,\n feed,\n resize,\n reset,\n getText,\n getTextRange,\n getCell,\n getLine,\n getLines,\n getRow: getLine,\n getRows: getLines,\n getCursor,\n getMode,\n getTitle,\n getScrollback,\n scrollViewport,\n encodeKey: encodeKeyToAnsi,\n capabilities,\n }\n\n return backend\n}\n","export { createVt100RustBackend, loadVt100RustNative } from \"./backend.ts\"\n\nimport { createVt100RustBackend } from \"./backend.ts\"\nimport type { TerminalBackend, TerminalOptions } from \"../../../src/terminal/types.ts\"\n\n/** Resolve this backend for the registry. Loads native Rust bindings. */\nexport async function resolve(opts?: Partial<TerminalOptions>): Promise<TerminalBackend> {\n return createVt100RustBackend(opts)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termless/vt100-rust",
3
- "version": "0.9.1",
3
+ "version": "0.10.2",
4
4
  "description": "Rust vt100 backend for termless — doy/vt100-rust crate via napi-rs for cross-terminal TUI testing",
5
5
  "keywords": [
6
6
  "headless",
@@ -25,8 +25,7 @@
25
25
  },
26
26
  "files": [
27
27
  "dist",
28
- "native",
29
- "*.node"
28
+ "termless-vt100-rust.node"
30
29
  ],
31
30
  "type": "module",
32
31
  "exports": {
@@ -39,7 +38,7 @@
39
38
  "access": "public"
40
39
  },
41
40
  "peerDependencies": {
42
- "@termless/core": "^0.9.1"
41
+ "@termless/core": "^0.10.2"
43
42
  },
44
43
  "tsdown": {
45
44
  "clean": true,
@@ -53,6 +52,6 @@
53
52
  },
54
53
  "scripts": {
55
54
  "build:native": "cd native && cargo build --release",
56
- "postbuild:native": "cp native/target/release/libtermless_vt100_rust_native.dylib termless-vt100-rust.node"
55
+ "postbuild:native": "bash ../../scripts/copy-native-addon.sh libtermless_vt100_rust_native termless-vt100-rust.node"
57
56
  }
58
57
  }
Binary file
package/native/Cargo.toml DELETED
@@ -1,16 +0,0 @@
1
- [package]
2
- name = "termless-vt100-rust-native"
3
- version = "0.1.0"
4
- edition = "2021"
5
- description = "napi-rs bindings for vt100 Rust crate"
6
-
7
- [lib]
8
- crate-type = ["cdylib"]
9
-
10
- [dependencies]
11
- napi = { version = "3", features = ["napi8"] }
12
- napi-derive = "3"
13
- vt100 = "0.15"
14
-
15
- [build-dependencies]
16
- napi-build = "2"
package/native/build.rs DELETED
@@ -1,5 +0,0 @@
1
- extern crate napi_build;
2
-
3
- fn main() {
4
- napi_build::setup();
5
- }
package/native/src/lib.rs DELETED
@@ -1,427 +0,0 @@
1
- use napi::bindgen_prelude::*;
2
- use napi_derive::napi;
3
- use std::sync::Mutex;
4
-
5
- // ===============================================================
6
- // Standard 256-color palette (indices 0..=255) → RGB
7
- // ===============================================================
8
-
9
- /// Convert a 256-color palette index to (r, g, b).
10
- fn palette_index_to_rgb(idx: u8) -> (u8, u8, u8) {
11
- match idx {
12
- // Standard 16 colors (ANSI)
13
- 0 => (0, 0, 0),
14
- 1 => (128, 0, 0),
15
- 2 => (0, 128, 0),
16
- 3 => (128, 128, 0),
17
- 4 => (0, 0, 128),
18
- 5 => (128, 0, 128),
19
- 6 => (0, 128, 128),
20
- 7 => (192, 192, 192),
21
- 8 => (128, 128, 128),
22
- 9 => (255, 0, 0),
23
- 10 => (0, 255, 0),
24
- 11 => (255, 255, 0),
25
- 12 => (0, 0, 255),
26
- 13 => (255, 0, 255),
27
- 14 => (0, 255, 255),
28
- 15 => (255, 255, 255),
29
- // 6x6x6 color cube (indices 16..=231)
30
- 16..=231 => {
31
- let n = idx - 16;
32
- let b = n % 6;
33
- let g = (n / 6) % 6;
34
- let r = n / 36;
35
- let to_val = |c: u8| if c == 0 { 0u8 } else { 55 + 40 * c };
36
- (to_val(r), to_val(g), to_val(b))
37
- }
38
- // Grayscale ramp (indices 232..=255)
39
- 232..=255 => {
40
- let v = 8 + 10 * (idx - 232);
41
- (v, v, v)
42
- }
43
- }
44
- }
45
-
46
- // ===============================================================
47
- // Exported cell type
48
- // ===============================================================
49
-
50
- #[napi(object)]
51
- pub struct NapiCell {
52
- pub text: String,
53
- pub fg_r: u8,
54
- pub fg_g: u8,
55
- pub fg_b: u8,
56
- pub fg_is_default: bool,
57
- pub bg_r: u8,
58
- pub bg_g: u8,
59
- pub bg_b: u8,
60
- pub bg_is_default: bool,
61
- pub bold: bool,
62
- pub faint: bool,
63
- pub italic: bool,
64
- pub underline: String,
65
- pub strikethrough: bool,
66
- pub inverse: bool,
67
- pub wide: bool,
68
- }
69
-
70
- #[napi(object)]
71
- pub struct NapiCursor {
72
- pub x: u32,
73
- pub y: u32,
74
- pub visible: bool,
75
- pub style: String,
76
- }
77
-
78
- #[napi(object)]
79
- pub struct NapiScrollback {
80
- pub viewport_offset: i32,
81
- pub total_lines: u32,
82
- pub screen_lines: u32,
83
- }
84
-
85
- // ===============================================================
86
- // Terminal wrapper
87
- // ===============================================================
88
-
89
- #[napi]
90
- pub struct Vt100RustTerminal {
91
- parser: Mutex<vt100::Parser>,
92
- cols: u16,
93
- rows: u16,
94
- title: Mutex<String>,
95
- }
96
-
97
- #[napi]
98
- impl Vt100RustTerminal {
99
- #[napi(constructor)]
100
- pub fn new(cols: u32, rows: u32, scrollback_limit: Option<u32>) -> Self {
101
- let scrollback = scrollback_limit.unwrap_or(1000) as usize;
102
- let r = rows as u16;
103
- let c = cols as u16;
104
-
105
- let parser = vt100::Parser::new(r, c, scrollback);
106
-
107
- Vt100RustTerminal {
108
- parser: Mutex::new(parser),
109
- cols: c,
110
- rows: r,
111
- title: Mutex::new(String::new()),
112
- }
113
- }
114
-
115
- #[napi]
116
- pub fn feed(&self, data: Buffer) -> Result<()> {
117
- let mut parser = self.parser.lock().map_err(|e| {
118
- Error::from_reason(format!("Failed to lock parser: {}", e))
119
- })?;
120
- parser.process(data.as_ref());
121
- // Capture title from terminal state
122
- let title = parser.screen().title().to_string();
123
- let mut t = self.title.lock().map_err(|e| {
124
- Error::from_reason(format!("Failed to lock title: {}", e))
125
- })?;
126
- *t = title;
127
- Ok(())
128
- }
129
-
130
- #[napi]
131
- pub fn resize(&self, cols: u32, rows: u32) -> Result<()> {
132
- let mut parser = self.parser.lock().map_err(|e| {
133
- Error::from_reason(format!("Failed to lock parser: {}", e))
134
- })?;
135
- parser.set_size(rows as u16, cols as u16);
136
- Ok(())
137
- }
138
-
139
- #[napi]
140
- pub fn reset(&self) -> Result<()> {
141
- let mut parser = self.parser.lock().map_err(|e| {
142
- Error::from_reason(format!("Failed to lock parser: {}", e))
143
- })?;
144
- // Feed RIS escape sequence to reset
145
- parser.process(b"\x1bc");
146
- let mut t = self.title.lock().map_err(|e| {
147
- Error::from_reason(format!("Failed to lock title: {}", e))
148
- })?;
149
- t.clear();
150
- Ok(())
151
- }
152
-
153
- #[napi]
154
- pub fn get_text(&self) -> Result<String> {
155
- let parser = self.parser.lock().map_err(|e| {
156
- Error::from_reason(format!("Failed to lock parser: {}", e))
157
- })?;
158
- let screen = parser.screen();
159
- let mut lines = Vec::new();
160
-
161
- // Scrollback lines
162
- let scrollback_len = screen.scrollback();
163
- for row_offset in 0..scrollback_len {
164
- // Scrollback rows are indexed negatively from the top of the screen
165
- let neg_row = -(scrollback_len as i32) + row_offset as i32;
166
- let row_idx = neg_row as i32;
167
- let mut line = String::new();
168
- for col in 0..self.cols {
169
- if let Some(cell) = screen.cell(row_idx as u16, col) {
170
- let contents = cell.contents();
171
- if contents.is_empty() {
172
- line.push(' ');
173
- } else {
174
- line.push_str(&contents);
175
- }
176
- } else {
177
- line.push(' ');
178
- }
179
- }
180
- lines.push(line.trim_end().to_string());
181
- }
182
-
183
- // Visible screen lines
184
- for row in 0..self.rows {
185
- let mut line = String::new();
186
- for col in 0..self.cols {
187
- if let Some(cell) = screen.cell(row, col) {
188
- let contents = cell.contents();
189
- if contents.is_empty() {
190
- line.push(' ');
191
- } else {
192
- line.push_str(&contents);
193
- }
194
- } else {
195
- line.push(' ');
196
- }
197
- }
198
- lines.push(line.trim_end().to_string());
199
- }
200
-
201
- Ok(lines.join("\n"))
202
- }
203
-
204
- #[napi]
205
- pub fn get_text_range(
206
- &self,
207
- start_row: i32,
208
- start_col: i32,
209
- end_row: i32,
210
- end_col: i32,
211
- ) -> Result<String> {
212
- let parser = self.parser.lock().map_err(|e| {
213
- Error::from_reason(format!("Failed to lock parser: {}", e))
214
- })?;
215
- let screen = parser.screen();
216
- let mut parts = Vec::new();
217
-
218
- for row in start_row..=end_row {
219
- if row < 0 || row >= self.rows as i32 {
220
- continue;
221
- }
222
- let col_start = if row == start_row {
223
- start_col as u16
224
- } else {
225
- 0
226
- };
227
- let col_end = if row == end_row {
228
- end_col as u16
229
- } else {
230
- self.cols
231
- };
232
-
233
- let mut text = String::new();
234
- for col in col_start..col_end.min(self.cols) {
235
- if let Some(cell) = screen.cell(row as u16, col) {
236
- let contents = cell.contents();
237
- if contents.is_empty() {
238
- text.push(' ');
239
- } else {
240
- text.push_str(&contents);
241
- }
242
- } else {
243
- text.push(' ');
244
- }
245
- }
246
- parts.push(text.trim_end().to_string());
247
- }
248
-
249
- Ok(parts.join("\n"))
250
- }
251
-
252
- #[napi]
253
- pub fn get_cell(&self, row: i32, col: i32) -> Result<NapiCell> {
254
- let parser = self.parser.lock().map_err(|e| {
255
- Error::from_reason(format!("Failed to lock parser: {}", e))
256
- })?;
257
- let screen = parser.screen();
258
-
259
- if row < 0 || row >= self.rows as i32 || col < 0 || col >= self.cols as i32 {
260
- return Ok(empty_cell());
261
- }
262
-
263
- match screen.cell(row as u16, col as u16) {
264
- Some(cell) => Ok(convert_cell(cell)),
265
- None => Ok(empty_cell()),
266
- }
267
- }
268
-
269
- #[napi]
270
- pub fn get_line(&self, row: i32) -> Result<Vec<NapiCell>> {
271
- let parser = self.parser.lock().map_err(|e| {
272
- Error::from_reason(format!("Failed to lock parser: {}", e))
273
- })?;
274
- let screen = parser.screen();
275
-
276
- if row < 0 || row >= self.rows as i32 {
277
- return Ok((0..self.cols).map(|_| empty_cell()).collect());
278
- }
279
-
280
- Ok((0..self.cols)
281
- .map(|col| match screen.cell(row as u16, col) {
282
- Some(cell) => convert_cell(cell),
283
- None => empty_cell(),
284
- })
285
- .collect())
286
- }
287
-
288
- #[napi]
289
- pub fn get_cursor(&self) -> Result<NapiCursor> {
290
- let parser = self.parser.lock().map_err(|e| {
291
- Error::from_reason(format!("Failed to lock parser: {}", e))
292
- })?;
293
- let screen = parser.screen();
294
- let (row, col) = screen.cursor_position();
295
-
296
- Ok(NapiCursor {
297
- x: col as u32,
298
- y: row as u32,
299
- visible: !screen.hide_cursor(),
300
- style: "block".to_string(),
301
- })
302
- }
303
-
304
- #[napi]
305
- pub fn get_mode(&self, mode: String) -> Result<bool> {
306
- let parser = self.parser.lock().map_err(|e| {
307
- Error::from_reason(format!("Failed to lock parser: {}", e))
308
- })?;
309
- let screen = parser.screen();
310
-
311
- Ok(match mode.as_str() {
312
- "altScreen" => screen.alternate_screen(),
313
- "cursorVisible" => !screen.hide_cursor(),
314
- "bracketedPaste" => screen.bracketed_paste(),
315
- "applicationCursor" => screen.application_cursor(),
316
- "applicationKeypad" => screen.application_keypad(),
317
- "mouseTracking" => screen.mouse_protocol_mode() != vt100::MouseProtocolMode::None,
318
- // Modes not directly exposed by the vt100 crate
319
- "autoWrap" => true, // Default on, not queryable
320
- "focusTracking" => false,
321
- "originMode" => false,
322
- "insertMode" => false,
323
- "reverseVideo" => false,
324
- _ => false,
325
- })
326
- }
327
-
328
- #[napi]
329
- pub fn get_title(&self) -> Result<String> {
330
- let t = self.title.lock().map_err(|e| {
331
- Error::from_reason(format!("Failed to lock title: {}", e))
332
- })?;
333
- Ok(t.clone())
334
- }
335
-
336
- #[napi]
337
- pub fn get_scrollback(&self) -> Result<NapiScrollback> {
338
- let parser = self.parser.lock().map_err(|e| {
339
- Error::from_reason(format!("Failed to lock parser: {}", e))
340
- })?;
341
- let screen = parser.screen();
342
- let scrollback_len = screen.scrollback() as u32;
343
-
344
- Ok(NapiScrollback {
345
- viewport_offset: 0, // Headless mode, no viewport scroll state
346
- total_lines: scrollback_len + self.rows as u32,
347
- screen_lines: self.rows as u32,
348
- })
349
- }
350
-
351
- #[napi]
352
- pub fn scroll_viewport(&self, _delta: i32) -> Result<()> {
353
- // No-op in headless mode — no viewport scroll position to track
354
- Ok(())
355
- }
356
- }
357
-
358
- // ===============================================================
359
- // Helpers
360
- // ===============================================================
361
-
362
- fn empty_cell() -> NapiCell {
363
- NapiCell {
364
- text: String::new(),
365
- fg_r: 0,
366
- fg_g: 0,
367
- fg_b: 0,
368
- fg_is_default: true,
369
- bg_r: 0,
370
- bg_g: 0,
371
- bg_b: 0,
372
- bg_is_default: true,
373
- bold: false,
374
- faint: false,
375
- italic: false,
376
- underline: "none".to_string(),
377
- strikethrough: false,
378
- inverse: false,
379
- wide: false,
380
- }
381
- }
382
-
383
- fn convert_cell(cell: &vt100::Cell) -> NapiCell {
384
- let text = cell.contents();
385
-
386
- // Extract foreground color
387
- let (fg_r, fg_g, fg_b, fg_is_default) = match cell.fgcolor() {
388
- vt100::Color::Default => (0, 0, 0, true),
389
- vt100::Color::Rgb(r, g, b) => (r, g, b, false),
390
- vt100::Color::Idx(idx) => {
391
- let (r, g, b) = palette_index_to_rgb(idx);
392
- (r, g, b, false)
393
- }
394
- };
395
-
396
- // Extract background color
397
- let (bg_r, bg_g, bg_b, bg_is_default) = match cell.bgcolor() {
398
- vt100::Color::Default => (0, 0, 0, true),
399
- vt100::Color::Rgb(r, g, b) => (r, g, b, false),
400
- vt100::Color::Idx(idx) => {
401
- let (r, g, b) = palette_index_to_rgb(idx);
402
- (r, g, b, false)
403
- }
404
- };
405
-
406
- // Underline style — the vt100 crate only has on/off
407
- let underline = if cell.underline() { "single" } else { "none" };
408
-
409
- NapiCell {
410
- text: text.to_string(),
411
- fg_r,
412
- fg_g,
413
- fg_b,
414
- fg_is_default,
415
- bg_r,
416
- bg_g,
417
- bg_b,
418
- bg_is_default,
419
- bold: cell.bold(),
420
- faint: false, // vt100 crate doesn't expose faint/dim
421
- italic: cell.italic(),
422
- underline: underline.to_string(),
423
- strikethrough: false, // vt100 crate doesn't expose strikethrough
424
- inverse: cell.inverse(),
425
- wide: cell.is_wide(),
426
- }
427
- }