@promptctl/rich-js 0.27.0 → 0.29.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.
- package/README.md +7 -6
- package/THIRD-PARTY-NOTICES +42 -0
- package/dist/core/animation.d.ts +65 -46
- package/dist/core/animation.js +91 -67
- package/dist/core/ansi.d.ts +19 -12
- package/dist/core/ansi.js +114 -36
- package/dist/core/bidi-data.d.ts +14 -0
- package/dist/core/bidi-data.js +385 -0
- package/dist/core/bidi.d.ts +89 -0
- package/dist/core/bidi.js +615 -0
- package/dist/core/box.d.ts +9 -1
- package/dist/core/box.js +11 -0
- package/dist/core/cell-widths.d.ts +3 -2
- package/dist/core/cell-widths.js +93 -93
- package/dist/core/cells.d.ts +20 -0
- package/dist/core/cells.js +94 -6
- package/dist/core/clock.d.ts +1 -1
- package/dist/core/color.d.ts +56 -57
- package/dist/core/color.js +79 -81
- package/dist/core/console.d.ts +24 -7
- package/dist/core/console.js +27 -8
- package/dist/core/control-codes.d.ts +61 -0
- package/dist/core/control-codes.js +157 -0
- package/dist/core/easing.d.ts +40 -40
- package/dist/core/easing.js +40 -40
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.js +9 -1
- package/dist/core/export-html.js +5 -4
- package/dist/core/export-lines.d.ts +13 -11
- package/dist/core/export-lines.js +19 -15
- package/dist/core/export-svg.d.ts +3 -3
- package/dist/core/export-svg.js +10 -9
- package/dist/core/json.js +9 -2
- package/dist/core/markup.js +5 -4
- package/dist/core/memo.d.ts +1 -1
- package/dist/core/memo.js +1 -1
- package/dist/core/oklch.d.ts +14 -14
- package/dist/core/oklch.js +10 -10
- package/dist/core/orient.d.ts +68 -0
- package/dist/core/orient.js +224 -0
- package/dist/core/osc8.d.ts +12 -5
- package/dist/core/osc8.js +14 -7
- package/dist/core/paint.d.ts +9 -9
- package/dist/core/paint.js +3 -3
- package/dist/core/pretty.d.ts +1 -1
- package/dist/core/pretty.js +23 -19
- package/dist/core/protocol.d.ts +47 -26
- package/dist/core/protocol.js +31 -23
- package/dist/core/render.d.ts +19 -14
- package/dist/core/render.js +30 -17
- package/dist/core/seal.d.ts +26 -0
- package/dist/core/seal.js +75 -0
- package/dist/core/segment.d.ts +12 -4
- package/dist/core/segment.js +16 -6
- package/dist/core/strip.d.ts +83 -83
- package/dist/core/strip.js +70 -70
- package/dist/core/style.d.ts +46 -18
- package/dist/core/style.js +92 -56
- package/dist/core/text-order.d.ts +51 -0
- package/dist/core/text-order.js +50 -0
- package/dist/core/text.d.ts +12 -2
- package/dist/core/text.js +136 -90
- package/dist/core/timeline.d.ts +5 -5
- package/dist/core/timeline.js +11 -11
- package/dist/core/tween.d.ts +6 -6
- package/dist/core/tween.js +10 -10
- package/dist/core/view.d.ts +5 -5
- package/dist/core/view.js +15 -14
- package/dist/core/wrap.d.ts +24 -6
- package/dist/core/wrap.js +82 -16
- package/dist/host/app.d.ts +50 -9
- package/dist/host/app.js +120 -15
- package/dist/host/host-environment.d.ts +2 -2
- package/dist/host/host-environment.js +2 -2
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +42 -9
- package/dist/host/input.js +91 -7
- package/dist/host/terminal-colors.d.ts +18 -0
- package/dist/host/terminal-colors.js +46 -0
- package/dist/host/terminal-host.d.ts +4 -3
- package/dist/host/terminal-host.js +1 -1
- package/dist/index.d.ts +17 -16
- package/dist/index.js +11 -11
- package/dist/node/prompt.d.ts +1 -1
- package/dist/node/prompt.js +1 -1
- package/dist/node/terminal-host.d.ts +1 -1
- package/dist/node/traceback.js +2 -2
- package/dist/renderables/change.d.ts +1 -3
- package/dist/renderables/change.js +6 -8
- package/dist/renderables/effect.d.ts +88 -87
- package/dist/renderables/effect.js +124 -114
- package/dist/renderables/effects.d.ts +105 -76
- package/dist/renderables/effects.js +309 -176
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +2 -2
- package/dist/renderables/flexStrip.d.ts +7 -7
- package/dist/renderables/flexStrip.js +15 -15
- package/dist/renderables/frames.d.ts +7 -16
- package/dist/renderables/frames.js +12 -27
- package/dist/renderables/glyph-effect.d.ts +22 -23
- package/dist/renderables/glyph-effect.js +43 -41
- package/dist/renderables/glyph-effects.d.ts +33 -14
- package/dist/renderables/glyph-effects.js +90 -22
- package/dist/renderables/html-entities.d.ts +1 -1
- package/dist/renderables/html-entities.js +1 -1
- package/dist/renderables/lifecycle.d.ts +8 -10
- package/dist/renderables/lifecycle.js +17 -18
- package/dist/renderables/live.d.ts +2 -2
- package/dist/renderables/live.js +7 -7
- package/dist/renderables/markdown-parse.js +3 -3
- package/dist/renderables/markdown.js +4 -2
- package/dist/renderables/panel.js +7 -5
- package/dist/renderables/progress.d.ts +1 -1
- package/dist/renderables/progress.js +1 -1
- package/dist/renderables/progressBar.d.ts +3 -3
- package/dist/renderables/progressBar.js +21 -18
- package/dist/renderables/prompt.d.ts +1 -1
- package/dist/renderables/scroll.d.ts +1 -1
- package/dist/renderables/scroll.js +3 -3
- package/dist/renderables/spinner.d.ts +4 -4
- package/dist/renderables/spinner.js +10 -10
- package/dist/renderables/syntax.d.ts +2 -2
- package/dist/renderables/syntax.js +9 -4
- package/dist/renderables/table.js +5 -4
- package/dist/renderables/traceback.d.ts +1 -1
- package/dist/renderables/traceback.js +1 -1
- package/dist/renderables/tree.js +3 -3
- package/dist/template-bindings/animation-funcs.d.ts +60 -29
- package/dist/template-bindings/animation-funcs.js +126 -43
- package/dist/template-bindings/color-funcs.d.ts +14 -14
- package/dist/template-bindings/color-funcs.js +25 -25
- package/dist/template-bindings/index.d.ts +16 -14
- package/dist/template-bindings/index.js +17 -14
- package/dist/template-bindings/palette-funcs.d.ts +2 -2
- package/dist/template-bindings/palette-funcs.js +4 -4
- package/dist/template-bindings/style-funcs.d.ts +5 -5
- package/dist/template-bindings/style-funcs.js +16 -16
- package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
- package/dist/themes/{transpose.js → adjust.js} +28 -28
- package/dist/themes/buildPalette.d.ts +6 -5
- package/dist/themes/buildPalette.js +22 -22
- package/dist/themes/colorMath.d.ts +16 -16
- package/dist/themes/colorMath.js +32 -32
- package/dist/themes/data/ansi-tables.d.ts +2 -2
- package/dist/themes/data/ansi-tables.js +2 -2
- package/dist/themes/data/index.js +1 -4
- package/dist/themes/data/textual-ansi.js +0 -143
- package/dist/themes/data/types.d.ts +4 -4
- package/dist/themes/data/types.js +2 -2
- package/dist/themes/palette.d.ts +12 -12
- package/dist/themes/palette.js +16 -16
- package/dist/themes/ramp.d.ts +17 -17
- package/dist/themes/ramp.js +12 -12
- package/dist/themes/registry.d.ts +6 -8
- package/dist/themes/registry.js +6 -8
- package/dist/themes/terminalThemes.d.ts +4 -11
- package/dist/themes/terminalThemes.js +31 -47
- package/dist/widgets/button.d.ts +3 -3
- package/dist/widgets/button.js +8 -8
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +4 -4
- package/dist/widgets/dropdown.d.ts +3 -3
- package/dist/widgets/dropdown.js +7 -5
- package/dist/widgets/event-router.d.ts +6 -6
- package/dist/widgets/event-router.js +2 -2
- package/dist/widgets/focus-manager.d.ts +3 -3
- package/dist/widgets/focus-manager.js +3 -3
- package/dist/widgets/index.d.ts +2 -2
- package/dist/widgets/index.js +1 -1
- package/dist/widgets/ink.d.ts +3 -3
- package/dist/widgets/ink.js +4 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/static-item.d.ts +2 -2
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +5 -5
- package/dist/widgets/themed-widget.js +4 -4
- package/dist/widgets/toggle.d.ts +4 -4
- package/dist/widgets/toggle.js +6 -6
- package/dist/widgets/types.d.ts +6 -6
- package/dist/widgets/types.js +2 -2
- package/dist/widgets/widget-app.d.ts +10 -10
- package/dist/widgets/widget-app.js +8 -8
- package/dist/widgets/widget-base.d.ts +2 -2
- package/package.json +5 -4
package/dist/host/app.js
CHANGED
|
@@ -31,21 +31,22 @@
|
|
|
31
31
|
* — a focused field in a `WidgetApp` included — can swallow them.
|
|
32
32
|
*/
|
|
33
33
|
import { frameRate, systemClock } from "../core/clock.js";
|
|
34
|
+
import { ColorDepth, resolveTerminal } from "../core/color.js";
|
|
34
35
|
import { Console } from "../core/console.js";
|
|
35
36
|
import { Segment } from "../core/segment.js";
|
|
36
|
-
import {
|
|
37
|
+
import { SurfaceRenderer } from "../core/paint.js";
|
|
37
38
|
import { fitHeight, render } from "../core/protocol.js";
|
|
38
39
|
import { Timeline } from "../core/timeline.js";
|
|
39
40
|
import { drawView, frameNow, hostView, undrawn } from "../core/view.js";
|
|
40
41
|
import { hostEnvironment } from "./host-environment.js";
|
|
41
42
|
import { InputParser, keyName, parseKeyName } from "./input.js";
|
|
42
|
-
// [LAW:one-source-of-truth] How a frame draws — its glyphs, the
|
|
43
|
-
// it knows, what happens to a style it
|
|
44
|
-
// highlighting — is the console's to say, so App takes those options as the
|
|
43
|
+
// [LAW:one-source-of-truth] How a frame draws — its glyphs, the terminal its
|
|
44
|
+
// colors resolve on, the style names it knows, what happens to a style it
|
|
45
|
+
// cannot resolve, markup and highlighting — is the console's to say, so App takes those options as the
|
|
45
46
|
// console declares them. This list is both the type and what is copied: the
|
|
46
|
-
// host alone decides the console's size,
|
|
47
|
+
// host alone decides the console's size, colors and sink, so no other
|
|
47
48
|
// property of the options object reaches it, whatever the object carries.
|
|
48
|
-
const DRAW_OPTIONS = ["asciiOnly", "reducedMotion", "theme", "onStyleError", "markup", "highlight", "highlighter"];
|
|
49
|
+
const DRAW_OPTIONS = ["asciiOnly", "reducedMotion", "textOrder", "terminal", "theme", "onStyleError", "markup", "highlight", "highlighter"];
|
|
49
50
|
function drawOptions(options) {
|
|
50
51
|
const picked = {};
|
|
51
52
|
const copy = (key) => {
|
|
@@ -68,6 +69,12 @@ const POINTER = {
|
|
|
68
69
|
// The sideways wheel is not here: nothing scrolls by columns, inside an app or
|
|
69
70
|
// around it.
|
|
70
71
|
const WHEEL_LINES = { scroll_up: -1, scroll_down: 1 };
|
|
72
|
+
// Primary Device Attributes. Every VT100 descendant answers it, and a terminal
|
|
73
|
+
// answers in the order it was asked, so its reply, sent after an ask's queries,
|
|
74
|
+
// says every reply to them that is coming has come.
|
|
75
|
+
const DA1 = "\x1b[c";
|
|
76
|
+
const isDa1Reply = (reply) => reply.kind === "csi" && reply.body.startsWith("?") && reply.body.endsWith("c");
|
|
77
|
+
const DEFAULT_ASK_TIMEOUT = 0.5;
|
|
71
78
|
const isWheelTurn = (event) => event.type in WHEEL_LINES;
|
|
72
79
|
export class App {
|
|
73
80
|
host;
|
|
@@ -84,12 +91,21 @@ export class App {
|
|
|
84
91
|
// What the frame on screen says about when it changes, on the clock.
|
|
85
92
|
// Before a frame, nothing.
|
|
86
93
|
said = undrawn(0);
|
|
87
|
-
// The one wait for the frame's `
|
|
94
|
+
// The one wait for the frame's `validUntil`, armed after each frame.
|
|
88
95
|
waking = null;
|
|
89
96
|
// [LAW:one-source-of-truth] The host is the console's whole environment:
|
|
90
|
-
// its size, its
|
|
97
|
+
// its size, its colors, where bytes go. Every frame renders with this
|
|
91
98
|
// console's options and is encoded for its destination.
|
|
92
99
|
console;
|
|
100
|
+
drawing;
|
|
101
|
+
// [LAW:single-enforcer] One ask at a time, through the one input reader:
|
|
102
|
+
// the next waits on the last, and `leave` ends the one in flight.
|
|
103
|
+
asking = null;
|
|
104
|
+
asked = Promise.resolve();
|
|
105
|
+
// DA1 replies still owed to asks that timed out. A terminal answers in the
|
|
106
|
+
// order it was asked, so every reply before the last of them belongs to an
|
|
107
|
+
// ask that has ended, and none reaches the ask after it.
|
|
108
|
+
owedDa1 = 0;
|
|
93
109
|
_phase = "idle";
|
|
94
110
|
_frame = [];
|
|
95
111
|
painter;
|
|
@@ -104,16 +120,88 @@ export class App {
|
|
|
104
120
|
settle = () => { };
|
|
105
121
|
constructor(options) {
|
|
106
122
|
this.host = options.host;
|
|
107
|
-
this.input = new InputParser({ key: (press) => this.key(press), mouse: (event) => this.mouse(event) }, options.host.inputChunks);
|
|
123
|
+
this.input = new InputParser({ key: (press) => this.key(press), mouse: (event) => this.mouse(event), reply: (reply) => this.reply(reply) }, options.host.inputChunks);
|
|
108
124
|
this.surface = options.surface;
|
|
109
|
-
this.painter = new
|
|
125
|
+
this.painter = new SurfaceRenderer(options.surface, (bytes) => this.host.write(bytes));
|
|
110
126
|
this.view = hostView(options.view);
|
|
111
127
|
this._clock = options.clock ?? systemClock();
|
|
112
128
|
this._epoch = this._clock.now();
|
|
113
129
|
this._timeline = options.timeline ?? Timeline.play(this._epoch);
|
|
114
130
|
this._rate = options.rate ?? DEFAULT_RATE;
|
|
115
131
|
this.quitKeys = new Set((options.quitKeys ?? DEFAULT_QUIT_KEYS).map(parseKeyName));
|
|
116
|
-
this.
|
|
132
|
+
this.drawing = drawOptions(options);
|
|
133
|
+
this.console = this.drawingConsole();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The terminal the app draws on: the colors a cell that sets none is drawn
|
|
137
|
+
* in, and the sixteen an ANSI color names — as given, or as the color
|
|
138
|
+
* depth assumes (`resolveTerminal`).
|
|
139
|
+
*/
|
|
140
|
+
get terminal() {
|
|
141
|
+
return resolveTerminal(this.console.options.terminal, this.console.colorSystem ?? ColorDepth.TRUECOLOR);
|
|
142
|
+
}
|
|
143
|
+
/** Draw on `terminal` from the next frame on — what `terminalColors` learned, say. */
|
|
144
|
+
set terminal(terminal) {
|
|
145
|
+
// [LAW:one-source-of-truth] The console is the one holder of how a frame
|
|
146
|
+
// draws, so the terminal changes by a new console, not beside the old one.
|
|
147
|
+
this.drawing = { ...this.drawing, terminal };
|
|
148
|
+
this.console = this.drawingConsole();
|
|
149
|
+
this.refresh();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Ask the terminal: write `queries`, then DA1, and resolve with the replies
|
|
153
|
+
* heard before DA1's, or before the timeout or the app handing the terminal
|
|
154
|
+
* back. It rejects only when writing to the terminal throws. Asks run one
|
|
155
|
+
* after another, and only while the app runs: it holds raw mode and reads
|
|
156
|
+
* the input, so a reply reaches the ask and never a key handler. A reply no
|
|
157
|
+
* ask is waiting for is dropped. `queries` must not ask for DA1 itself: its
|
|
158
|
+
* reply would end the ask early.
|
|
159
|
+
*/
|
|
160
|
+
ask(queries, options = {}) {
|
|
161
|
+
// An ask before the app runs is a mistake in the program; one after it
|
|
162
|
+
// stopped ends `left`, so the error that stopped it is the one heard.
|
|
163
|
+
if (this._phase === "idle")
|
|
164
|
+
throw new Error("App.ask: an app asks its terminal once it runs, and this one has not started");
|
|
165
|
+
const timeout = options.timeout ?? DEFAULT_ASK_TIMEOUT;
|
|
166
|
+
if (!(timeout > 0 && Number.isFinite(timeout)))
|
|
167
|
+
throw new RangeError(`App.ask: a timeout is a positive, finite number of seconds, not ${timeout}`);
|
|
168
|
+
const turn = this.asked.then(() => this.askNow(queries, timeout));
|
|
169
|
+
// One ask's failure is its caller's; the asks queued after it still run.
|
|
170
|
+
this.asked = turn.catch(() => undefined);
|
|
171
|
+
return turn;
|
|
172
|
+
}
|
|
173
|
+
// The one way the app builds the console it draws with.
|
|
174
|
+
drawingConsole() {
|
|
175
|
+
return new Console({ ...this.drawing, environment: hostEnvironment(this.host) });
|
|
176
|
+
}
|
|
177
|
+
askNow(queries, timeout) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
if (this._phase !== "running")
|
|
180
|
+
return resolve({ ended: "left", replies: [] });
|
|
181
|
+
if (!this.host.writesToTerminal)
|
|
182
|
+
return resolve({ ended: "unasked", replies: [] });
|
|
183
|
+
// Written before the ask takes the input, so a write that throws leaves
|
|
184
|
+
// no ask in flight and no timer armed.
|
|
185
|
+
this.host.write(queries.join("") + DA1);
|
|
186
|
+
const replies = [];
|
|
187
|
+
const timer = this._clock.at(this._clock.now() + timeout, () => {
|
|
188
|
+
this.owedDa1++;
|
|
189
|
+
asking.finish("timeout");
|
|
190
|
+
});
|
|
191
|
+
const asking = {
|
|
192
|
+
replies,
|
|
193
|
+
finish: (ended) => {
|
|
194
|
+
timer();
|
|
195
|
+
this.asking = null;
|
|
196
|
+
// [LAW:single-enforcer] Every way an ask ends goes through here, so
|
|
197
|
+
// what the parser held for its replies is settled on every one.
|
|
198
|
+
this.input.awaitReplies(false);
|
|
199
|
+
resolve({ ended, replies });
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
this.asking = asking;
|
|
203
|
+
this.input.awaitReplies(true);
|
|
204
|
+
});
|
|
117
205
|
}
|
|
118
206
|
get phase() {
|
|
119
207
|
return this._phase;
|
|
@@ -238,6 +326,8 @@ export class App {
|
|
|
238
326
|
return;
|
|
239
327
|
this._phase = "suspended";
|
|
240
328
|
this.tick();
|
|
329
|
+
// What is half-read belongs to no one once the terminal is handed back.
|
|
330
|
+
this.input.reset();
|
|
241
331
|
this.leave();
|
|
242
332
|
await this.host.suspend();
|
|
243
333
|
// Stopped while suspended — by a signal, say: the terminal is already
|
|
@@ -258,7 +348,7 @@ export class App {
|
|
|
258
348
|
return () => this.paintHandlers.delete(handler);
|
|
259
349
|
}
|
|
260
350
|
/**
|
|
261
|
-
* Hear every key the app does not quit on, by the `
|
|
351
|
+
* Hear every key the app does not quit on, by the `KeyEvent` the terminal
|
|
262
352
|
* reported; `keyName` names it. A handler that throws fails the app.
|
|
263
353
|
*/
|
|
264
354
|
onKey(handler) {
|
|
@@ -306,6 +396,9 @@ export class App {
|
|
|
306
396
|
this.host.write(POINTER[this.surface].on);
|
|
307
397
|
}
|
|
308
398
|
leave() {
|
|
399
|
+
this.asking?.finish("left");
|
|
400
|
+
// Whatever answers the terminal sends from here reach whoever holds it next.
|
|
401
|
+
this.owedDa1 = 0;
|
|
309
402
|
this.host.write(POINTER[this.surface].off);
|
|
310
403
|
this.painter.handBack();
|
|
311
404
|
this.host.setRawMode(false);
|
|
@@ -332,6 +425,18 @@ export class App {
|
|
|
332
425
|
this.settle(outcome);
|
|
333
426
|
}
|
|
334
427
|
// --- input ---
|
|
428
|
+
reply(reply) {
|
|
429
|
+
if (this.owedDa1 > 0) {
|
|
430
|
+
if (isDa1Reply(reply))
|
|
431
|
+
this.owedDa1--;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (this.asking === null)
|
|
435
|
+
return;
|
|
436
|
+
if (isDa1Reply(reply))
|
|
437
|
+
return this.asking.finish("answered");
|
|
438
|
+
this.asking.replies.push(reply);
|
|
439
|
+
}
|
|
335
440
|
key(press) {
|
|
336
441
|
if (this.quitKeys.has(keyName(press)))
|
|
337
442
|
return this.stop();
|
|
@@ -383,8 +488,8 @@ export class App {
|
|
|
383
488
|
// says whether the view moves.
|
|
384
489
|
tick() {
|
|
385
490
|
const running = this._phase === "running";
|
|
386
|
-
const { t,
|
|
387
|
-
const wanted = running &&
|
|
491
|
+
const { t, validUntil } = this.said;
|
|
492
|
+
const wanted = running && validUntil === t;
|
|
388
493
|
if (this.ticking !== null && (!wanted || this.ticking.rate.perSecond !== this._rate.perSecond)) {
|
|
389
494
|
this.ticking.stop();
|
|
390
495
|
this.ticking = null;
|
|
@@ -393,7 +498,7 @@ export class App {
|
|
|
393
498
|
this.ticking = { rate: this._rate, stop: this._clock.every(this._rate, () => this.paint()) };
|
|
394
499
|
}
|
|
395
500
|
this.waking?.();
|
|
396
|
-
this.waking = running && t <
|
|
501
|
+
this.waking = running && t < validUntil && validUntil < Number.POSITIVE_INFINITY ? this._clock.at(validUntil, () => this.paint()) : null;
|
|
397
502
|
}
|
|
398
503
|
// [LAW:no-silent-failure] A frame that throws ends the app with that
|
|
399
504
|
// error: the terminal is handed back, and `run` rejects with it.
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* [LAW:one-source-of-truth] A console asks its destination four things: where
|
|
7
7
|
* bytes go, is it a terminal, how big is it, and — through the environment —
|
|
8
|
-
* what
|
|
8
|
+
* what colors it draws. A bare sink answers only the first, and the other
|
|
9
9
|
* three fall to the ambient `process`: some other stream's answer in node,
|
|
10
10
|
* none in a browser, where a console on xterm.js saw an unnamed TTY and drew
|
|
11
|
-
* 16
|
|
11
|
+
* 16 colors. Handing the console the whole environment keeps all four
|
|
12
12
|
* answers the host's.
|
|
13
13
|
*
|
|
14
14
|
* The host's size is `size()`, so the environment handed on carries no
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* [LAW:one-source-of-truth] A console asks its destination four things: where
|
|
7
7
|
* bytes go, is it a terminal, how big is it, and — through the environment —
|
|
8
|
-
* what
|
|
8
|
+
* what colors it draws. A bare sink answers only the first, and the other
|
|
9
9
|
* three fall to the ambient `process`: some other stream's answer in node,
|
|
10
10
|
* none in a browser, where a console on xterm.js saw an unnamed TTY and drew
|
|
11
|
-
* 16
|
|
11
|
+
* 16 colors. Handing the console the whole environment keeps all four
|
|
12
12
|
* answers the host's.
|
|
13
13
|
*
|
|
14
14
|
* The host's size is `size()`, so the environment handed on carries no
|
package/dist/host/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export { BrowserTerminalHost } from "./terminal-host.js";
|
|
|
2
2
|
export type { TerminalHost, TerminalSize, DataHandler, InputChunks, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
3
3
|
export { hostEnvironment } from "./host-environment.js";
|
|
4
4
|
export { App } from "./app.js";
|
|
5
|
-
export type { AppMouseEvent, AppOptions, AppPhase,
|
|
5
|
+
export type { AppMouseEvent, AppOptions, AppPhase, Asked, AskOptions, WheelEvent } from "./app.js";
|
|
6
|
+
export { terminalColors } from "./terminal-colors.js";
|
|
6
7
|
export type { Surface } from "../core/paint.js";
|
|
7
8
|
export { keyName } from "./input.js";
|
|
8
|
-
export type {
|
|
9
|
+
export type { KeyEvent, TerminalMouseEvent, TerminalReply } from "./input.js";
|
package/dist/host/index.js
CHANGED
package/dist/host/input.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal input: the bytes a terminal sends for keys and the pointer,
|
|
3
|
-
* parsed into `
|
|
3
|
+
* parsed into `KeyEvent` and `TerminalMouseEvent` values, and the one name
|
|
4
4
|
* each key goes by.
|
|
5
5
|
*
|
|
6
6
|
* [LAW:single-enforcer] Escape-sequence parsing lives only here. `App` owns
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* "are we mid-sequence" mode flag — the buffer head carries that state, so on
|
|
14
14
|
* a host whose chunks can split a key, a UTF-8 character or an escape
|
|
15
15
|
* sequence split across two chunks is completed by the second. What a chunk
|
|
16
|
-
* end means is the host's `InputChunks`,
|
|
17
|
-
*
|
|
16
|
+
* end means is the host's `InputChunks`, and whether an ask awaits a reply is
|
|
17
|
+
* what its owner last told `awaitReplies`: the two inputs to the parser that
|
|
18
|
+
* are not bytes, and both decide only what becomes of an unfinished ESC.
|
|
18
19
|
*
|
|
19
20
|
* An unfinished escape sequence is ambiguous: `ESC` alone is the Escape key
|
|
20
21
|
* or the start of a sequence, and `ESC [` is Alt+[ or the start of a CSI.
|
|
@@ -29,10 +30,19 @@
|
|
|
29
30
|
* lone ESC is also settled early by a next chunk that opens with anything but
|
|
30
31
|
* `[` or `O`, because a busy worker can queue the next key behind it.
|
|
31
32
|
* `flush()` settles the hold synchronously.
|
|
33
|
+
*
|
|
34
|
+
* `ESC ]`, `ESC P` and `ESC _` open the control strings a terminal replies
|
|
35
|
+
* in, and are also Alt+], Alt+P and Alt+_. A reply can arrive split only on
|
|
36
|
+
* a split host while an ask awaits one (`replyMayArrive`), and only then is an
|
|
37
|
+
* unterminated one, or a lone ESC that may open one, held — past the escape
|
|
38
|
+
* delay, until `awaitReplies(false)` arms the delay afresh. Otherwise it is Alt
|
|
39
|
+
* on its key at once. A terminated one is a reply either way, and one cut off
|
|
40
|
+
* by an ESC that is not ST was never a reply, and is Alt on its key, as with
|
|
41
|
+
* none awaited.
|
|
32
42
|
*/
|
|
33
43
|
import type { InputChunks } from "./terminal-host.js";
|
|
34
44
|
/** A key as the terminal reported it: plain data, with no say over who hears it. */
|
|
35
|
-
export interface
|
|
45
|
+
export interface KeyEvent {
|
|
36
46
|
readonly key: string;
|
|
37
47
|
/** The printable character the key types, or "" for a key that types none. */
|
|
38
48
|
readonly character: string;
|
|
@@ -40,7 +50,7 @@ export interface KeyPress {
|
|
|
40
50
|
readonly ctrl: boolean;
|
|
41
51
|
readonly meta: boolean;
|
|
42
52
|
}
|
|
43
|
-
export type
|
|
53
|
+
export type TerminalMouseEvent = {
|
|
44
54
|
[T in MouseEventType]: MouseEventOf<T>;
|
|
45
55
|
}[MouseEventType];
|
|
46
56
|
type MouseEventType = "mouse_down" | "mouse_up" | "mouse_move" | "scroll_up" | "scroll_down" | "scroll_left" | "scroll_right";
|
|
@@ -60,11 +70,23 @@ interface MouseEventOf<Type extends string> {
|
|
|
60
70
|
* `ctrl+c`, `shift+tab`, `meta+left`. Shift is not named on a character,
|
|
61
71
|
* which already carries it: Shift+G is `G`.
|
|
62
72
|
*/
|
|
63
|
-
export declare function keyName(press:
|
|
73
|
+
export declare function keyName(press: KeyEvent): string;
|
|
74
|
+
/**
|
|
75
|
+
* What a terminal sends back when it is asked something: an OSC, DCS or APC
|
|
76
|
+
* string, its introducer and terminator taken off (`11;rgb:1e1e/1e1e/1e1e`
|
|
77
|
+
* for an OSC 11 reply), or a CSI report with a private prefix, its final byte
|
|
78
|
+
* kept (`?62;22c` for DA1, `?2026;2$y` for DECRQM). No key is sent in either
|
|
79
|
+
* form, so neither is ever read as one.
|
|
80
|
+
*/
|
|
81
|
+
export interface TerminalReply {
|
|
82
|
+
readonly kind: "osc" | "dcs" | "apc" | "csi";
|
|
83
|
+
readonly body: string;
|
|
84
|
+
}
|
|
64
85
|
/** Where a parser delivers what it parses. */
|
|
65
86
|
export interface InputSink {
|
|
66
|
-
key(press:
|
|
67
|
-
mouse(event:
|
|
87
|
+
key(press: KeyEvent): void;
|
|
88
|
+
mouse(event: TerminalMouseEvent): void;
|
|
89
|
+
reply(reply: TerminalReply): void;
|
|
68
90
|
}
|
|
69
91
|
/**
|
|
70
92
|
* `name`, if `keyName` can produce it: modifiers in its order, then a named
|
|
@@ -87,11 +109,22 @@ export declare class InputParser {
|
|
|
87
109
|
private readonly chunks;
|
|
88
110
|
private buffer;
|
|
89
111
|
private escTimer;
|
|
112
|
+
private awaiting;
|
|
90
113
|
constructor(sink: InputSink, chunks: InputChunks);
|
|
91
114
|
/** Feed a chunk of bytes (or a string of them) into the parser. */
|
|
92
115
|
feed(chunk: Uint8Array | string): void;
|
|
93
116
|
private drainAndHold;
|
|
94
|
-
private
|
|
117
|
+
private validity;
|
|
118
|
+
/**
|
|
119
|
+
* Whether an ask is awaiting the terminal's replies, told by whoever asks
|
|
120
|
+
* at the ask's start and end. Only while one is does an unfinished
|
|
121
|
+
* `ESC ]`, `ESC P` or `ESC _` open a reply still arriving; otherwise it is
|
|
122
|
+
* Alt on that key. The wait's end restarts the escape delay for what is
|
|
123
|
+
* held, on a split host, the only one that holds.
|
|
124
|
+
*/
|
|
125
|
+
awaitReplies(awaiting: boolean): void;
|
|
126
|
+
private replyMayArrive;
|
|
127
|
+
private continuesEscape;
|
|
95
128
|
private settleChunk;
|
|
96
129
|
private drainFully;
|
|
97
130
|
/** Settle a held escape sequence now, as the end of a whole-key chunk. */
|
package/dist/host/input.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal input: the bytes a terminal sends for keys and the pointer,
|
|
3
|
-
* parsed into `
|
|
3
|
+
* parsed into `KeyEvent` and `TerminalMouseEvent` values, and the one name
|
|
4
4
|
* each key goes by.
|
|
5
5
|
*
|
|
6
6
|
* [LAW:single-enforcer] Escape-sequence parsing lives only here. `App` owns
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* "are we mid-sequence" mode flag — the buffer head carries that state, so on
|
|
14
14
|
* a host whose chunks can split a key, a UTF-8 character or an escape
|
|
15
15
|
* sequence split across two chunks is completed by the second. What a chunk
|
|
16
|
-
* end means is the host's `InputChunks`,
|
|
17
|
-
*
|
|
16
|
+
* end means is the host's `InputChunks`, and whether an ask awaits a reply is
|
|
17
|
+
* what its owner last told `awaitReplies`: the two inputs to the parser that
|
|
18
|
+
* are not bytes, and both decide only what becomes of an unfinished ESC.
|
|
18
19
|
*
|
|
19
20
|
* An unfinished escape sequence is ambiguous: `ESC` alone is the Escape key
|
|
20
21
|
* or the start of a sequence, and `ESC [` is Alt+[ or the start of a CSI.
|
|
@@ -29,6 +30,15 @@
|
|
|
29
30
|
* lone ESC is also settled early by a next chunk that opens with anything but
|
|
30
31
|
* `[` or `O`, because a busy worker can queue the next key behind it.
|
|
31
32
|
* `flush()` settles the hold synchronously.
|
|
33
|
+
*
|
|
34
|
+
* `ESC ]`, `ESC P` and `ESC _` open the control strings a terminal replies
|
|
35
|
+
* in, and are also Alt+], Alt+P and Alt+_. A reply can arrive split only on
|
|
36
|
+
* a split host while an ask awaits one (`replyMayArrive`), and only then is an
|
|
37
|
+
* unterminated one, or a lone ESC that may open one, held — past the escape
|
|
38
|
+
* delay, until `awaitReplies(false)` arms the delay afresh. Otherwise it is Alt
|
|
39
|
+
* on its key at once. A terminated one is a reply either way, and one cut off
|
|
40
|
+
* by an ESC that is not ST was never a reply, and is Alt on its key, as with
|
|
41
|
+
* none awaited.
|
|
32
42
|
*/
|
|
33
43
|
/**
|
|
34
44
|
* The one name a key goes by — what a keymap is keyed by and what
|
|
@@ -136,6 +146,12 @@ const LANGLE = 0x3c; // <
|
|
|
136
146
|
const O_BYTE = 0x4f; // O
|
|
137
147
|
const M_UPPER = 0x4d; // M
|
|
138
148
|
const M_LOWER = 0x6d; // m
|
|
149
|
+
const BEL = 0x07;
|
|
150
|
+
const BACKSLASH = 0x5c;
|
|
151
|
+
// The control strings a terminal answers in, by the byte after ESC: OSC, DCS, APC.
|
|
152
|
+
const STRING_REPLIES = { 0x5d: "osc", 0x50: "dcs", 0x5f: "apc" };
|
|
153
|
+
// A CSI whose parameters open with one of these is a report, never a key.
|
|
154
|
+
const PRIVATE_PREFIXES = new Set(["?", ">"]);
|
|
139
155
|
// [LAW:single-enforcer] Shared encode/decode singletons. The parser
|
|
140
156
|
// receives `Uint8Array | string` from the host and walks bytes through
|
|
141
157
|
// its state machine; conversion happens at the two boundaries (string
|
|
@@ -198,6 +214,7 @@ export class InputParser {
|
|
|
198
214
|
// [LAW:types-are-the-program] `setTimeout`/`clearTimeout` are universal
|
|
199
215
|
// across node and browser; `setImmediate` is node-only.
|
|
200
216
|
escTimer;
|
|
217
|
+
awaiting = false;
|
|
201
218
|
constructor(sink, chunks) {
|
|
202
219
|
this.sink = sink;
|
|
203
220
|
this.chunks = chunks;
|
|
@@ -213,7 +230,9 @@ export class InputParser {
|
|
|
213
230
|
// Any other next chunk means Escape was its own key, even when the hold
|
|
214
231
|
// timer has not run yet: a busy worker can queue the next key ahead of it,
|
|
215
232
|
// and joining them would read Escape then G as Alt+G (rich-host-input-0c4f).
|
|
216
|
-
|
|
233
|
+
// While a reply is awaited, a next chunk opening one of its control strings
|
|
234
|
+
// continues it too: a reply split right after its ESC.
|
|
235
|
+
const escaped = this.holdingEscape() && next.length > 0 && !this.continuesEscape(next[0]);
|
|
217
236
|
this.cancelEscape();
|
|
218
237
|
// The buffer is settled before the sink hears Escape, so a sink that
|
|
219
238
|
// resets the parser on it (an app stopping) leaves nothing to drain.
|
|
@@ -231,7 +250,7 @@ export class InputParser {
|
|
|
231
250
|
this.drainFully();
|
|
232
251
|
}
|
|
233
252
|
finally {
|
|
234
|
-
this.
|
|
253
|
+
this.validity();
|
|
235
254
|
}
|
|
236
255
|
}
|
|
237
256
|
// [LAW:no-ambient-temporal-coupling] Whatever drain leaves at an ESC is an
|
|
@@ -242,14 +261,41 @@ export class InputParser {
|
|
|
242
261
|
// timer before it reads input queued behind a long frame, so the delay's end
|
|
243
262
|
// settles the hold only after one more turn of the event loop has read what
|
|
244
263
|
// was waiting. The parser holds one timer, so arming it replaces any other.
|
|
245
|
-
hold
|
|
264
|
+
// While a reply may arrive the delay settles no hold that may open one: a
|
|
265
|
+
// reply split over a slow link may take longer than it. The wait's end arms
|
|
266
|
+
// it afresh (`awaitReplies`), so what is held then gets the whole delay.
|
|
267
|
+
validity() {
|
|
246
268
|
this.cancelEscape();
|
|
247
269
|
if (this.buffer[0] !== ESC)
|
|
248
270
|
return;
|
|
249
271
|
this.escTimer = setTimeout(() => {
|
|
250
|
-
this.escTimer = setTimeout(() =>
|
|
272
|
+
this.escTimer = setTimeout(() => {
|
|
273
|
+
if (!this.replyMayArrive() || !opensString(this.buffer))
|
|
274
|
+
this.flush();
|
|
275
|
+
}, 0);
|
|
251
276
|
}, ESCAPE_DELAY_MS);
|
|
252
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether an ask is awaiting the terminal's replies, told by whoever asks
|
|
280
|
+
* at the ask's start and end. Only while one is does an unfinished
|
|
281
|
+
* `ESC ]`, `ESC P` or `ESC _` open a reply still arriving; otherwise it is
|
|
282
|
+
* Alt on that key. The wait's end restarts the escape delay for what is
|
|
283
|
+
* held, on a split host, the only one that holds.
|
|
284
|
+
*/
|
|
285
|
+
awaitReplies(awaiting) {
|
|
286
|
+
this.awaiting = awaiting;
|
|
287
|
+
if (!awaiting && this.chunks === "split")
|
|
288
|
+
this.validity();
|
|
289
|
+
}
|
|
290
|
+
// [LAW:single-enforcer] The one reading of whether an unfinished control
|
|
291
|
+
// string may be a reply still arriving: only a split host splits one, and
|
|
292
|
+
// only an ask awaits one.
|
|
293
|
+
replyMayArrive() {
|
|
294
|
+
return this.chunks === "split" && this.awaiting;
|
|
295
|
+
}
|
|
296
|
+
continuesEscape(byte) {
|
|
297
|
+
return byte === LBRACKET || byte === O_BYTE || (STRING_REPLIES[byte] !== undefined && this.replyMayArrive());
|
|
298
|
+
}
|
|
253
299
|
// A whole-key chunk has ended, so an ESC still at the buffer head opens no
|
|
254
300
|
// sequence, and is read as `reissue` reads it. Each key is taken off the
|
|
255
301
|
// buffer before the sink hears it, so no byte of one chunk outlives it.
|
|
@@ -311,6 +357,8 @@ export class InputParser {
|
|
|
311
357
|
this.sink.key(result.event);
|
|
312
358
|
else if (result.kind === "mouse")
|
|
313
359
|
this.sink.mouse(result.event);
|
|
360
|
+
else if (result.kind === "reply")
|
|
361
|
+
this.sink.reply(result.event);
|
|
314
362
|
// skip → drop bytes silently
|
|
315
363
|
}
|
|
316
364
|
}
|
|
@@ -377,6 +425,15 @@ export class InputParser {
|
|
|
377
425
|
// ESC [ ... → CSI
|
|
378
426
|
if (b1 === LBRACKET)
|
|
379
427
|
return this.consumeCSI(buf);
|
|
428
|
+
const string = STRING_REPLIES[b1];
|
|
429
|
+
if (string !== undefined) {
|
|
430
|
+
const result = consumeString(buf, string);
|
|
431
|
+
// Unterminated where no reply may arrive, it was never a reply: Alt on
|
|
432
|
+
// its key, and what followed is read on its own.
|
|
433
|
+
if (result.kind === "incomplete" && !this.replyMayArrive())
|
|
434
|
+
return { kind: "key", bytes: 2, event: altKey(b1) };
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
380
437
|
// ESC O X → SS3 (F1–F4 / arrows on some terms)
|
|
381
438
|
if (b1 === O_BYTE)
|
|
382
439
|
return this.consumeSS3(buf);
|
|
@@ -555,7 +612,34 @@ function decodeMouseFromCb(cb, x, y, isPress) {
|
|
|
555
612
|
ctrl,
|
|
556
613
|
};
|
|
557
614
|
}
|
|
615
|
+
// A lone ESC, or one opening a control string: the holds a reply may be.
|
|
616
|
+
function opensString(buf) {
|
|
617
|
+
return buf.length === 1 || STRING_REPLIES[buf[1]] !== undefined;
|
|
618
|
+
}
|
|
619
|
+
// A control string runs to its terminator, ST (`ESC \\`) or BEL, and holds
|
|
620
|
+
// any byte but ESC before it. Terminated, it is a reply whether or not one was
|
|
621
|
+
// awaited. Unterminated, it is unfinished, and `consumeEscape` decides by
|
|
622
|
+
// whether a reply may arrive if that means held or Alt on its key. An ESC that
|
|
623
|
+
// is not ST ends it before it closed: it was never a reply, so it is Alt on
|
|
624
|
+
// its key and what followed is read on its own, as with none awaited.
|
|
625
|
+
function consumeString(buf, kind) {
|
|
626
|
+
for (let i = 2; i < buf.length; i++) {
|
|
627
|
+
const b = buf[i];
|
|
628
|
+
if (b === BEL)
|
|
629
|
+
return { kind: "reply", bytes: i + 1, event: { kind, body: UTF8_DECODER.decode(buf.subarray(2, i)) } };
|
|
630
|
+
if (b !== ESC)
|
|
631
|
+
continue;
|
|
632
|
+
if (i + 1 === buf.length)
|
|
633
|
+
return { kind: "incomplete" };
|
|
634
|
+
if (buf[i + 1] !== BACKSLASH)
|
|
635
|
+
return { kind: "key", bytes: 2, event: altKey(buf[1]) };
|
|
636
|
+
return { kind: "reply", bytes: i + 2, event: { kind, body: UTF8_DECODER.decode(buf.subarray(2, i)) } };
|
|
637
|
+
}
|
|
638
|
+
return { kind: "incomplete" };
|
|
639
|
+
}
|
|
558
640
|
function decodeCSI(params, final, bytes) {
|
|
641
|
+
if (PRIVATE_PREFIXES.has(params[0] ?? ""))
|
|
642
|
+
return { kind: "reply", bytes, event: { kind: "csi", body: params + final } };
|
|
559
643
|
// Handle modifier suffixes like `1;2A` (shift+up). We split params on `;`.
|
|
560
644
|
const parts = params.split(";");
|
|
561
645
|
const first = parts[0] ?? "";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's own foreground and background, asked of it with OSC 10 and
|
|
3
|
+
* OSC 11 — the colors every contrast decision is measured against, which the
|
|
4
|
+
* library otherwise has to assume.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:effects-at-boundaries] The ask is `App`'s, the one reader of its
|
|
7
|
+
* terminal's input; what is here is the query bytes and the parse of their
|
|
8
|
+
* replies, which stays pure.
|
|
9
|
+
*/
|
|
10
|
+
import { TerminalColors } from "../core/color.js";
|
|
11
|
+
import type { App } from "./app.js";
|
|
12
|
+
/**
|
|
13
|
+
* Ask the terminal `app` runs on for its foreground and background, and return
|
|
14
|
+
* the terminal `app` draws on with them in place — its sixteen ANSI colors
|
|
15
|
+
* kept, since nothing asks for those. `undefined` when the terminal did not
|
|
16
|
+
* answer both, so the caller keeps what it had. Set it as `app.terminal`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function terminalColors(app: App): Promise<TerminalColors | undefined>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's own foreground and background, asked of it with OSC 10 and
|
|
3
|
+
* OSC 11 — the colors every contrast decision is measured against, which the
|
|
4
|
+
* library otherwise has to assume.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:effects-at-boundaries] The ask is `App`'s, the one reader of its
|
|
7
|
+
* terminal's input; what is here is the query bytes and the parse of their
|
|
8
|
+
* replies, which stays pure.
|
|
9
|
+
*/
|
|
10
|
+
import { ColorRgba, TerminalColors } from "../core/color.js";
|
|
11
|
+
import { pattern } from "../core/pattern.js";
|
|
12
|
+
// OSC 10 and OSC 11 with `?` ask for the dynamic foreground and background.
|
|
13
|
+
const QUERIES = ["\x1b]10;?\x1b\\", "\x1b]11;?\x1b\\"];
|
|
14
|
+
// XParseColor's `rgb:` form, 1–4 hex digits a channel, as xterm and every
|
|
15
|
+
// terminal that copies it answers, or `rgba:`, as urxvt answers with
|
|
16
|
+
// transparency on; the alpha says nothing about the color a cell is drawn in.
|
|
17
|
+
const RGB = pattern(/^rgba?:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})(?:\/[0-9a-f]{1,4})?$/i);
|
|
18
|
+
// A channel of n hex digits is a fraction of 16ⁿ − 1, scaled to 8 bits.
|
|
19
|
+
const channel = (hex) => Math.round((parseInt(hex, 16) / (16 ** hex.length - 1)) * 255);
|
|
20
|
+
/**
|
|
21
|
+
* The color an OSC 10 or OSC 11 reply reports, or `undefined` when `reply`
|
|
22
|
+
* is not that answer to `slot`.
|
|
23
|
+
*/
|
|
24
|
+
function parseColorReply(reply, slot) {
|
|
25
|
+
if (reply.kind !== "osc")
|
|
26
|
+
return undefined;
|
|
27
|
+
const prefix = `${slot};`;
|
|
28
|
+
if (!reply.body.startsWith(prefix))
|
|
29
|
+
return undefined;
|
|
30
|
+
const match = RGB.exec(reply.body.slice(prefix.length));
|
|
31
|
+
return match === null ? undefined : new ColorRgba(channel(match[1]), channel(match[2]), channel(match[3]));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ask the terminal `app` runs on for its foreground and background, and return
|
|
35
|
+
* the terminal `app` draws on with them in place — its sixteen ANSI colors
|
|
36
|
+
* kept, since nothing asks for those. `undefined` when the terminal did not
|
|
37
|
+
* answer both, so the caller keeps what it had. Set it as `app.terminal`.
|
|
38
|
+
*/
|
|
39
|
+
export async function terminalColors(app) {
|
|
40
|
+
const { replies } = await app.ask(QUERIES);
|
|
41
|
+
const foreground = replies.map((reply) => parseColorReply(reply, 10)).find((color) => color !== undefined);
|
|
42
|
+
const background = replies.map((reply) => parseColorReply(reply, 11)).find((color) => color !== undefined);
|
|
43
|
+
if (foreground === undefined || background === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
return new TerminalColors(background, foreground, app.terminal.ansiColors);
|
|
46
|
+
}
|
|
@@ -69,7 +69,8 @@ export interface TerminalHost {
|
|
|
69
69
|
onData(handler: DataHandler): Unsubscribe;
|
|
70
70
|
/**
|
|
71
71
|
* Where `onData`'s chunks may end. The parser holds an unfinished escape
|
|
72
|
-
* sequence open for its rest
|
|
72
|
+
* sequence open for its rest — for the escape delay, or a control string
|
|
73
|
+
* for as long as an ask awaits its reply — only on a host whose
|
|
73
74
|
* chunks can split one; a whole-keys host settles each chunk as it arrives,
|
|
74
75
|
* because a busy worker there reads a queued next key before any timer
|
|
75
76
|
* runs (rich-host-input-ab9n).
|
|
@@ -95,13 +96,13 @@ export interface TerminalHost {
|
|
|
95
96
|
readonly isTTY: boolean;
|
|
96
97
|
/**
|
|
97
98
|
* Whether the bytes `write` sends land on a terminal, whatever the input
|
|
98
|
-
* is.
|
|
99
|
+
* is. Color detection asks this: a program with piped stdin and a terminal
|
|
99
100
|
* stdout still draws on a terminal, and `isTTY` would call it plain.
|
|
100
101
|
*/
|
|
101
102
|
readonly writesToTerminal: boolean;
|
|
102
103
|
/**
|
|
103
104
|
* The environment a program running on this terminal sees — `TERM`,
|
|
104
|
-
* `COLORTERM`, `NO_COLOR` and the rest.
|
|
105
|
+
* `COLORTERM`, `NO_COLOR` and the rest. Color detection reads it: what a
|
|
105
106
|
* terminal can draw is a fact about the host, so it travels with the host
|
|
106
107
|
* rather than being read off whatever `process` the renderer happens to
|
|
107
108
|
* share a runtime with, which in a browser is none.
|
|
@@ -64,7 +64,7 @@ export class BrowserTerminalHost {
|
|
|
64
64
|
}
|
|
65
65
|
// What xterm.js is, stated the way a terminal states it: it identifies as
|
|
66
66
|
// `xterm-256color` and draws 24-bit SGR. With no env at all, detection saw
|
|
67
|
-
// an unnamed TTY and drew every
|
|
67
|
+
// an unnamed TTY and drew every color at 16-color depth.
|
|
68
68
|
get env() {
|
|
69
69
|
return XTERM_ENV;
|
|
70
70
|
}
|