@tuidom/terminal-backend 0.1.0

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.
@@ -0,0 +1,83 @@
1
+ import type { ITerminalBackend } from "@tuidom/core/backend/iTerminalBackend";
2
+ import { Point, Size } from "@tuidom/core/common/geometryPromitives";
3
+ import type { KeyPressEvent } from "@tuidom/core/input/keyEvent";
4
+ import type { MouseToken } from "@tuidom/core/input/rawTerminalToken";
5
+ import { Grid } from "@tuidom/core/rendering/grid";
6
+ /**
7
+ * Real terminal backend: reads from process.stdin, writes to process.stdout.
8
+ * Handles alternate screen, raw mode, cursor visibility, signal cleanup.
9
+ * Enables the Kitty Keyboard Protocol; inside tmux the input-mode sequences are sent
10
+ * directly so tmux manages them per-pane (passthrough is reserved for OSC clipboard).
11
+ */
12
+ export declare class NodeTerminalBackend implements ITerminalBackend {
13
+ private inputCallbacks;
14
+ private mouseCallbacks;
15
+ private pasteCallbacks;
16
+ private resizeCallbacks;
17
+ private oscResponseCallbacks;
18
+ private deviceReportCallbacks;
19
+ private stdin;
20
+ private stdout;
21
+ private onDataHandler;
22
+ private partialInputTimer;
23
+ private onResizeHandler;
24
+ private resizeThrottleTimer;
25
+ private resizeThrottleMs;
26
+ private lastEmittedSize;
27
+ private resizePending;
28
+ private cleanupHandlers;
29
+ private readonly isTmux;
30
+ private readonly inputParser;
31
+ private readonly renderer;
32
+ /**
33
+ * The cell-diff for the current frame is buffered here instead of going straight
34
+ * to stdout, so renderFrame() can decide whether the frame is empty before
35
+ * emitting the frame wrapper (sync markers + cursor hide/show). See renderFrame.
36
+ */
37
+ private readonly frameBuffer;
38
+ private prevGrid;
39
+ private prevCursorPosition;
40
+ constructor(stdin?: NodeJS.ReadStream, stdout?: NodeJS.WriteStream, options?: {
41
+ resizeThrottleMs?: number;
42
+ });
43
+ /**
44
+ * Write a raw escape sequence to stdout, wrapping in TMUX passthrough if needed.
45
+ *
46
+ * ВАЖНО: passthrough применять ТОЛЬКО к последовательностям, которые tmux НЕ понимает
47
+ * и которые должны дойти до внешнего терминала напрямую (например, OSC 52 — буфер обмена).
48
+ *
49
+ * Последовательности режимов ВВОДА (Kitty keyboard protocol `CSI > u`/`CSI < u`/`CSI ? u`,
50
+ * mouse tracking `?1000/1002/1003/1006`) tmux понимает и отслеживает ПОПАНЕЛЬНО — их нужно
51
+ * слать напрямую (writeDirect), иначе passthrough включит режим на внешнем терминале глобально,
52
+ * и кодировка клавиш «протечёт» во все панели tmux (Ctrl+S перестаёт быть префиксом, а в других
53
+ * вкладках появляются `[115;5u`-подобные символы).
54
+ */
55
+ private writePassthrough;
56
+ /**
57
+ * Write a sequence straight to stdout without TMUX passthrough. Use for input-mode
58
+ * sequences (Kitty keyboard protocol, mouse tracking) so tmux manages them per-pane.
59
+ */
60
+ private writeDirect;
61
+ /**
62
+ * Write an OSC or other escape sequence to the terminal.
63
+ * TMUX passthrough is applied automatically when running inside TMUX.
64
+ */
65
+ writeOscSequence(sequence: string): void;
66
+ onInput(callback: (event: KeyPressEvent) => void): void;
67
+ onMouse(callback: (event: MouseToken) => void): void;
68
+ onPaste(callback: (text: string) => void): void;
69
+ onResize(callback: (size: Size) => void): void;
70
+ onOscResponse(callback: (code: number, data: string) => void): void;
71
+ probeKeyboardProtocol(onResult: (supported: boolean) => void): void;
72
+ /** Fan a parsed input batch out to the registered callbacks. */
73
+ private dispatchInput;
74
+ /** If a partial sequence is buffered, flush it after a short grace period (the rest never came). */
75
+ private schedulePartialInputFlush;
76
+ private clearPartialInputTimer;
77
+ /** Emit resize only if dimensions actually changed */
78
+ private emitResize;
79
+ renderFrame(grid: Grid, cursorPosition: Point | null): void;
80
+ getSize(): Size;
81
+ setup(): void;
82
+ teardown(): void;
83
+ }
@@ -0,0 +1,378 @@
1
+ import { Point, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { KeyInputParser } from "@tuidom/core/input/keyInputParser";
3
+ import { MOUSE_TRACKING_ALL_ENABLE, MOUSE_TRACKING_DISABLE } from "@tuidom/core/input/mouseTracking";
4
+ import { Grid } from "@tuidom/core/rendering/grid";
5
+ import { TerminalRenderer } from "@tuidom/core/rendering/terminalRenderer";
6
+ import { isInsideTmux } from "./terminalEnv.js";
7
+ /**
8
+ * Kitty Keyboard Protocol escape sequences.
9
+ *
10
+ * Flags (push mode): disambiguate(1) + event types(2) + all keys as escapes(8) + alternate keys(4) = 15
11
+ * Alternate keys (flag 4) makes the terminal report baseLayoutKey — the physical key position in the
12
+ * base (US QWERTY) layout — as the third sub-parameter of the codepoint field. This lets us match
13
+ * shortcuts like Ctrl+S regardless of the active keyboard layout (e.g. Russian).
14
+ * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
15
+ */
16
+ const KITTY_ENABLE = "\x1b[>15u";
17
+ const KITTY_DISABLE = "\x1b[<u";
18
+ /**
19
+ * xterm modifyOtherKeys, level 2 (`CSI > 4 ; 2 m`) + reset (`CSI > 4 ; 0 m`).
20
+ *
21
+ * tmux does NOT speak the Kitty keyboard protocol push (`CSI > 15 u`) — it ignores it,
22
+ * so inside tmux modified keys like Ctrl+Tab / Ctrl+Shift+<letter> arrive as their plain
23
+ * legacy byte (Ctrl+Tab → `\t`) and the shortcut is lost. tmux DOES honor modifyOtherKeys
24
+ * and then emits those keys in CSI-u form (Ctrl+Tab → `\x1b[9;5u`), which our tokenizer
25
+ * already parses. Sending both enables is the standard belt-and-suspenders: real Kitty (no
26
+ * tmux) uses the Kitty protocol and ignores modifyOtherKeys; tmux uses modifyOtherKeys.
27
+ * Written per-pane (`writeDirect`, no passthrough) like the other input modes.
28
+ */
29
+ const MODIFY_OTHER_KEYS_ENABLE = "\x1b[>4;2m";
30
+ const MODIFY_OTHER_KEYS_DISABLE = "\x1b[>4;0m";
31
+ /**
32
+ * Bracketed paste (DEC private mode ?2004). When enabled, the terminal wraps pasted
33
+ * clipboard content in `ESC[200~ … ESC[201~` so we insert it as one literal text block
34
+ * instead of replaying it as keystrokes (which loses newlines and runs special chars as
35
+ * shortcuts). Like the Kitty/mouse modes, tmux understands and tracks it per-pane, so it
36
+ * is written directly (no passthrough).
37
+ */
38
+ const BRACKETED_PASTE_ENABLE = "\x1b[?2004h";
39
+ const BRACKETED_PASTE_DISABLE = "\x1b[?2004l";
40
+ /**
41
+ * Keyboard-protocol probe: query current Kitty flags (`CSI ? u`) immediately followed by
42
+ * Primary Device Attributes (`CSI c`). Only Kitty-capable terminals answer the first; every
43
+ * terminal answers DA1, so the DA1 reply is the "all replies are in" sentinel. The timeout
44
+ * bounds the negative case (no reply at all → not supported).
45
+ */
46
+ const KITTY_FLAGS_QUERY = "\x1b[?u";
47
+ const DA1_QUERY = "\x1b[c";
48
+ const KEYBOARD_PROBE_TIMEOUT_MS = 200;
49
+ /**
50
+ * How long to hold a partial escape sequence that arrived at the end of a stdin read,
51
+ * waiting for the rest to follow (it does, on a split keypress over SSH/tmux). If nothing
52
+ * follows, the tail is flushed as-is — a lone ESC then resolves to the Escape key. Mirrors
53
+ * the classic terminal "escape timeout"; 50ms keeps Escape responsive while absorbing splits.
54
+ */
55
+ const PARTIAL_INPUT_FLUSH_MS = 50;
56
+ /**
57
+ * Wrap an escape sequence for TMUX DCS passthrough.
58
+ *
59
+ * TMUX не понимает произвольные escape-последовательности и глотает их.
60
+ * Чтобы передать их терминалу напрямую, нужно обернуть в DCS passthrough:
61
+ * \x1bPtmux;\x1b<sequence>\x1b\\
62
+ *
63
+ * Внутри passthrough каждый ESC (\x1b) в оригинальной последовательности
64
+ * удваивается (\x1b → \x1b\x1b).
65
+ *
66
+ * See: https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
67
+ */
68
+ function wrapForTmux(sequence) {
69
+ const escaped = sequence.replace(/\x1b/g, "\x1b\x1b");
70
+ return `\x1bPtmux;${escaped}\x1b\\`;
71
+ }
72
+ /**
73
+ * Real terminal backend: reads from process.stdin, writes to process.stdout.
74
+ * Handles alternate screen, raw mode, cursor visibility, signal cleanup.
75
+ * Enables the Kitty Keyboard Protocol; inside tmux the input-mode sequences are sent
76
+ * directly so tmux manages them per-pane (passthrough is reserved for OSC clipboard).
77
+ */
78
+ export class NodeTerminalBackend {
79
+ inputCallbacks = [];
80
+ mouseCallbacks = [];
81
+ pasteCallbacks = [];
82
+ resizeCallbacks = [];
83
+ oscResponseCallbacks = [];
84
+ deviceReportCallbacks = [];
85
+ stdin;
86
+ stdout;
87
+ onDataHandler = null;
88
+ partialInputTimer = null;
89
+ onResizeHandler = null;
90
+ resizeThrottleTimer = null;
91
+ resizeThrottleMs;
92
+ lastEmittedSize = null;
93
+ resizePending = false;
94
+ cleanupHandlers = [];
95
+ isTmux;
96
+ inputParser = new KeyInputParser();
97
+ renderer;
98
+ /**
99
+ * The cell-diff for the current frame is buffered here instead of going straight
100
+ * to stdout, so renderFrame() can decide whether the frame is empty before
101
+ * emitting the frame wrapper (sync markers + cursor hide/show). See renderFrame.
102
+ */
103
+ frameBuffer = new StringSink();
104
+ prevGrid = null;
105
+ prevCursorPosition = null;
106
+ constructor(stdin = process.stdin, stdout = process.stdout, options) {
107
+ this.stdin = stdin;
108
+ this.stdout = stdout;
109
+ this.resizeThrottleMs = options?.resizeThrottleMs ?? 100;
110
+ this.isTmux = isInsideTmux();
111
+ this.renderer = new TerminalRenderer(this.frameBuffer);
112
+ }
113
+ /**
114
+ * Write a raw escape sequence to stdout, wrapping in TMUX passthrough if needed.
115
+ *
116
+ * ВАЖНО: passthrough применять ТОЛЬКО к последовательностям, которые tmux НЕ понимает
117
+ * и которые должны дойти до внешнего терминала напрямую (например, OSC 52 — буфер обмена).
118
+ *
119
+ * Последовательности режимов ВВОДА (Kitty keyboard protocol `CSI > u`/`CSI < u`/`CSI ? u`,
120
+ * mouse tracking `?1000/1002/1003/1006`) tmux понимает и отслеживает ПОПАНЕЛЬНО — их нужно
121
+ * слать напрямую (writeDirect), иначе passthrough включит режим на внешнем терминале глобально,
122
+ * и кодировка клавиш «протечёт» во все панели tmux (Ctrl+S перестаёт быть префиксом, а в других
123
+ * вкладках появляются `[115;5u`-подобные символы).
124
+ */
125
+ writePassthrough(sequence) {
126
+ this.stdout.write(this.isTmux ? wrapForTmux(sequence) : sequence);
127
+ }
128
+ /**
129
+ * Write a sequence straight to stdout without TMUX passthrough. Use for input-mode
130
+ * sequences (Kitty keyboard protocol, mouse tracking) so tmux manages them per-pane.
131
+ */
132
+ writeDirect(sequence) {
133
+ this.stdout.write(sequence);
134
+ }
135
+ /**
136
+ * Write an OSC or other escape sequence to the terminal.
137
+ * TMUX passthrough is applied automatically when running inside TMUX.
138
+ */
139
+ writeOscSequence(sequence) {
140
+ this.writePassthrough(sequence);
141
+ }
142
+ onInput(callback) {
143
+ this.inputCallbacks.push(callback);
144
+ }
145
+ onMouse(callback) {
146
+ this.mouseCallbacks.push(callback);
147
+ }
148
+ onPaste(callback) {
149
+ this.pasteCallbacks.push(callback);
150
+ }
151
+ onResize(callback) {
152
+ this.resizeCallbacks.push(callback);
153
+ }
154
+ onOscResponse(callback) {
155
+ this.oscResponseCallbacks.push(callback);
156
+ }
157
+ probeKeyboardProtocol(onResult) {
158
+ let settled = false;
159
+ let supported = false;
160
+ let timer = null;
161
+ const finish = () => {
162
+ if (settled)
163
+ return;
164
+ settled = true;
165
+ if (timer !== null)
166
+ clearTimeout(timer);
167
+ onResult(supported);
168
+ };
169
+ this.deviceReportCallbacks.push((report) => {
170
+ if (settled)
171
+ return;
172
+ if (report === "kitty-flags")
173
+ supported = true;
174
+ else
175
+ finish(); // DA1 reply = all replies are in
176
+ });
177
+ // Direct (not passthrough): the reply must reflect tmux's own Kitty support, per-pane.
178
+ this.writeDirect(KITTY_FLAGS_QUERY + DA1_QUERY);
179
+ timer = setTimeout(finish, KEYBOARD_PROBE_TIMEOUT_MS);
180
+ }
181
+ /** Fan a parsed input batch out to the registered callbacks. */
182
+ dispatchInput(result) {
183
+ for (const event of result.keys) {
184
+ for (const cb of this.inputCallbacks)
185
+ cb(event);
186
+ }
187
+ for (const mouseToken of result.mouse) {
188
+ for (const cb of this.mouseCallbacks)
189
+ cb(mouseToken);
190
+ }
191
+ for (const text of result.paste) {
192
+ for (const cb of this.pasteCallbacks)
193
+ cb(text);
194
+ }
195
+ for (const oscToken of result.osc) {
196
+ for (const cb of this.oscResponseCallbacks)
197
+ cb(oscToken.code, oscToken.data);
198
+ }
199
+ for (const report of result.deviceReports) {
200
+ for (const cb of this.deviceReportCallbacks)
201
+ cb(report.report, report.params);
202
+ }
203
+ }
204
+ /** If a partial sequence is buffered, flush it after a short grace period (the rest never came). */
205
+ schedulePartialInputFlush() {
206
+ if (!this.inputParser.hasPending())
207
+ return;
208
+ this.partialInputTimer = setTimeout(() => {
209
+ this.partialInputTimer = null;
210
+ this.dispatchInput(this.inputParser.flush());
211
+ }, PARTIAL_INPUT_FLUSH_MS);
212
+ }
213
+ clearPartialInputTimer() {
214
+ if (this.partialInputTimer !== null) {
215
+ clearTimeout(this.partialInputTimer);
216
+ this.partialInputTimer = null;
217
+ }
218
+ }
219
+ /** Emit resize only if dimensions actually changed */
220
+ emitResize() {
221
+ const size = this.getSize();
222
+ if (this.lastEmittedSize !== null &&
223
+ this.lastEmittedSize.width === size.width &&
224
+ this.lastEmittedSize.height === size.height) {
225
+ return;
226
+ }
227
+ this.lastEmittedSize = size;
228
+ for (const cb of this.resizeCallbacks) {
229
+ cb(size);
230
+ }
231
+ }
232
+ renderFrame(grid, cursorPosition) {
233
+ this.prevGrid ??= new Grid(grid.size);
234
+ const sizeChanged = this.prevGrid.width !== grid.width || this.prevGrid.height !== grid.height;
235
+ if (sizeChanged) {
236
+ this.prevGrid = new Grid(grid.size);
237
+ }
238
+ // Diff into a buffer (not straight to stdout) so we know whether anything changed.
239
+ this.frameBuffer.reset();
240
+ this.renderer.render(grid, this.prevGrid);
241
+ const body = this.frameBuffer.value;
242
+ const cursorChanged = !samePoint(cursorPosition, this.prevCursorPosition);
243
+ // Nothing to repaint and the cursor hasn't moved → emit absolutely nothing.
244
+ // Without this, every mouse-move event re-sent the frame wrapper (hide/show
245
+ // cursor + reposition), making the terminal cursor flicker on plain motion.
246
+ if (!sizeChanged && body.length === 0 && !cursorChanged) {
247
+ return;
248
+ }
249
+ let out = "\x1b[?2026h"; // begin synchronized output
250
+ if (sizeChanged) {
251
+ out += "\x1b[2J"; // clear screen to avoid stale reflow artifacts
252
+ }
253
+ // Hide the cursor while we paint cells, or when it should be hidden outright.
254
+ if (body.length > 0 || cursorPosition === null) {
255
+ out += "\x1b[?25l"; // hide cursor
256
+ }
257
+ out += body;
258
+ if (cursorPosition !== null) {
259
+ out += `\x1b[${(cursorPosition.y + 1).toString()};${(cursorPosition.x + 1).toString()}H`; // position cursor
260
+ out += "\x1b[?25h"; // show cursor
261
+ }
262
+ out += "\x1b[?2026l"; // end synchronized output
263
+ this.stdout.write(out);
264
+ this.prevCursorPosition = cursorPosition;
265
+ }
266
+ getSize() {
267
+ return new Size(this.stdout.columns, this.stdout.rows);
268
+ }
269
+ setup() {
270
+ // Switch to alternate screen buffer
271
+ this.stdout.write("\x1b[?1049h");
272
+ // Show cursor (will be positioned by the focused element)
273
+ this.stdout.write("\x1b[?25h");
274
+ // Enable Kitty Keyboard Protocol — direct, so tmux tracks it per-pane (no passthrough)
275
+ this.writeDirect(KITTY_ENABLE);
276
+ // Also enable xterm modifyOtherKeys — tmux ignores the Kitty push but honors this,
277
+ // delivering Ctrl+Tab / Ctrl+Shift+<key> as CSI-u. Direct, per-pane.
278
+ this.writeDirect(MODIFY_OTHER_KEYS_ENABLE);
279
+ // Enable mouse tracking (all-motion mode for hover/enter/leave) — direct, per-pane
280
+ this.writeDirect(MOUSE_TRACKING_ALL_ENABLE);
281
+ // Enable bracketed paste so pastes arrive as one text block — direct, per-pane
282
+ this.writeDirect(BRACKETED_PASTE_ENABLE);
283
+ // Raw mode for character-by-character input
284
+ this.stdin.setRawMode(true);
285
+ this.stdin.setEncoding("utf8");
286
+ this.stdin.resume();
287
+ // Listen for input
288
+ this.onDataHandler = (chunk) => {
289
+ // A new chunk supersedes any buffered tail (it gets prepended inside the parser),
290
+ // so cancel a pending flush; reschedule below if a tail still remains afterward.
291
+ this.clearPartialInputTimer();
292
+ this.dispatchInput(this.inputParser.parseWithMouse(chunk));
293
+ this.schedulePartialInputFlush();
294
+ };
295
+ this.stdin.on("data", this.onDataHandler);
296
+ // Listen for terminal resize (throttled + deduplicated)
297
+ this.onResizeHandler = () => {
298
+ if (this.resizeThrottleTimer !== null) {
299
+ // Already throttling — just mark that a new resize arrived
300
+ this.resizePending = true;
301
+ return;
302
+ }
303
+ this.emitResize();
304
+ this.resizeThrottleTimer = setTimeout(() => {
305
+ this.resizeThrottleTimer = null;
306
+ if (this.resizePending) {
307
+ this.resizePending = false;
308
+ this.emitResize();
309
+ }
310
+ }, this.resizeThrottleMs);
311
+ };
312
+ this.stdout.on("resize", this.onResizeHandler);
313
+ // Cleanup on exit/SIGINT
314
+ const onExit = () => {
315
+ this.teardown();
316
+ };
317
+ const onSigint = () => {
318
+ this.teardown();
319
+ process.exit(0);
320
+ };
321
+ process.on("exit", onExit);
322
+ process.on("SIGINT", onSigint);
323
+ this.cleanupHandlers.push(() => {
324
+ process.removeListener("exit", onExit);
325
+ process.removeListener("SIGINT", onSigint);
326
+ });
327
+ }
328
+ teardown() {
329
+ // Disable Kitty Keyboard Protocol — direct (matches the per-pane enable in setup)
330
+ this.writeDirect(KITTY_DISABLE);
331
+ // Reset modifyOtherKeys — direct (matches the per-pane enable in setup)
332
+ this.writeDirect(MODIFY_OTHER_KEYS_DISABLE);
333
+ // Disable mouse tracking — direct
334
+ this.writeDirect(MOUSE_TRACKING_DISABLE);
335
+ // Disable bracketed paste — direct (matches the per-pane enable in setup)
336
+ this.writeDirect(BRACKETED_PASTE_DISABLE);
337
+ // Restore cursor
338
+ this.stdout.write("\x1b[?25h");
339
+ // Restore normal screen buffer
340
+ this.stdout.write("\x1b[?1049l");
341
+ // Remove stdin listener
342
+ if (this.onDataHandler) {
343
+ this.stdin.removeListener("data", this.onDataHandler);
344
+ this.onDataHandler = null;
345
+ }
346
+ this.clearPartialInputTimer();
347
+ // Cancel pending throttle and remove resize listener
348
+ if (this.resizeThrottleTimer !== null) {
349
+ clearTimeout(this.resizeThrottleTimer);
350
+ this.resizeThrottleTimer = null;
351
+ }
352
+ this.resizePending = false;
353
+ if (this.onResizeHandler) {
354
+ this.stdout.removeListener("resize", this.onResizeHandler);
355
+ this.onResizeHandler = null;
356
+ }
357
+ // Remove process listeners
358
+ for (const cleanup of this.cleanupHandlers) {
359
+ cleanup();
360
+ }
361
+ this.cleanupHandlers = [];
362
+ }
363
+ }
364
+ /** In-memory sink that accumulates writes into a string. Implements WritableOutput. */
365
+ class StringSink {
366
+ value = "";
367
+ write(data) {
368
+ this.value += data;
369
+ }
370
+ reset() {
371
+ this.value = "";
372
+ }
373
+ }
374
+ function samePoint(a, b) {
375
+ if (a === null || b === null)
376
+ return a === b;
377
+ return a.x === b.x && a.y === b.y;
378
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared predicates over the process environment, used by both the terminal
3
+ * backend and the terminal-environment detector. Single source of truth so the
4
+ * two layers can't disagree about "are we in tmux / over ssh".
5
+ */
6
+ /** True when running inside a TMUX session. */
7
+ export declare function isInsideTmux(env?: NodeJS.ProcessEnv): boolean;
8
+ /** True when the session is reached over SSH. */
9
+ export declare function isSsh(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared predicates over the process environment, used by both the terminal
3
+ * backend and the terminal-environment detector. Single source of truth so the
4
+ * two layers can't disagree about "are we in tmux / over ssh".
5
+ */
6
+ /** True when running inside a TMUX session. */
7
+ export function isInsideTmux(env = process.env) {
8
+ return env.TMUX != null && env.TMUX !== "";
9
+ }
10
+ /** True when the session is reached over SSH. */
11
+ export function isSsh(env = process.env) {
12
+ return (env.SSH_CONNECTION != null && env.SSH_CONNECTION !== "") || (env.SSH_TTY != null && env.SSH_TTY !== "");
13
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@tuidom/terminal-backend",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Experimental terminal UI DOM — real terminal backend for Node (tty, ANSI diffing, Kitty protocol). API is unstable.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tuidom/tuidom.git",
10
+ "directory": "packages/terminal-backend"
11
+ },
12
+ "engines": {
13
+ "node": ">=24.0.0"
14
+ },
15
+ "keywords": [
16
+ "tui",
17
+ "terminal",
18
+ "ui",
19
+ "dom",
20
+ "backend"
21
+ ],
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ "./*.js": "./dist/*.js",
28
+ "./*": "./dist/*.js"
29
+ },
30
+ "dependencies": {
31
+ "@tuidom/core": "0.1.0"
32
+ }
33
+ }