@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, PROBE_TIMEOUT_MS);
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) {
@@ -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;
@@ -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.6.1",
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.6.1"
31
+ "@tuidom/core": "0.7.0"
32
32
  }
33
33
  }