@tuidom/terminal-backend 0.6.1 → 0.7.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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ITerminalBackend } from "@tuidom/core/backend/iTerminalBackend";
|
|
1
|
+
import type { HostTerminalColors, ITerminalBackend } from "@tuidom/core/backend/iTerminalBackend";
|
|
2
2
|
import { Point, Size } from "@tuidom/core/common/geometryPromitives";
|
|
3
3
|
import type { KeyPressEvent } from "@tuidom/core/input/keyEvent";
|
|
4
4
|
import type { MouseToken } from "@tuidom/core/input/rawTerminalToken";
|
|
@@ -30,6 +30,7 @@ export declare class NodeTerminalBackend implements ITerminalBackend {
|
|
|
30
30
|
private resizePending;
|
|
31
31
|
private cleanupHandlers;
|
|
32
32
|
private readonly isTmux;
|
|
33
|
+
private readonly isGnuScreen;
|
|
33
34
|
private readonly inputParser;
|
|
34
35
|
private readonly renderer;
|
|
35
36
|
/**
|
|
@@ -73,11 +74,13 @@ export declare class NodeTerminalBackend implements ITerminalBackend {
|
|
|
73
74
|
onOscResponse(callback: (code: number, data: string) => void): void;
|
|
74
75
|
probeKeyboardProtocol(onResult: (supported: boolean) => void): void;
|
|
75
76
|
probeTerminalVersion(onResult: (nameAndVersion: string | undefined) => void): void;
|
|
77
|
+
probeHostColors(onResult: (colors: HostTerminalColors) => void): void;
|
|
76
78
|
/**
|
|
77
79
|
* Send `query` followed by DA1 and collect device reports until *our* DA1 reply
|
|
78
80
|
* arrives (or the timeout fires), then call `finish` exactly once. Replies come back
|
|
79
81
|
* in request order, so the n-th DA1 reply closes the n-th probe — two probes in
|
|
80
|
-
* flight don't settle each other early.
|
|
82
|
+
* flight don't settle each other early. `untilDa1: false` leaves only the timeout
|
|
83
|
+
* (and the returned early-settle function) to close the probe.
|
|
81
84
|
*/
|
|
82
85
|
private probe;
|
|
83
86
|
/** Fan a parsed input batch out to the registered callbacks. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { parseXColor } from "@tuidom/core/common/colorUtils";
|
|
1
2
|
import { Point, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
3
|
import { KeyInputParser } from "@tuidom/core/input/keyInputParser";
|
|
3
4
|
import { MOUSE_TRACKING_ALL_ENABLE, MOUSE_TRACKING_DISABLE } from "@tuidom/core/input/mouseTracking";
|
|
4
5
|
import { Grid } from "@tuidom/core/rendering/grid";
|
|
5
6
|
import { TerminalRenderer } from "@tuidom/core/rendering/terminalRenderer";
|
|
6
|
-
import { isInsideTmux } from "./terminalEnv.js";
|
|
7
|
+
import { isInsideGnuScreen, isInsideTmux } from "./terminalEnv.js";
|
|
7
8
|
/**
|
|
8
9
|
* Kitty Keyboard Protocol escape sequences.
|
|
9
10
|
*
|
|
@@ -52,6 +53,23 @@ const PROBE_TIMEOUT_MS = 200;
|
|
|
52
53
|
* silent on the first query but always answer DA1.
|
|
53
54
|
*/
|
|
54
55
|
const XTVERSION_QUERY = "\x1b[>0q";
|
|
56
|
+
/** How many ANSI palette entries the host-colors probe asks for (0..15). */
|
|
57
|
+
const ANSI_COLOR_COUNT = 16;
|
|
58
|
+
/**
|
|
59
|
+
* Host-colors probe: default fg/bg (OSC 10/11) and the 16 ANSI entries (OSC 4), each
|
|
60
|
+
* entry its own query — Konsole answers only the first index of a multi-index OSC 4,
|
|
61
|
+
* and tmux drops a palette reply for an index it didn't expect. ST-terminated; replies
|
|
62
|
+
* may end with BEL or ST either way (the tokenizer takes both).
|
|
63
|
+
*/
|
|
64
|
+
const HOST_COLORS_QUERY = "\x1b]10;?\x1b\\\x1b]11;?\x1b\\" +
|
|
65
|
+
Array.from({ length: ANSI_COLOR_COUNT }, (_, index) => `\x1b]4;${String(index)};?\x1b\\`).join("");
|
|
66
|
+
/**
|
|
67
|
+
* Inside tmux DA1 is no sentinel for the host-colors probe: tmux answers DA1 at once,
|
|
68
|
+
* but a palette entry it hasn't set itself it forwards to the outer terminal and relays
|
|
69
|
+
* the reply later (tmux ≥ 3.6; its own wait is 500 ms). There we settle on the last
|
|
70
|
+
* expected reply or on this timeout.
|
|
71
|
+
*/
|
|
72
|
+
const TMUX_HOST_COLORS_TIMEOUT_MS = 600;
|
|
55
73
|
/**
|
|
56
74
|
* How long to hold a partial escape sequence that arrived at the end of a stdin read,
|
|
57
75
|
* waiting for the rest to follow (it does, on a split keypress over SSH/tmux). If nothing
|
|
@@ -75,6 +93,25 @@ function wrapForTmux(sequence) {
|
|
|
75
93
|
const escaped = sequence.replace(/\x1b/g, "\x1b\x1b");
|
|
76
94
|
return `\x1bPtmux;${escaped}\x1b\\`;
|
|
77
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* A reply to the host-colors probe: `OSC 10;<color>` / `OSC 11;<color>` (fg/bg) or
|
|
98
|
+
* `OSC 4;<index>;<color>` for one of the 16 ANSI entries. Anything else (OSC 52, a
|
|
99
|
+
* palette index past 15, an unparsable color) is `undefined` — not ours to count.
|
|
100
|
+
*/
|
|
101
|
+
function parseHostColorReply(code, data) {
|
|
102
|
+
if (code === 10 || code === 11) {
|
|
103
|
+
const color = parseXColor(data);
|
|
104
|
+
return color === undefined ? undefined : { slot: code === 10 ? "foreground" : "background", color };
|
|
105
|
+
}
|
|
106
|
+
if (code !== 4)
|
|
107
|
+
return undefined;
|
|
108
|
+
const match = /^(\d+);(.*)$/.exec(data);
|
|
109
|
+
if (match === null)
|
|
110
|
+
return undefined;
|
|
111
|
+
const index = Number(match[1]);
|
|
112
|
+
const color = parseXColor(match[2]);
|
|
113
|
+
return index < ANSI_COLOR_COUNT && color !== undefined ? { slot: index, color } : undefined;
|
|
114
|
+
}
|
|
78
115
|
/**
|
|
79
116
|
* Real terminal backend: reads from process.stdin, writes to process.stdout.
|
|
80
117
|
* Handles alternate screen, raw mode, cursor visibility, signal cleanup.
|
|
@@ -102,6 +139,7 @@ export class NodeTerminalBackend {
|
|
|
102
139
|
resizePending = false;
|
|
103
140
|
cleanupHandlers = [];
|
|
104
141
|
isTmux;
|
|
142
|
+
isGnuScreen;
|
|
105
143
|
inputParser = new KeyInputParser();
|
|
106
144
|
renderer;
|
|
107
145
|
/**
|
|
@@ -117,6 +155,7 @@ export class NodeTerminalBackend {
|
|
|
117
155
|
this.stdout = stdout;
|
|
118
156
|
this.resizeThrottleMs = options?.resizeThrottleMs ?? 100;
|
|
119
157
|
this.isTmux = isInsideTmux();
|
|
158
|
+
this.isGnuScreen = isInsideGnuScreen();
|
|
120
159
|
this.renderer = new TerminalRenderer(this.frameBuffer);
|
|
121
160
|
}
|
|
122
161
|
/**
|
|
@@ -183,13 +222,53 @@ export class NodeTerminalBackend {
|
|
|
183
222
|
onResult(nameAndVersion);
|
|
184
223
|
});
|
|
185
224
|
}
|
|
225
|
+
probeHostColors(onResult) {
|
|
226
|
+
const ansi = new Array(ANSI_COLOR_COUNT).fill(undefined);
|
|
227
|
+
let foreground;
|
|
228
|
+
let background;
|
|
229
|
+
const finish = () => {
|
|
230
|
+
onResult({ foreground, background, ansi });
|
|
231
|
+
};
|
|
232
|
+
// GNU Screen doesn't answer color queries and doesn't pass them on — don't send them.
|
|
233
|
+
if (this.isGnuScreen) {
|
|
234
|
+
finish();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let settled = false;
|
|
238
|
+
// Distinct slots: a repeated reply for the same entry doesn't bring the end closer.
|
|
239
|
+
const reported = new Set();
|
|
240
|
+
// Direct, like the other probes: inside tmux it's tmux that answers for its pane.
|
|
241
|
+
const settle = this.probe(HOST_COLORS_QUERY, () => {
|
|
242
|
+
/* only OSC replies matter here */
|
|
243
|
+
}, () => {
|
|
244
|
+
settled = true;
|
|
245
|
+
finish();
|
|
246
|
+
}, this.isTmux ? { untilDa1: false, timeoutMs: TMUX_HOST_COLORS_TIMEOUT_MS } : undefined);
|
|
247
|
+
this.oscResponseCallbacks.push((code, data) => {
|
|
248
|
+
if (settled)
|
|
249
|
+
return;
|
|
250
|
+
const reply = parseHostColorReply(code, data);
|
|
251
|
+
if (reply === undefined)
|
|
252
|
+
return;
|
|
253
|
+
if (reply.slot === "foreground")
|
|
254
|
+
foreground = reply.color;
|
|
255
|
+
else if (reply.slot === "background")
|
|
256
|
+
background = reply.color;
|
|
257
|
+
else
|
|
258
|
+
ansi[reply.slot] = reply.color;
|
|
259
|
+
reported.add(reply.slot);
|
|
260
|
+
if (reported.size === 2 + ANSI_COLOR_COUNT)
|
|
261
|
+
settle();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
186
264
|
/**
|
|
187
265
|
* Send `query` followed by DA1 and collect device reports until *our* DA1 reply
|
|
188
266
|
* arrives (or the timeout fires), then call `finish` exactly once. Replies come back
|
|
189
267
|
* in request order, so the n-th DA1 reply closes the n-th probe — two probes in
|
|
190
|
-
* flight don't settle each other early.
|
|
268
|
+
* flight don't settle each other early. `untilDa1: false` leaves only the timeout
|
|
269
|
+
* (and the returned early-settle function) to close the probe.
|
|
191
270
|
*/
|
|
192
|
-
probe(query, onReport, finish) {
|
|
271
|
+
probe(query, onReport, finish, options = { untilDa1: true, timeoutMs: PROBE_TIMEOUT_MS }) {
|
|
193
272
|
const myDa1 = ++this.da1Sent;
|
|
194
273
|
let settled = false;
|
|
195
274
|
let timer = null;
|
|
@@ -206,11 +285,12 @@ export class NodeTerminalBackend {
|
|
|
206
285
|
return;
|
|
207
286
|
if (report !== "da1")
|
|
208
287
|
onReport(report, params);
|
|
209
|
-
else if (this.da1Received >= myDa1)
|
|
288
|
+
else if (options.untilDa1 && this.da1Received >= myDa1)
|
|
210
289
|
settle(); // our DA1 reply = all replies are in
|
|
211
290
|
});
|
|
212
291
|
this.writeDirect(query + DA1_QUERY);
|
|
213
|
-
timer = setTimeout(settle,
|
|
292
|
+
timer = setTimeout(settle, options.timeoutMs);
|
|
293
|
+
return settle;
|
|
214
294
|
}
|
|
215
295
|
/** Fan a parsed input batch out to the registered callbacks. */
|
|
216
296
|
dispatchInput(result) {
|
package/dist/terminalEnv.d.ts
CHANGED
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** True when running inside a TMUX session. */
|
|
7
7
|
export declare function isInsideTmux(env?: NodeJS.ProcessEnv): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True when the nearest multiplexer is GNU Screen (`STY` set, no tmux in between):
|
|
10
|
+
* it neither answers color queries nor passes them on.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isInsideGnuScreen(env?: NodeJS.ProcessEnv): boolean;
|
|
8
13
|
/** True when the session is reached over SSH. */
|
|
9
14
|
export declare function isSsh(env?: NodeJS.ProcessEnv): boolean;
|
package/dist/terminalEnv.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
export function isInsideTmux(env = process.env) {
|
|
8
8
|
return env.TMUX != null && env.TMUX !== "";
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* True when the nearest multiplexer is GNU Screen (`STY` set, no tmux in between):
|
|
12
|
+
* it neither answers color queries nor passes them on.
|
|
13
|
+
*/
|
|
14
|
+
export function isInsideGnuScreen(env = process.env) {
|
|
15
|
+
return env.STY != null && env.STY !== "" && !isInsideTmux(env);
|
|
16
|
+
}
|
|
10
17
|
/** True when the session is reached over SSH. */
|
|
11
18
|
export function isSsh(env = process.env) {
|
|
12
19
|
return (env.SSH_CONNECTION != null && env.SSH_CONNECTION !== "") || (env.SSH_TTY != null && env.SSH_TTY !== "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuidom/terminal-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Experimental terminal UI DOM — real terminal backend for Node (tty, ANSI diffing, Kitty protocol). API is unstable.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"./*": "./dist/*.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@tuidom/core": "0.
|
|
31
|
+
"@tuidom/core": "0.7.0"
|
|
32
32
|
}
|
|
33
33
|
}
|