@promptctl/rich-js 0.18.1 → 0.20.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 +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
package/dist/core/color.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal color representation, parsing, downgrading, and ANSI code generation.
|
|
3
3
|
*/
|
|
4
|
+
import { Memo } from "./memo.js";
|
|
4
5
|
// --- ColorRgba ---
|
|
5
6
|
function hex2(byte) {
|
|
6
7
|
return byte.toString(16).padStart(2, "0");
|
|
@@ -73,8 +74,9 @@ export class ColorRgba {
|
|
|
73
74
|
/**
|
|
74
75
|
* The surface a terminal draws a translucent colour over. A terminal cannot
|
|
75
76
|
* know what lies under its cells, so the SGR writer (`Style.toSgrCodes`), the
|
|
76
|
-
*
|
|
77
|
-
*
|
|
77
|
+
* exports that picture its output (`resolveLook`), the strip's seam test and
|
|
78
|
+
* the contrast choosers (`themes/colorMath`) all composite over this one
|
|
79
|
+
* colour. [LAW:one-source-of-truth] One constant, so
|
|
78
80
|
* the colour text is chosen against is the colour the writer draws.
|
|
79
81
|
*/
|
|
80
82
|
export const SURFACE_BLACK = new ColorRgba(0, 0, 0);
|
|
@@ -110,36 +112,27 @@ function luminanceRatio(la, lb) {
|
|
|
110
112
|
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
111
113
|
}
|
|
112
114
|
// --- ColorTable ---
|
|
113
|
-
/**
|
|
114
|
-
* An indexed palette: entry `i` of `colors` is terminal index `firstIndex + i`.
|
|
115
|
-
* `firstIndex` lets a table hold only the part of a palette a downgrade may
|
|
116
|
-
* choose (the 256-colour cube and grey ramp start at 16) while every index it
|
|
117
|
-
* reports is the terminal's own.
|
|
118
|
-
*/
|
|
119
|
-
// [LAW:single-enforcer] The one size policy for ColorTable's memos: a key is
|
|
120
|
-
// derived from colours a long-running host computes without end (ramp stops,
|
|
121
|
-
// mixes), so an unbounded map grows with every render. Clearing at the cap is
|
|
122
|
-
// the policy `cellLen` already uses; a refill costs one table scan per key.
|
|
123
|
-
const TABLE_CACHE_MAX = 4096;
|
|
124
|
-
function remember(cache, key, index) {
|
|
125
|
-
if (cache.size >= TABLE_CACHE_MAX)
|
|
126
|
-
cache.clear();
|
|
127
|
-
cache.set(key, index);
|
|
128
|
-
return index;
|
|
129
|
-
}
|
|
130
115
|
// [LAW:one-source-of-truth] The one distance every table scan ranks by, so
|
|
131
|
-
// `matchWhere`'s "nearest by the distance `match` uses" cannot drift
|
|
132
|
-
|
|
116
|
+
// `matchWhere`'s "nearest by the distance `match` uses" cannot drift, and
|
|
117
|
+
// `Effected` weighs a written colour against `ColorSpec.matchOn` by it too.
|
|
118
|
+
export function rgbDistance(a, b) {
|
|
133
119
|
const dr = a.red - b.red;
|
|
134
120
|
const dg = a.green - b.green;
|
|
135
121
|
const db = a.blue - b.blue;
|
|
136
122
|
return dr * dr + dg * dg + db * db;
|
|
137
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* An indexed palette: entry `i` of `colors` is terminal index `firstIndex + i`.
|
|
126
|
+
* `firstIndex` lets a table hold only the part of a palette a downgrade may
|
|
127
|
+
* choose (the 256-colour cube and grey ramp start at 16) while every index it
|
|
128
|
+
* reports is the terminal's own.
|
|
129
|
+
*/
|
|
138
130
|
export class ColorTable {
|
|
139
131
|
colors;
|
|
140
132
|
firstIndex;
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
// Keyed by colours a host computes without end (ramp stops, mixes), so bounded.
|
|
134
|
+
matchMemo = new Memo();
|
|
135
|
+
readableMemo = new Memo();
|
|
143
136
|
constructor(colors, firstIndex = 0) {
|
|
144
137
|
this.colors = colors;
|
|
145
138
|
this.firstIndex = firstIndex;
|
|
@@ -164,9 +157,9 @@ export class ColorTable {
|
|
|
164
157
|
*/
|
|
165
158
|
match(value) {
|
|
166
159
|
const key = `${value.red},${value.green},${value.blue},${value.alpha}`;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
return this.matchMemo.get(key, () => this.nearest(value));
|
|
161
|
+
}
|
|
162
|
+
nearest(value) {
|
|
170
163
|
let bestIndex = 0;
|
|
171
164
|
let bestDist = Infinity;
|
|
172
165
|
for (let i = 0; i < this.colors.length; i++) {
|
|
@@ -177,7 +170,7 @@ export class ColorTable {
|
|
|
177
170
|
bestIndex = i;
|
|
178
171
|
}
|
|
179
172
|
}
|
|
180
|
-
return
|
|
173
|
+
return this.firstIndex + bestIndex;
|
|
181
174
|
}
|
|
182
175
|
/**
|
|
183
176
|
* The nearest entry to `value` (the distance `match` uses) among those that
|
|
@@ -190,9 +183,9 @@ export class ColorTable {
|
|
|
190
183
|
*/
|
|
191
184
|
matchReadable(value, on, minRatio) {
|
|
192
185
|
const key = `${value.red},${value.green},${value.blue}|${on.red},${on.green},${on.blue}|${minRatio}`;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
186
|
+
return this.readableMemo.get(key, () => this.nearestReadable(value, on, minRatio));
|
|
187
|
+
}
|
|
188
|
+
nearestReadable(value, on, minRatio) {
|
|
196
189
|
const lOn = relativeLuminance(on);
|
|
197
190
|
let best = 0;
|
|
198
191
|
let bestPasses = false;
|
|
@@ -212,7 +205,7 @@ export class ColorTable {
|
|
|
212
205
|
bestScore = score;
|
|
213
206
|
}
|
|
214
207
|
}
|
|
215
|
-
return
|
|
208
|
+
return this.firstIndex + best;
|
|
216
209
|
}
|
|
217
210
|
/**
|
|
218
211
|
* The nearest entry to `value` (the distance `match` uses) among those
|
|
@@ -238,38 +231,62 @@ export var ColorDepth;
|
|
|
238
231
|
ColorDepth[ColorDepth["TRUECOLOR"] = 3] = "TRUECOLOR";
|
|
239
232
|
ColorDepth[ColorDepth["WINDOWS"] = 4] = "WINDOWS";
|
|
240
233
|
})(ColorDepth || (ColorDepth = {}));
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
234
|
+
/**
|
|
235
|
+
* How many colours a depth can name, as a rank. WINDOWS is a sixteen-colour
|
|
236
|
+
* depth whose enum value sits above TRUECOLOR, so the enum's order is not this.
|
|
237
|
+
* STANDARD and WINDOWS share a rank: the same sixteen slots, drawn by
|
|
238
|
+
* different terminals, so each converts to the other by keeping the slot.
|
|
239
|
+
*/
|
|
240
|
+
function fidelity(depth) {
|
|
241
|
+
switch (depth) {
|
|
242
|
+
case ColorDepth.DEFAULT:
|
|
243
|
+
return 0;
|
|
244
|
+
case ColorDepth.STANDARD:
|
|
245
|
+
case ColorDepth.WINDOWS:
|
|
246
|
+
return 1;
|
|
247
|
+
case ColorDepth.EIGHT_BIT:
|
|
248
|
+
return 2;
|
|
249
|
+
case ColorDepth.TRUECOLOR:
|
|
250
|
+
return 3;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// The depths a colour system is named for. Its keys and "auto" are the whole
|
|
254
|
+
// vocabulary: `ColorSystemName` is derived from them, so the type and the
|
|
255
|
+
// table cannot disagree. [LAW:one-source-of-truth]
|
|
256
|
+
const COLOR_SYSTEM_DEPTHS = {
|
|
249
257
|
truecolor: ColorDepth.TRUECOLOR,
|
|
250
258
|
"256": ColorDepth.EIGHT_BIT,
|
|
251
259
|
ansi: ColorDepth.STANDARD,
|
|
252
260
|
none: null,
|
|
261
|
+
};
|
|
262
|
+
// [LAW:one-source-of-truth] What the environment says about colour, by the
|
|
263
|
+
// value it says it with: FORCE_COLOR values (the chalk/supports-color
|
|
264
|
+
// convention), TERM_PROGRAM identifiers, and exact TERM names whose colour
|
|
265
|
+
// capability is known. A different vocabulary from `ColorSystemName`, so a
|
|
266
|
+
// different table: `FORCE_COLOR=none` is not a request for no colour. A `Map`,
|
|
267
|
+
// so a value naming an `Object.prototype` member is unknown, not a function.
|
|
268
|
+
// `null` means "no color".
|
|
269
|
+
const ENV_DEPTHS = new Map([
|
|
253
270
|
// FORCE_COLOR values
|
|
254
|
-
"0"
|
|
255
|
-
false
|
|
256
|
-
"1"
|
|
257
|
-
true
|
|
258
|
-
"2"
|
|
259
|
-
"3"
|
|
271
|
+
["0", null],
|
|
272
|
+
["false", null],
|
|
273
|
+
["1", ColorDepth.STANDARD],
|
|
274
|
+
["true", ColorDepth.STANDARD],
|
|
275
|
+
["2", ColorDepth.EIGHT_BIT],
|
|
276
|
+
["3", ColorDepth.TRUECOLOR],
|
|
260
277
|
// TERM_PROGRAM identifiers
|
|
261
|
-
"iTerm.app"
|
|
262
|
-
Apple_Terminal
|
|
263
|
-
vscode
|
|
264
|
-
Tabby
|
|
278
|
+
["iTerm.app", ColorDepth.TRUECOLOR],
|
|
279
|
+
["Apple_Terminal", ColorDepth.EIGHT_BIT],
|
|
280
|
+
["vscode", ColorDepth.TRUECOLOR],
|
|
281
|
+
["Tabby", ColorDepth.TRUECOLOR],
|
|
265
282
|
// Known truecolor TERM names
|
|
266
|
-
"xterm-kitty"
|
|
267
|
-
"xterm-ghostty"
|
|
268
|
-
wezterm
|
|
269
|
-
alacritty
|
|
270
|
-
foot
|
|
271
|
-
contour
|
|
272
|
-
|
|
283
|
+
["xterm-kitty", ColorDepth.TRUECOLOR],
|
|
284
|
+
["xterm-ghostty", ColorDepth.TRUECOLOR],
|
|
285
|
+
["wezterm", ColorDepth.TRUECOLOR],
|
|
286
|
+
["alacritty", ColorDepth.TRUECOLOR],
|
|
287
|
+
["foot", ColorDepth.TRUECOLOR],
|
|
288
|
+
["contour", ColorDepth.TRUECOLOR],
|
|
289
|
+
]);
|
|
273
290
|
/**
|
|
274
291
|
* Detect the terminal's color capability from env + TTY state.
|
|
275
292
|
*
|
|
@@ -296,12 +313,21 @@ function forcedColorSystem(env) {
|
|
|
296
313
|
const force = env["FORCE_COLOR"];
|
|
297
314
|
if (force === undefined || force === "")
|
|
298
315
|
return undefined;
|
|
299
|
-
const mapped =
|
|
316
|
+
const mapped = ENV_DEPTHS.get(force);
|
|
300
317
|
return mapped !== undefined ? mapped : ColorDepth.STANDARD;
|
|
301
318
|
}
|
|
302
319
|
function envOf(options) {
|
|
303
320
|
return options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
304
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* A terminal that says it cannot be drawn on: `TERM` is `dumb` or `unknown`,
|
|
324
|
+
* as Emacs' shell mode sets it. It takes no colour and nothing that moves the
|
|
325
|
+
* cursor.
|
|
326
|
+
*/
|
|
327
|
+
export function isDumbTerminal(env) {
|
|
328
|
+
const term = env["TERM"];
|
|
329
|
+
return term === "dumb" || term === "unknown";
|
|
330
|
+
}
|
|
305
331
|
// What the destination can draw, preferences aside: TTY presence,
|
|
306
332
|
// TERM=dumb/unknown, COLORTERM, known terminal names, TERM patterns. `null`
|
|
307
333
|
// here means the destination takes no escapes at all.
|
|
@@ -311,19 +337,19 @@ function terminalColorSystem(options) {
|
|
|
311
337
|
(typeof process !== "undefined" ? (process.stdout?.isTTY ?? false) : false);
|
|
312
338
|
if (!isTTY)
|
|
313
339
|
return null;
|
|
314
|
-
|
|
315
|
-
if (term === "dumb" || term === "unknown")
|
|
340
|
+
if (isDumbTerminal(env))
|
|
316
341
|
return null;
|
|
342
|
+
const term = env["TERM"] ?? "";
|
|
317
343
|
const colorterm = env["COLORTERM"];
|
|
318
344
|
if (colorterm === "truecolor" || colorterm === "24bit") {
|
|
319
345
|
return ColorDepth.TRUECOLOR;
|
|
320
346
|
}
|
|
321
|
-
const termDepth =
|
|
347
|
+
const termDepth = ENV_DEPTHS.get(term);
|
|
322
348
|
if (termDepth !== undefined)
|
|
323
349
|
return termDepth;
|
|
324
350
|
const termProgram = env["TERM_PROGRAM"];
|
|
325
351
|
if (termProgram !== undefined) {
|
|
326
|
-
const mapped =
|
|
352
|
+
const mapped = ENV_DEPTHS.get(termProgram);
|
|
327
353
|
if (mapped !== undefined)
|
|
328
354
|
return mapped;
|
|
329
355
|
}
|
|
@@ -364,20 +390,20 @@ export function resolveDestination(spec, options) {
|
|
|
364
390
|
};
|
|
365
391
|
}
|
|
366
392
|
/**
|
|
367
|
-
* Resolve a
|
|
393
|
+
* Resolve a colour system name into a `ColorDepth` (or `null` for no color).
|
|
368
394
|
*
|
|
369
|
-
* `"auto"` triggers env-based detection;
|
|
370
|
-
*
|
|
371
|
-
*
|
|
395
|
+
* `"auto"` triggers env-based detection; every other name is a fixed depth.
|
|
396
|
+
* A name outside the vocabulary — reachable only from untyped JavaScript —
|
|
397
|
+
* throws: silent fallback would mask a typo.
|
|
372
398
|
*
|
|
373
|
-
* [LAW:single-enforcer] All
|
|
399
|
+
* [LAW:single-enforcer] All name→ColorDepth resolution flows through here.
|
|
374
400
|
*/
|
|
375
401
|
export function resolveColorSystem(spec, options) {
|
|
376
402
|
if (spec === "auto")
|
|
377
403
|
return detectColorSystem(options);
|
|
378
|
-
if (Object.hasOwn(
|
|
379
|
-
return
|
|
380
|
-
throw new ColorParseError(`Unknown color
|
|
404
|
+
if (Object.hasOwn(COLOR_SYSTEM_DEPTHS, spec))
|
|
405
|
+
return COLOR_SYSTEM_DEPTHS[spec];
|
|
406
|
+
throw new ColorParseError(`Unknown color system name: ${JSON.stringify(spec)} (expected "auto", "truecolor", "256", "ansi", or "none")`);
|
|
381
407
|
}
|
|
382
408
|
// --- ColorSpec ---
|
|
383
409
|
export class ColorParseError extends Error {
|
|
@@ -386,8 +412,8 @@ export class ColorParseError extends Error {
|
|
|
386
412
|
this.name = "ColorParseError";
|
|
387
413
|
}
|
|
388
414
|
}
|
|
389
|
-
// [LAW:one-source-of-truth]
|
|
390
|
-
const
|
|
415
|
+
// [LAW:one-source-of-truth] The single source for parsed ColorSpec instances.
|
|
416
|
+
const colorSpecParseMemo = new Memo();
|
|
391
417
|
export class ColorSpec {
|
|
392
418
|
name;
|
|
393
419
|
type;
|
|
@@ -408,8 +434,9 @@ export class ColorSpec {
|
|
|
408
434
|
* any alpha is flattened, so a translucent value is drawn composited over
|
|
409
435
|
* its ground (`flattenAlpha`) and should be measured that way: a
|
|
410
436
|
* truecolor value, or a 256-colour cube or grey-ramp entry (xterm fixes
|
|
411
|
-
* indices 16–255, and `fromAnsi` types only those as EIGHT_BIT). ANSI 0–15
|
|
412
|
-
*
|
|
437
|
+
* indices 16–255, and `fromAnsi` types only those as EIGHT_BIT). ANSI 0–15,
|
|
438
|
+
* the default colour and a blend reaching either are the terminal theme's
|
|
439
|
+
* own, so they have none.
|
|
413
440
|
*/
|
|
414
441
|
get fixedValue() {
|
|
415
442
|
switch (this.type) {
|
|
@@ -489,7 +516,7 @@ export class ColorSpec {
|
|
|
489
516
|
downgrade(targetSystem) {
|
|
490
517
|
if (this.type === ColorDepth.DEFAULT)
|
|
491
518
|
return this;
|
|
492
|
-
if (this.type
|
|
519
|
+
if (this.type === targetSystem || fidelity(this.type) < fidelity(targetSystem))
|
|
493
520
|
return this;
|
|
494
521
|
const cached = this.downgradeCache.get(targetSystem);
|
|
495
522
|
if (cached)
|
|
@@ -507,19 +534,14 @@ export class ColorSpec {
|
|
|
507
534
|
return this.value;
|
|
508
535
|
case ColorDepth.EIGHT_BIT:
|
|
509
536
|
// Slots 0–15 are the theme's own whatever depth names them, as `fixedValue` says.
|
|
510
|
-
return this.number < 16 ? (theme
|
|
511
|
-
case ColorDepth.STANDARD:
|
|
512
|
-
|
|
513
|
-
return
|
|
514
|
-
}
|
|
537
|
+
return this.number < 16 ? resolveTerminal(theme, this.type).ansiColors.get(this.number) : EIGHT_BIT_TABLE.get(this.number);
|
|
538
|
+
case ColorDepth.STANDARD:
|
|
539
|
+
case ColorDepth.WINDOWS:
|
|
540
|
+
return resolveTerminal(theme, this.type).ansiColors.get(this.number);
|
|
515
541
|
case ColorDepth.DEFAULT: {
|
|
516
|
-
const t = theme
|
|
542
|
+
const t = resolveTerminal(theme, this.type);
|
|
517
543
|
return foreground ? t.foregroundColor : t.backgroundColor;
|
|
518
544
|
}
|
|
519
|
-
case ColorDepth.WINDOWS: {
|
|
520
|
-
const t = theme ?? INTERNAL_DEFAULT_THEME;
|
|
521
|
-
return t.ansiColors.get(this.number);
|
|
522
|
-
}
|
|
523
545
|
}
|
|
524
546
|
}
|
|
525
547
|
// --- Static factories ---
|
|
@@ -537,34 +559,83 @@ export class ColorSpec {
|
|
|
537
559
|
return ColorSpec.fromRgba(new ColorRgba(r, g, b));
|
|
538
560
|
}
|
|
539
561
|
/**
|
|
540
|
-
*
|
|
562
|
+
* The colour `t` of the way from `from` to `to`: what `blend(from,to,t)`
|
|
563
|
+
* parses to. Two fixed colours mix to a fixed colour. A mix reaching a
|
|
564
|
+
* theme's own colour stays a mix until it is drawn, so whatever theme draws
|
|
565
|
+
* an end draws the mix from it: exported under a theme whose `blue` is
|
|
566
|
+
* #61afef, a mix with `blue` starts from #61afef. A terminal is told its RGB
|
|
567
|
+
* under the standard table, the only one a render has.
|
|
568
|
+
*
|
|
569
|
+
* Each end is one opaque colour: `default` is no colour to mix, and a
|
|
570
|
+
* translucent end has no one shade until it is laid on a ground.
|
|
571
|
+
*/
|
|
572
|
+
static blend(from, to, t) {
|
|
573
|
+
for (const end of [from, to]) {
|
|
574
|
+
if (end.isDefault)
|
|
575
|
+
throw new ColorParseError(`A blend cannot mix "default", which is no colour`);
|
|
576
|
+
if (end.value !== undefined && end.value.alpha < 1) {
|
|
577
|
+
throw new ColorParseError(`A blend's ends are opaque; "${end.name}" is translucent`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (!(t >= 0 && t <= 1))
|
|
581
|
+
throw new ColorParseError(`A blend's fraction is in [0, 1]; got ${t}`);
|
|
582
|
+
const a = from.fixedValue;
|
|
583
|
+
const b = to.fixedValue;
|
|
584
|
+
return a !== undefined && b !== undefined ? ColorSpec.fromRgba(blendRgb(a, b, t)) : new ColorBlend(from, to, t);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* The spec that draws `value` nearest to it at `depth` on a terminal known
|
|
588
|
+
* to show `theme`, as the colour of the glyph (`foreground`) or of the
|
|
589
|
+
* ground. Where `downgrade` knows no terminal, and so rounds only against
|
|
590
|
+
* colours every terminal fixes (or, at sixteen colours, the stock
|
|
591
|
+
* sixteen), this rounds against every colour the depth can write whose
|
|
592
|
+
* look `theme` fixes: the default colour, the theme's sixteen, and at 256
|
|
593
|
+
* colours the cube and grey ramp too. A colour read off any of those and
|
|
594
|
+
* moved a little is drawn as the colour it came from — or, where the theme
|
|
595
|
+
* gives two slots one colour, as the lower slot, which looks the same.
|
|
596
|
+
*
|
|
597
|
+
* `value` is opaque: a translucent colour has no one look until it is laid
|
|
598
|
+
* on a ground (`flattenAlpha`).
|
|
599
|
+
*/
|
|
600
|
+
static matchOn(value, depth, theme, foreground) {
|
|
601
|
+
if (value.alpha < 1)
|
|
602
|
+
throw new RangeError(`matchOn rounds an opaque colour; got alpha ${value.alpha}`);
|
|
603
|
+
if (depth === ColorDepth.TRUECOLOR)
|
|
604
|
+
return ColorSpec.fromRgba(value);
|
|
605
|
+
if (depth === ColorDepth.DEFAULT)
|
|
606
|
+
return ColorSpec.default();
|
|
607
|
+
const slot = theme.ansiColors.match(value);
|
|
608
|
+
// [LAW:dataflow-not-control-flow] Every candidate is scored; the depth
|
|
609
|
+
// only decides whether the cube is among them. Ties go to the earlier.
|
|
610
|
+
const candidates = [
|
|
611
|
+
[ColorSpec.default(), foreground ? theme.foregroundColor : theme.backgroundColor],
|
|
612
|
+
[new ColorSpec(`color(${slot})`, depth === ColorDepth.WINDOWS ? depth : ColorDepth.STANDARD, slot), theme.ansiColors.get(slot)],
|
|
613
|
+
...(depth === ColorDepth.EIGHT_BIT ? [cubeCandidate(value)] : []),
|
|
614
|
+
];
|
|
615
|
+
return candidates.reduce((best, next) => (rgbDistance(next[1], value) < rgbDistance(best[1], value) ? next : best))[0];
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Parse a color string. Memoized (`Memo`): a string parsed again while
|
|
619
|
+
* remembered returns the same instance.
|
|
541
620
|
*/
|
|
542
621
|
static parse(colorString) {
|
|
543
|
-
|
|
544
|
-
const cached = parseCache.get(key);
|
|
545
|
-
if (cached)
|
|
546
|
-
return cached;
|
|
547
|
-
const result = parseSingle(key);
|
|
548
|
-
parseCache.set(key, result);
|
|
549
|
-
return result;
|
|
622
|
+
return colorSpecParseMemo.get(colorString.toLowerCase().trim(), parseSingle);
|
|
550
623
|
}
|
|
551
624
|
// --- private ---
|
|
552
625
|
performDowngrade(targetSystem) {
|
|
553
|
-
//
|
|
554
|
-
|
|
626
|
+
// The colour as the terminal this depth assumes draws it: a blend's named
|
|
627
|
+
// ends are the Windows console's own at WINDOWS.
|
|
628
|
+
const triplet = this.getTruecolor(resolveTerminal(undefined, targetSystem));
|
|
555
629
|
switch (targetSystem) {
|
|
556
|
-
case ColorDepth.EIGHT_BIT:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
630
|
+
case ColorDepth.EIGHT_BIT:
|
|
631
|
+
return ColorSpec.fromAnsi(downgradeTable(targetSystem).match(triplet));
|
|
632
|
+
case ColorDepth.STANDARD:
|
|
633
|
+
case ColorDepth.WINDOWS: {
|
|
634
|
+
// An ANSI slot is already one of the sixteen either depth writes; only
|
|
635
|
+
// a fixed colour is matched against the depth's table, as Rich does.
|
|
636
|
+
const index = this.number !== undefined && this.number < 16 ? this.number : downgradeTable(targetSystem).match(triplet);
|
|
637
|
+
return new ColorSpec(`color(${index})`, targetSystem, index);
|
|
563
638
|
}
|
|
564
|
-
// WINDOWS is a detection result (max enum value), not a downgrade target.
|
|
565
|
-
// The `this.type <= targetSystem` guard in downgrade() always short-circuits
|
|
566
|
-
// before reaching this method with WINDOWS, so this case is unreachable.
|
|
567
|
-
case ColorDepth.WINDOWS:
|
|
568
639
|
case ColorDepth.TRUECOLOR:
|
|
569
640
|
return this;
|
|
570
641
|
case ColorDepth.DEFAULT:
|
|
@@ -572,11 +643,47 @@ export class ColorSpec {
|
|
|
572
643
|
}
|
|
573
644
|
}
|
|
574
645
|
}
|
|
646
|
+
/** The cube or grey-ramp entry nearest `value`, and its colour. */
|
|
647
|
+
function cubeCandidate(value) {
|
|
648
|
+
const index = EIGHT_BIT_DOWNGRADE_TABLE.match(value);
|
|
649
|
+
return [ColorSpec.fromAnsi(index), EIGHT_BIT_DOWNGRADE_TABLE.get(index)];
|
|
650
|
+
}
|
|
651
|
+
function mix(from, to, t, theme) {
|
|
652
|
+
return blendRgb(from.getTruecolor(theme), to.getTruecolor(theme), t);
|
|
653
|
+
}
|
|
654
|
+
// How a blend spells an end: by what it draws as, so every spelling of one
|
|
655
|
+
// colour ("blue", "color(4)", an EIGHT_BIT 4) gives the blend one name.
|
|
656
|
+
function blendTerm(end) {
|
|
657
|
+
return end.fixedValue?.hex ?? (end.number !== undefined ? `color(${end.number})` : end.name);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* `ColorSpec.blend` with an end the theme draws. A terminal is told it as a
|
|
661
|
+
* truecolor value, so it is one, and `value` is what it is told; a theme is
|
|
662
|
+
* asked of its two ends instead. Its name is what `ColorSpec.parse` reads back.
|
|
663
|
+
*/
|
|
664
|
+
class ColorBlend extends ColorSpec {
|
|
665
|
+
from;
|
|
666
|
+
to;
|
|
667
|
+
t;
|
|
668
|
+
constructor(from, to, t) {
|
|
669
|
+
super(`blend(${blendTerm(from)},${blendTerm(to)},${t})`, ColorDepth.TRUECOLOR, undefined, mix(from, to, t));
|
|
670
|
+
this.from = from;
|
|
671
|
+
this.to = to;
|
|
672
|
+
this.t = t;
|
|
673
|
+
}
|
|
674
|
+
get fixedValue() {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
getTruecolor(theme) {
|
|
678
|
+
return mix(this.from, this.to, this.t, theme);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
575
681
|
// --- Parsing internals ---
|
|
576
|
-
const HEX_RE = /^#([0-9a-f]{6})$/;
|
|
577
|
-
const HEX_RGBA_RE = /^#([0-9a-f]{8})$/;
|
|
578
682
|
const RGB_RE = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
|
|
579
683
|
const COLOR_NUMBER_RE = /^color\((\d+)\)$/;
|
|
684
|
+
const BLEND_RE = /^blend\((.*)\)$/;
|
|
685
|
+
// A decimal as `String(number)` spells one, exponent included; `blend` owns its range.
|
|
686
|
+
const FRACTION_RE = /^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?\s*$/;
|
|
580
687
|
function parseSingle(key) {
|
|
581
688
|
if (key === "default" || key === "") {
|
|
582
689
|
return ColorSpec.default();
|
|
@@ -587,15 +694,8 @@ function parseSingle(key) {
|
|
|
587
694
|
const type = namedIndex < 16 ? ColorDepth.STANDARD : ColorDepth.EIGHT_BIT;
|
|
588
695
|
return new ColorSpec(key, type, namedIndex);
|
|
589
696
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const hexRgbaMatch = HEX_RGBA_RE.exec(key);
|
|
593
|
-
if (hexRgbaMatch) {
|
|
594
|
-
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, parseRgbaHex(hexRgbaMatch[1]));
|
|
595
|
-
}
|
|
596
|
-
const hexMatch = HEX_RE.exec(key);
|
|
597
|
-
if (hexMatch) {
|
|
598
|
-
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, parseRgbHex(hexMatch[1]));
|
|
697
|
+
if (key.startsWith("#")) {
|
|
698
|
+
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, parseHexColor(key));
|
|
599
699
|
}
|
|
600
700
|
// rgb()
|
|
601
701
|
const rgbMatch = RGB_RE.exec(key);
|
|
@@ -612,8 +712,38 @@ function parseSingle(key) {
|
|
|
612
712
|
const type = n < 16 ? ColorDepth.STANDARD : ColorDepth.EIGHT_BIT;
|
|
613
713
|
return new ColorSpec(key, type, n);
|
|
614
714
|
}
|
|
715
|
+
// blend(a,b,t)
|
|
716
|
+
const blendMatch = BLEND_RE.exec(key);
|
|
717
|
+
if (blendMatch) {
|
|
718
|
+
const terms = blendTerms(blendMatch[1]);
|
|
719
|
+
const fraction = terms[2] ?? "";
|
|
720
|
+
if (terms.length !== 3 || !FRACTION_RE.test(fraction)) {
|
|
721
|
+
throw new ColorParseError(`ColorSpec "${key}": a blend is blend(<color>,<color>,<fraction>)`);
|
|
722
|
+
}
|
|
723
|
+
return ColorSpec.blend(ColorSpec.parse(terms[0]), ColorSpec.parse(terms[1]), Number(fraction));
|
|
724
|
+
}
|
|
615
725
|
throw new ColorParseError(`Failed to parse color: "${key}"`);
|
|
616
726
|
}
|
|
727
|
+
// A blend's terms, split at the commas outside parentheses so an `rgb(…)` or a
|
|
728
|
+
// nested `blend(…)` stays one term.
|
|
729
|
+
function blendTerms(inner) {
|
|
730
|
+
const terms = [];
|
|
731
|
+
let depth = 0;
|
|
732
|
+
let start = 0;
|
|
733
|
+
for (let i = 0; i < inner.length; i++) {
|
|
734
|
+
const c = inner[i];
|
|
735
|
+
if (c === "(")
|
|
736
|
+
depth++;
|
|
737
|
+
else if (c === ")")
|
|
738
|
+
depth--;
|
|
739
|
+
else if (c === "," && depth === 0) {
|
|
740
|
+
terms.push(inner.slice(start, i));
|
|
741
|
+
start = i + 1;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
terms.push(inner.slice(start));
|
|
745
|
+
return terms;
|
|
746
|
+
}
|
|
617
747
|
// [LAW:single-enforcer] The regexes admit any run of digits and leave the
|
|
618
748
|
// range to this check alone, so every out-of-range byte is a ColorParseError
|
|
619
749
|
// naming the field, never the generic parse failure or the constructor's RangeError.
|
|
@@ -625,26 +755,45 @@ function parseByte(key, digits, field) {
|
|
|
625
755
|
return n;
|
|
626
756
|
}
|
|
627
757
|
// --- Utility functions ---
|
|
758
|
+
// [LAW:single-enforcer] The hex colour grammar, checked here and nowhere else:
|
|
759
|
+
// every string spelling a colour in hex — a ColorSpec, a theme's data, a colour
|
|
760
|
+
// reference, a template argument — becomes one through the three parsers below,
|
|
761
|
+
// and each is this check at the lengths it admits. Each digit pair is a byte, so
|
|
762
|
+
// the grammar holding is what keeps `ColorRgba`'s channels in range.
|
|
763
|
+
const HEX_DIGITS_RE = /^[0-9a-fA-F]+$/;
|
|
764
|
+
// Each admitted digit count, by the grammar it spells: the message a failure
|
|
765
|
+
// carries is read off the lengths a parser admits, so the two cannot disagree.
|
|
766
|
+
const HEX_GRAMMARS = { 6: "RRGGBB", 8: "RRGGBBAA" };
|
|
767
|
+
function hexColor(input, lead, digits, lengths) {
|
|
768
|
+
if (!lengths.includes(digits.length) || !HEX_DIGITS_RE.test(digits)) {
|
|
769
|
+
const expected = lengths.map((n) => lead + HEX_GRAMMARS[n]).join(" or ");
|
|
770
|
+
throw new ColorParseError(`Invalid hex colour ${JSON.stringify(input)} (expected ${expected})`);
|
|
771
|
+
}
|
|
772
|
+
const byte = (at) => parseInt(digits.slice(at, at + 2), 16);
|
|
773
|
+
return new ColorRgba(byte(0), byte(2), byte(4), digits.length === 8 ? byte(6) / 255 : 1);
|
|
774
|
+
}
|
|
775
|
+
/** Six hex digits, no `#`, as an opaque colour. @throws {ColorParseError} on anything else. */
|
|
628
776
|
export function parseRgbHex(hex) {
|
|
629
|
-
|
|
630
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
631
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
632
|
-
return new ColorRgba(r, g, b);
|
|
777
|
+
return hexColor(hex, "", hex, [6]);
|
|
633
778
|
}
|
|
779
|
+
/** Eight hex digits, no `#`, the last pair alpha. @throws {ColorParseError} on anything else. */
|
|
634
780
|
export function parseRgbaHex(hex) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
781
|
+
return hexColor(hex, "", hex, [8]);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* A `#RRGGBB` or `#RRGGBBAA` literal, exactly — no surrounding whitespace.
|
|
785
|
+
* @throws {ColorParseError} on anything else.
|
|
786
|
+
*/
|
|
787
|
+
export function parseHexColor(literal) {
|
|
788
|
+
return hexColor(literal, "#", literal.startsWith("#") ? literal.slice(1) : "", [6, 8]);
|
|
640
789
|
}
|
|
641
790
|
export function blendRgb(color1, color2, crossFade = 0.5) {
|
|
642
791
|
return new ColorRgba(Math.round(color1.red + (color2.red - color1.red) * crossFade), Math.round(color1.green + (color2.green - color1.green) * crossFade), Math.round(color1.blue + (color2.blue - color1.blue) * crossFade), color1.alpha + (color2.alpha - color1.alpha) * crossFade);
|
|
643
792
|
}
|
|
644
793
|
// --- TerminalTheme ---
|
|
645
794
|
/**
|
|
646
|
-
* A terminal theme — surface/foreground baseline, the ANSI
|
|
647
|
-
* semantic palette.
|
|
795
|
+
* A terminal theme — surface/foreground baseline, the sixteen ANSI colours,
|
|
796
|
+
* and a semantic palette.
|
|
648
797
|
*
|
|
649
798
|
* **`ansiColors` is the theme's own sixteen colours**, the shades a terminal
|
|
650
799
|
* showing this theme draws `red`, `blue` and the rest in. `ColorSpec.parse("red")`
|
|
@@ -661,6 +810,11 @@ export class TerminalTheme {
|
|
|
661
810
|
this.foregroundColor = foregroundColor;
|
|
662
811
|
this.ansiColors = ansiColors;
|
|
663
812
|
this.palette = palette;
|
|
813
|
+
// [LAW:parse-dont-validate] Every reader indexes `ansiColors` 0–15 as the
|
|
814
|
+
// terminal's sixteen, so a theme is the proof it holds exactly those.
|
|
815
|
+
if (ansiColors.firstIndex !== 0 || ansiColors.size !== 16) {
|
|
816
|
+
throw new RangeError(`a TerminalTheme's ansiColors are the sixteen ANSI colours, indices 0–15; got ${ansiColors.size} from index ${ansiColors.firstIndex}`);
|
|
817
|
+
}
|
|
664
818
|
}
|
|
665
819
|
}
|
|
666
820
|
// --- ColorTable data ---
|
|
@@ -702,24 +856,29 @@ function build256Table() {
|
|
|
702
856
|
}
|
|
703
857
|
return colors;
|
|
704
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* The Windows console's own sixteen (its default Campbell scheme), in ANSI
|
|
861
|
+
* index order: a WINDOWS colour is written as the same `30+n`/`90+n` SGR a
|
|
862
|
+
* STANDARD one is, so slot `n` here is the colour the console draws for it.
|
|
863
|
+
*/
|
|
705
864
|
function buildWindowsTable() {
|
|
706
865
|
return [
|
|
707
|
-
new ColorRgba(
|
|
708
|
-
new ColorRgba(
|
|
709
|
-
new ColorRgba(
|
|
710
|
-
new ColorRgba(
|
|
711
|
-
new ColorRgba(
|
|
712
|
-
new ColorRgba(
|
|
713
|
-
new ColorRgba(
|
|
714
|
-
new ColorRgba(
|
|
715
|
-
new ColorRgba(
|
|
716
|
-
new ColorRgba(
|
|
717
|
-
new ColorRgba(
|
|
718
|
-
new ColorRgba(
|
|
719
|
-
new ColorRgba(
|
|
720
|
-
new ColorRgba(
|
|
721
|
-
new ColorRgba(
|
|
722
|
-
new ColorRgba(
|
|
866
|
+
new ColorRgba(12, 12, 12), // 0 black
|
|
867
|
+
new ColorRgba(197, 15, 31), // 1 red
|
|
868
|
+
new ColorRgba(19, 161, 14), // 2 green
|
|
869
|
+
new ColorRgba(193, 156, 0), // 3 yellow
|
|
870
|
+
new ColorRgba(0, 55, 218), // 4 blue
|
|
871
|
+
new ColorRgba(136, 23, 152), // 5 magenta
|
|
872
|
+
new ColorRgba(58, 150, 221), // 6 cyan
|
|
873
|
+
new ColorRgba(204, 204, 204), // 7 white
|
|
874
|
+
new ColorRgba(118, 118, 118), // 8 bright_black
|
|
875
|
+
new ColorRgba(231, 72, 86), // 9 bright_red
|
|
876
|
+
new ColorRgba(22, 198, 12), // 10 bright_green
|
|
877
|
+
new ColorRgba(249, 241, 165), // 11 bright_yellow
|
|
878
|
+
new ColorRgba(59, 120, 255), // 12 bright_blue
|
|
879
|
+
new ColorRgba(180, 0, 158), // 13 bright_magenta
|
|
880
|
+
new ColorRgba(97, 214, 214), // 14 bright_cyan
|
|
881
|
+
new ColorRgba(242, 242, 242), // 15 bright_white
|
|
723
882
|
];
|
|
724
883
|
}
|
|
725
884
|
export const STANDARD_TABLE = new ColorTable(buildStandard16());
|
|
@@ -739,14 +898,43 @@ export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
|
|
|
739
898
|
// `themes/`. Keeping them out of `core/color.ts` makes the dependency strictly
|
|
740
899
|
// `themes/* -> core/color`, with no runtime back-edge.
|
|
741
900
|
//
|
|
742
|
-
// `getTruecolor()` retains a fallback for callers that omit `theme
|
|
743
|
-
//
|
|
744
|
-
// for
|
|
901
|
+
// `getTruecolor()` retains a fallback for callers that omit `theme`: the
|
|
902
|
+
// terminal a colour's depth assumes, built from that depth's own sixteen with
|
|
903
|
+
// an empty Palette — sufficient for the lookups it serves, which never read
|
|
904
|
+
// `palette`.
|
|
745
905
|
//
|
|
746
906
|
// Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
|
|
747
907
|
// only `import type`s from `core/color.ts`, so there is no runtime cycle.
|
|
748
908
|
import { Palette } from "../themes/palette.js";
|
|
749
909
|
const INTERNAL_DEFAULT_THEME = new TerminalTheme(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE, new Palette("default", true, new Map()));
|
|
910
|
+
/** The Windows console in its default Campbell scheme: a WINDOWS colour's terminal. */
|
|
911
|
+
const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE, new Palette("windows", true, new Map()));
|
|
912
|
+
/**
|
|
913
|
+
* The terminal a colour at `depth` is drawn by: `theme`, or when a caller names
|
|
914
|
+
* none, the one the depth assumes — the Windows console at WINDOWS, the VGA
|
|
915
|
+
* sixteen otherwise — for `ColorSpec.getTruecolor` and the contrast measurement
|
|
916
|
+
* that reads it. (Widgets fall back to `DEFAULT_TERMINAL_THEME`, which carries a
|
|
917
|
+
* palette this does not.)
|
|
918
|
+
*/
|
|
919
|
+
export function resolveTerminal(theme, depth) {
|
|
920
|
+
return theme ?? (depth === ColorDepth.WINDOWS ? WINDOWS_CONSOLE_THEME : INTERNAL_DEFAULT_THEME);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* The table a downgrade to `depth` rounds a fixed colour against.
|
|
924
|
+
* [LAW:one-source-of-truth] `ColorSpec.downgrade` picks the index a colour is
|
|
925
|
+
* written as from it, and colorMath hands back its entries as colours the
|
|
926
|
+
* writer rounds to those same indices.
|
|
927
|
+
*/
|
|
928
|
+
export function downgradeTable(depth) {
|
|
929
|
+
switch (depth) {
|
|
930
|
+
case ColorDepth.EIGHT_BIT:
|
|
931
|
+
return EIGHT_BIT_DOWNGRADE_TABLE;
|
|
932
|
+
case ColorDepth.STANDARD:
|
|
933
|
+
return STANDARD_TABLE;
|
|
934
|
+
case ColorDepth.WINDOWS:
|
|
935
|
+
return WINDOWS_TABLE;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
750
938
|
// --- ANSI ColorSpec Names ---
|
|
751
939
|
// [LAW:one-source-of-truth] Single canonical mapping from name → palette index
|
|
752
940
|
/**
|