@tuidom/headless-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,72 @@
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 { type SerializeMouseInit } from "@tuidom/core/input/serializeMouse";
6
+ import type { Grid } from "@tuidom/core/rendering/grid";
7
+ import { type GridSnapshot } from "@tuidom/core/rendering/gridSnapshot";
8
+ /**
9
+ * Terminal backend that runs the real application without a real terminal.
10
+ *
11
+ * Unlike {@link import("@tuidom/terminal-backend/nodeTerminalBackend").NodeTerminalBackend} it never
12
+ * touches stdin/stdout: `setup`/`teardown` are no-ops and rendered frames are
13
+ * captured into a {@link GridSnapshot} instead of being written as ANSI. Input is
14
+ * injected the same way {@link import("@tuidom/testing/mockTerminalBackend").MockTerminalBackend}
15
+ * does it — through the real {@link KeyInputParser} and {@link serializeKey} DSL —
16
+ * so the app sees byte-for-byte the same `KeyPressEvent`s a terminal would produce.
17
+ *
18
+ * Drives the `--headless` mode: the inspector exposes {@link sendKey} /
19
+ * {@link captureFrame} over WebSocket so a client can script the editor and read
20
+ * back the screen for image rendering.
21
+ */
22
+ export declare class HeadlessCaptureBackend implements ITerminalBackend {
23
+ private readonly inputCallbacks;
24
+ private readonly mouseCallbacks;
25
+ private readonly pasteCallbacks;
26
+ private readonly resizeCallbacks;
27
+ private readonly oscResponseCallbacks;
28
+ private readonly inputParser;
29
+ private size;
30
+ private lastFrame;
31
+ constructor(size?: Size);
32
+ onInput(callback: (event: KeyPressEvent) => void): void;
33
+ onMouse(callback: (event: MouseToken) => void): void;
34
+ onPaste(callback: (text: string) => void): void;
35
+ onResize(callback: (size: Size) => void): void;
36
+ onOscResponse(callback: (code: number, data: string) => void): void;
37
+ /**
38
+ * There is no terminal to probe. We never resolve the callback: leaving the
39
+ * probe pending is harmless (it is fire-and-forget) and keeps capabilities at
40
+ * their deterministic env-derived baseline, which matters for reproducible
41
+ * screenshots.
42
+ */
43
+ probeKeyboardProtocol(_onResult: (supported: boolean) => void): void;
44
+ renderFrame(grid: Grid, cursorPosition: Point | null): void;
45
+ getSize(): Size;
46
+ setup(): void;
47
+ teardown(): void;
48
+ /** OSC output (e.g. clipboard writes) has nowhere to go in headless mode. */
49
+ writeOscSequence(_sequence: string): void;
50
+ /**
51
+ * Inject a key using the human-readable DSL (`"a"`, `"Enter"`, `"Ctrl+P"`).
52
+ * Goes through the same parser as real terminal input.
53
+ */
54
+ sendKey(name: string): void;
55
+ /** Inject a bracketed paste: `text` is delivered to paste subscribers as one block. */
56
+ sendPaste(text: string): void;
57
+ /** Inject raw terminal bytes (escape sequences etc.) for edge cases. */
58
+ sendRaw(data: string): void;
59
+ /** Inject a ready-made mouse token straight to subscribers, bypassing the parser. */
60
+ simulateMouse(token: MouseToken): void;
61
+ /**
62
+ * Inject a mouse event through the real parser: it is encoded as an SGR (1006)
63
+ * sequence and tokenized back, exactly like input from a terminal. Coordinates
64
+ * are 1-based, as {@link MouseToken} carries them.
65
+ */
66
+ sendMouse(init: SerializeMouseInit): void;
67
+ /** Resize the virtual terminal and notify the application. */
68
+ resize(size: Size): void;
69
+ /** The most recently rendered frame as plain data. */
70
+ captureFrame(): GridSnapshot;
71
+ private emitStreams;
72
+ }
@@ -0,0 +1,132 @@
1
+ import { Point, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { KeyInputParser } from "@tuidom/core/input/keyInputParser";
3
+ import { serializeKey } from "@tuidom/core/input/serializeKey";
4
+ import { serializeMouse } from "@tuidom/core/input/serializeMouse";
5
+ import { emptyGridSnapshot, snapshotGrid } from "@tuidom/core/rendering/gridSnapshot";
6
+ /**
7
+ * Terminal backend that runs the real application without a real terminal.
8
+ *
9
+ * Unlike {@link import("@tuidom/terminal-backend/nodeTerminalBackend").NodeTerminalBackend} it never
10
+ * touches stdin/stdout: `setup`/`teardown` are no-ops and rendered frames are
11
+ * captured into a {@link GridSnapshot} instead of being written as ANSI. Input is
12
+ * injected the same way {@link import("@tuidom/testing/mockTerminalBackend").MockTerminalBackend}
13
+ * does it — through the real {@link KeyInputParser} and {@link serializeKey} DSL —
14
+ * so the app sees byte-for-byte the same `KeyPressEvent`s a terminal would produce.
15
+ *
16
+ * Drives the `--headless` mode: the inspector exposes {@link sendKey} /
17
+ * {@link captureFrame} over WebSocket so a client can script the editor and read
18
+ * back the screen for image rendering.
19
+ */
20
+ export class HeadlessCaptureBackend {
21
+ inputCallbacks = [];
22
+ mouseCallbacks = [];
23
+ pasteCallbacks = [];
24
+ resizeCallbacks = [];
25
+ oscResponseCallbacks = [];
26
+ inputParser = new KeyInputParser();
27
+ size;
28
+ lastFrame;
29
+ constructor(size = new Size(120, 32)) {
30
+ this.size = size;
31
+ this.lastFrame = emptyGridSnapshot(size.width, size.height);
32
+ }
33
+ // ─── ITerminalBackend ───
34
+ onInput(callback) {
35
+ this.inputCallbacks.push(callback);
36
+ }
37
+ onMouse(callback) {
38
+ this.mouseCallbacks.push(callback);
39
+ }
40
+ onPaste(callback) {
41
+ this.pasteCallbacks.push(callback);
42
+ }
43
+ onResize(callback) {
44
+ this.resizeCallbacks.push(callback);
45
+ }
46
+ onOscResponse(callback) {
47
+ this.oscResponseCallbacks.push(callback);
48
+ }
49
+ /**
50
+ * There is no terminal to probe. We never resolve the callback: leaving the
51
+ * probe pending is harmless (it is fire-and-forget) and keeps capabilities at
52
+ * their deterministic env-derived baseline, which matters for reproducible
53
+ * screenshots.
54
+ */
55
+ probeKeyboardProtocol(_onResult) {
56
+ // intentionally never resolved — see doc comment
57
+ }
58
+ renderFrame(grid, cursorPosition) {
59
+ this.lastFrame = snapshotGrid(grid, cursorPosition);
60
+ }
61
+ getSize() {
62
+ return this.size;
63
+ }
64
+ setup() {
65
+ // No terminal to initialize.
66
+ }
67
+ teardown() {
68
+ // No terminal to restore.
69
+ }
70
+ /** OSC output (e.g. clipboard writes) has nowhere to go in headless mode. */
71
+ writeOscSequence(_sequence) {
72
+ // No-op: no terminal to receive the sequence.
73
+ }
74
+ // ─── Driver surface (used by the inspector in headless mode) ───
75
+ /**
76
+ * Inject a key using the human-readable DSL (`"a"`, `"Enter"`, `"Ctrl+P"`).
77
+ * Goes through the same parser as real terminal input.
78
+ */
79
+ sendKey(name) {
80
+ this.sendRaw(serializeKey(name));
81
+ // serializeKey always yields a complete sequence, so drain any tail a lone
82
+ // ESC ("Escape") would otherwise leave buffered awaiting a continuation.
83
+ if (this.inputParser.hasPending()) {
84
+ this.emitStreams(this.inputParser.flush());
85
+ }
86
+ }
87
+ /** Inject a bracketed paste: `text` is delivered to paste subscribers as one block. */
88
+ sendPaste(text) {
89
+ this.sendRaw(`\x1b[200~${text}\x1b[201~`);
90
+ }
91
+ /** Inject raw terminal bytes (escape sequences etc.) for edge cases. */
92
+ sendRaw(data) {
93
+ this.emitStreams(this.inputParser.parseWithMouse(data));
94
+ }
95
+ /** Inject a ready-made mouse token straight to subscribers, bypassing the parser. */
96
+ simulateMouse(token) {
97
+ for (const cb of this.mouseCallbacks)
98
+ cb(token);
99
+ }
100
+ /**
101
+ * Inject a mouse event through the real parser: it is encoded as an SGR (1006)
102
+ * sequence and tokenized back, exactly like input from a terminal. Coordinates
103
+ * are 1-based, as {@link MouseToken} carries them.
104
+ */
105
+ sendMouse(init) {
106
+ this.sendRaw(serializeMouse(init));
107
+ }
108
+ /** Resize the virtual terminal and notify the application. */
109
+ resize(size) {
110
+ this.size = size;
111
+ for (const cb of this.resizeCallbacks)
112
+ cb(size);
113
+ }
114
+ /** The most recently rendered frame as plain data. */
115
+ captureFrame() {
116
+ return this.lastFrame;
117
+ }
118
+ emitStreams(streams) {
119
+ for (const event of streams.keys) {
120
+ for (const cb of this.inputCallbacks)
121
+ cb(event);
122
+ }
123
+ for (const token of streams.mouse) {
124
+ for (const cb of this.mouseCallbacks)
125
+ cb(token);
126
+ }
127
+ for (const text of streams.paste) {
128
+ for (const cb of this.pasteCallbacks)
129
+ cb(text);
130
+ }
131
+ }
132
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@tuidom/headless-backend",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Experimental terminal UI DOM — headless capture backend (in-memory frames for screenshots/e2e). API is unstable.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tuidom/tuidom.git",
10
+ "directory": "packages/headless-backend"
11
+ },
12
+ "engines": {
13
+ "node": ">=24.0.0"
14
+ },
15
+ "keywords": [
16
+ "tui",
17
+ "terminal",
18
+ "ui",
19
+ "dom",
20
+ "headless"
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
+ }