@termless/vt100-rust 0.8.4 → 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 +2 -0
- package/dist/index.d.mts +47 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +23 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
- package/termless-vt100-rust.node +0 -0
- package/native/Cargo.toml +0 -16
- package/native/build.rs +0 -5
- package/native/src/lib.rs +0 -427
package/README.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
//#region ../../src/
|
|
2
|
-
interface TerminalOptions {
|
|
3
|
-
cols: number;
|
|
4
|
-
rows: number;
|
|
5
|
-
scrollbackLimit?: number;
|
|
6
|
-
}
|
|
1
|
+
//#region ../../src/io/picture.d.ts
|
|
7
2
|
interface Cell {
|
|
8
3
|
char: string;
|
|
9
4
|
fg: Color | null;
|
|
@@ -50,13 +45,32 @@ interface Cursor {
|
|
|
50
45
|
visible: boolean | null;
|
|
51
46
|
/** Cursor shape. `null` if the backend doesn't know. */
|
|
52
47
|
style: CursorStyle | null;
|
|
53
|
-
/** @deprecated
|
|
48
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link Cursor.col}. Kept required during the migration window. */
|
|
54
49
|
x: number;
|
|
55
|
-
/** @deprecated
|
|
50
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link Cursor.row}. Kept required during the migration window. */
|
|
56
51
|
y: number;
|
|
57
52
|
}
|
|
58
53
|
type CursorStyle = "block" | "underline" | "beam";
|
|
59
|
-
|
|
54
|
+
/**
|
|
55
|
+
* A terminal mode an emulator can report.
|
|
56
|
+
*
|
|
57
|
+
* The full-word mode vocabulary. `terminal/types.ts` re-exports this as the
|
|
58
|
+
* deprecated `TerminalMode`.
|
|
59
|
+
*/
|
|
60
|
+
type Mode = "altScreen" | "cursorVisible" | "bracketedPaste" | "applicationCursor" | "applicationKeypad" | "autoWrap" | "mouseTracking" | "focusTracking" | "originMode" | "insertMode" | "reverseVideo";
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../../src/terminal/types.d.ts
|
|
63
|
+
interface TerminalOptions {
|
|
64
|
+
cols: number;
|
|
65
|
+
rows: number;
|
|
66
|
+
scrollbackLimit?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* @deprecated REMOVING in unterm phase A4 — renamed to `Mode`
|
|
70
|
+
* (`@termless/core/io`). A true alias: same union, full-word member names
|
|
71
|
+
* unchanged.
|
|
72
|
+
*/
|
|
73
|
+
type TerminalMode = Mode;
|
|
60
74
|
interface ScrollbackState {
|
|
61
75
|
/**
|
|
62
76
|
* Absolute row index of the row where the viewport region starts in the buffer.
|
|
@@ -78,11 +92,11 @@ interface ScrollbackState {
|
|
|
78
92
|
* (rows `totalRows - screenRows` through `totalRows - 1`).
|
|
79
93
|
*/
|
|
80
94
|
screenRows: number;
|
|
81
|
-
/** @deprecated
|
|
95
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link ScrollbackState.viewportTop}. Kept required during the migration window. */
|
|
82
96
|
viewportOffset: number;
|
|
83
|
-
/** @deprecated
|
|
97
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link ScrollbackState.totalRows}. Kept required during the migration window. */
|
|
84
98
|
totalLines: number;
|
|
85
|
-
/** @deprecated
|
|
99
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link ScrollbackState.screenRows}. Kept required during the migration window. */
|
|
86
100
|
screenLines: number;
|
|
87
101
|
}
|
|
88
102
|
interface KeyDescriptor {
|
|
@@ -94,6 +108,7 @@ interface KeyDescriptor {
|
|
|
94
108
|
}
|
|
95
109
|
interface TerminalCapabilities {
|
|
96
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. */
|
|
97
112
|
version: string;
|
|
98
113
|
truecolor: boolean;
|
|
99
114
|
kittyKeyboard: boolean;
|
|
@@ -115,6 +130,13 @@ interface TerminalCapabilities {
|
|
|
115
130
|
* **Coordinate system**: All row parameters use **absolute buffer rows**.
|
|
116
131
|
* Row 0 is the first row in scrollback history. The screen occupies the
|
|
117
132
|
* last `screenRows` rows (from `totalRows - screenRows` to `totalRows - 1`).
|
|
133
|
+
*
|
|
134
|
+
* @deprecated REMOVING in unterm phase A4 — the read half of `Emulator`
|
|
135
|
+
* (`@termless/core/io`), where the readable picture lives on the emulator
|
|
136
|
+
* itself. Not a true alias: `Emulator` spells the cursor, modes and size as
|
|
137
|
+
* properties (`cursor`, `modes`, `size`) rather than accessors
|
|
138
|
+
* (`getCursor()`, `getMode(m)`, `getScrollback()`). Adapt with
|
|
139
|
+
* `emulatorFromBackend()` in `terminal/io-compat.ts`.
|
|
118
140
|
*/
|
|
119
141
|
interface Terminal {
|
|
120
142
|
/** Get all buffer text (scrollback + screen) as a newline-joined string. */
|
|
@@ -131,9 +153,9 @@ interface Terminal {
|
|
|
131
153
|
getRow(row: number): Cell[];
|
|
132
154
|
/** Get every row as a cell array (entire buffer: scrollback + screen). */
|
|
133
155
|
getRows(): Cell[][];
|
|
134
|
-
/** @deprecated
|
|
156
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link Terminal.getRow} (row = cells). Kept required during the migration window. */
|
|
135
157
|
getLine(row: number): Cell[];
|
|
136
|
-
/** @deprecated
|
|
158
|
+
/** @deprecated REMOVING in unterm phase A4 — renamed to {@link Terminal.getRows} (row = cells). Kept required during the migration window. */
|
|
137
159
|
getLines(): Cell[][];
|
|
138
160
|
getCursor(): Cursor;
|
|
139
161
|
getMode(mode: TerminalMode): boolean;
|
|
@@ -177,6 +199,17 @@ interface ScreenshotOptions {
|
|
|
177
199
|
*/
|
|
178
200
|
renderer?: "canvas" | "resvg" | "swash" | "browser" | "auto";
|
|
179
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Every backend implements this: the {@link Terminal} read contract plus
|
|
204
|
+
* lifecycle, the byte feed, resize, key encoding and capabilities.
|
|
205
|
+
*
|
|
206
|
+
* @deprecated REMOVING in unterm phase A4 — replaced by `Emulator`
|
|
207
|
+
* (`@termless/core/io`), which collapses `feed`/`resize`/`reset` into a single
|
|
208
|
+
* `apply(event)` door. Not a true alias: `Emulator` has no `init`/`destroy`
|
|
209
|
+
* lifecycle, no `encodeKey`, and no `capabilities` — those either move to the
|
|
210
|
+
* factory that builds the emulator or to the parts around it. Adapt an
|
|
211
|
+
* existing backend with `emulatorFromBackend()` in `terminal/io-compat.ts`.
|
|
212
|
+
*/
|
|
180
213
|
interface TerminalBackend extends Terminal {
|
|
181
214
|
readonly name: string;
|
|
182
215
|
init(opts: TerminalOptions): void;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/terminal/types.ts","../src/backend.ts","../src/index.ts"],"mappings":";
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
353
|
+
version: "0.16.2",
|
|
346
354
|
truecolor: true,
|
|
347
355
|
kittyKeyboard: false,
|
|
348
356
|
kittyGraphics: false,
|
package/dist/index.mjs.map
CHANGED
|
@@ -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 CursorState,\n TerminalMode,\n ScrollbackState,\n TerminalCapabilities,\n RGB,\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: RGB | null = cell.fgIsDefault ? null : { r: cell.fgR, g: cell.fgG, b: cell.fgB }\n const bg: RGB | 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(): CursorState {\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 CursorState[\"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.
|
|
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
|
-
"
|
|
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.
|
|
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": "
|
|
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
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
|
-
}
|