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