@promptctl/rich-js 0.17.0 → 0.18.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 +13 -26
- package/package.json +7 -3
- package/dist/core/box.d.ts +0 -144
- package/dist/core/box.js +0 -391
- package/dist/core/cells.d.ts +0 -148
- package/dist/core/cells.js +0 -266
- package/dist/core/color.d.ts +0 -250
- package/dist/core/color.js +0 -1029
- package/dist/core/console.d.ts +0 -156
- package/dist/core/console.js +0 -472
- package/dist/core/emoji.d.ts +0 -24
- package/dist/core/emoji.js +0 -391
- package/dist/core/export-html.d.ts +0 -24
- package/dist/core/export-html.js +0 -106
- package/dist/core/export-lines.d.ts +0 -121
- package/dist/core/export-lines.js +0 -141
- package/dist/core/highlighter.d.ts +0 -33
- package/dist/core/highlighter.js +0 -124
- package/dist/core/json.d.ts +0 -27
- package/dist/core/json.js +0 -53
- package/dist/core/markup.d.ts +0 -97
- package/dist/core/markup.js +0 -619
- package/dist/core/measure.d.ts +0 -27
- package/dist/core/measure.js +0 -61
- package/dist/core/oklch.d.ts +0 -161
- package/dist/core/oklch.js +0 -312
- package/dist/core/osc8.d.ts +0 -68
- package/dist/core/osc8.js +0 -90
- package/dist/core/pretty.d.ts +0 -141
- package/dist/core/pretty.js +0 -472
- package/dist/core/protocol.d.ts +0 -137
- package/dist/core/protocol.js +0 -101
- package/dist/core/render.d.ts +0 -84
- package/dist/core/render.js +0 -145
- package/dist/core/segment.d.ts +0 -142
- package/dist/core/segment.js +0 -433
- package/dist/core/spinnerData.d.ts +0 -10
- package/dist/core/spinnerData.js +0 -115
- package/dist/core/strip.d.ts +0 -145
- package/dist/core/strip.js +0 -308
- package/dist/core/style.d.ts +0 -144
- package/dist/core/style.js +0 -723
- package/dist/core/subscription.d.ts +0 -12
- package/dist/core/subscription.js +0 -12
- package/dist/core/text.d.ts +0 -255
- package/dist/core/text.js +0 -990
- package/dist/core/wrap.d.ts +0 -41
- package/dist/core/wrap.js +0 -127
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/host/index.d.ts +0 -3
- package/dist/host/index.js +0 -2
- package/dist/host/terminal-host.d.ts +0 -143
- package/dist/host/terminal-host.js +0 -132
- package/dist/index.d.ts +0 -87
- package/dist/index.js +0 -104
- package/dist/node/prompt.d.ts +0 -21
- package/dist/node/prompt.js +0 -30
- package/dist/node/save.d.ts +0 -17
- package/dist/node/save.js +0 -21
- package/dist/node/terminal-host.d.ts +0 -84
- package/dist/node/terminal-host.js +0 -160
- package/dist/node/traceback.d.ts +0 -27
- package/dist/node/traceback.js +0 -112
- package/dist/renderables/align.d.ts +0 -16
- package/dist/renderables/align.js +0 -46
- package/dist/renderables/columns.d.ts +0 -61
- package/dist/renderables/columns.js +0 -168
- package/dist/renderables/constrain.d.ts +0 -15
- package/dist/renderables/constrain.js +0 -37
- package/dist/renderables/flexStrip.d.ts +0 -41
- package/dist/renderables/flexStrip.js +0 -200
- package/dist/renderables/group.d.ts +0 -11
- package/dist/renderables/group.js +0 -15
- package/dist/renderables/layout.d.ts +0 -92
- package/dist/renderables/layout.js +0 -315
- package/dist/renderables/live.d.ts +0 -47
- package/dist/renderables/live.js +0 -131
- package/dist/renderables/markdown.d.ts +0 -24
- package/dist/renderables/markdown.js +0 -182
- package/dist/renderables/padding.d.ts +0 -38
- package/dist/renderables/padding.js +0 -140
- package/dist/renderables/panel.d.ts +0 -116
- package/dist/renderables/panel.js +0 -322
- package/dist/renderables/progress.d.ts +0 -90
- package/dist/renderables/progress.js +0 -241
- package/dist/renderables/progressBar.d.ts +0 -31
- package/dist/renderables/progressBar.js +0 -52
- package/dist/renderables/prompt.d.ts +0 -49
- package/dist/renderables/prompt.js +0 -125
- package/dist/renderables/rule.d.ts +0 -24
- package/dist/renderables/rule.js +0 -90
- package/dist/renderables/spinner.d.ts +0 -29
- package/dist/renderables/spinner.js +0 -64
- package/dist/renderables/status.d.ts +0 -23
- package/dist/renderables/status.js +0 -60
- package/dist/renderables/syntax.d.ts +0 -35
- package/dist/renderables/syntax.js +0 -114
- package/dist/renderables/table.d.ts +0 -182
- package/dist/renderables/table.js +0 -697
- package/dist/renderables/traceback.d.ts +0 -31
- package/dist/renderables/traceback.js +0 -109
- package/dist/renderables/tree.d.ts +0 -54
- package/dist/renderables/tree.js +0 -159
- package/dist/template-bindings/color-funcs.d.ts +0 -87
- package/dist/template-bindings/color-funcs.js +0 -219
- package/dist/template-bindings/helpers.d.ts +0 -24
- package/dist/template-bindings/helpers.js +0 -48
- package/dist/template-bindings/index.d.ts +0 -107
- package/dist/template-bindings/index.js +0 -144
- package/dist/template-bindings/palette-funcs.d.ts +0 -94
- package/dist/template-bindings/palette-funcs.js +0 -151
- package/dist/template-bindings/style-funcs.d.ts +0 -28
- package/dist/template-bindings/style-funcs.js +0 -151
- package/dist/themes/buildPalette.d.ts +0 -36
- package/dist/themes/buildPalette.js +0 -56
- package/dist/themes/colorMath.d.ts +0 -82
- package/dist/themes/colorMath.js +0 -254
- package/dist/themes/colorRef.d.ts +0 -51
- package/dist/themes/colorRef.js +0 -96
- package/dist/themes/data/atom-one-dark.d.ts +0 -3
- package/dist/themes/data/atom-one-dark.js +0 -158
- package/dist/themes/data/atom-one-light.d.ts +0 -3
- package/dist/themes/data/atom-one-light.js +0 -158
- package/dist/themes/data/catppuccin-frappe.d.ts +0 -3
- package/dist/themes/data/catppuccin-frappe.js +0 -158
- package/dist/themes/data/catppuccin-latte.d.ts +0 -3
- package/dist/themes/data/catppuccin-latte.js +0 -158
- package/dist/themes/data/catppuccin-macchiato.d.ts +0 -3
- package/dist/themes/data/catppuccin-macchiato.js +0 -158
- package/dist/themes/data/catppuccin-mocha.d.ts +0 -3
- package/dist/themes/data/catppuccin-mocha.js +0 -158
- package/dist/themes/data/cyberpunk.d.ts +0 -17
- package/dist/themes/data/cyberpunk.js +0 -191
- package/dist/themes/data/default.d.ts +0 -3
- package/dist/themes/data/default.js +0 -158
- package/dist/themes/data/dracula.d.ts +0 -3
- package/dist/themes/data/dracula.js +0 -158
- package/dist/themes/data/flexoki.d.ts +0 -3
- package/dist/themes/data/flexoki.js +0 -158
- package/dist/themes/data/gruvbox.d.ts +0 -3
- package/dist/themes/data/gruvbox.js +0 -158
- package/dist/themes/data/index.d.ts +0 -35
- package/dist/themes/data/index.js +0 -64
- package/dist/themes/data/monokai.d.ts +0 -3
- package/dist/themes/data/monokai.js +0 -158
- package/dist/themes/data/nord.d.ts +0 -3
- package/dist/themes/data/nord.js +0 -158
- package/dist/themes/data/rose-pine-dawn.d.ts +0 -3
- package/dist/themes/data/rose-pine-dawn.js +0 -158
- package/dist/themes/data/rose-pine-moon.d.ts +0 -3
- package/dist/themes/data/rose-pine-moon.js +0 -158
- package/dist/themes/data/rose-pine.d.ts +0 -3
- package/dist/themes/data/rose-pine.js +0 -158
- package/dist/themes/data/solarized-dark.d.ts +0 -3
- package/dist/themes/data/solarized-dark.js +0 -158
- package/dist/themes/data/solarized-light.d.ts +0 -3
- package/dist/themes/data/solarized-light.js +0 -158
- package/dist/themes/data/svg-export.d.ts +0 -3
- package/dist/themes/data/svg-export.js +0 -158
- package/dist/themes/data/textual-ansi.d.ts +0 -3
- package/dist/themes/data/textual-ansi.js +0 -158
- package/dist/themes/data/textual-dark.d.ts +0 -3
- package/dist/themes/data/textual-dark.js +0 -158
- package/dist/themes/data/textual-light.d.ts +0 -3
- package/dist/themes/data/textual-light.js +0 -158
- package/dist/themes/data/tokyo-night.d.ts +0 -3
- package/dist/themes/data/tokyo-night.js +0 -158
- package/dist/themes/data/types.d.ts +0 -15
- package/dist/themes/data/types.js +0 -1
- package/dist/themes/palette.d.ts +0 -21
- package/dist/themes/palette.js +0 -28
- package/dist/themes/ramp.d.ts +0 -83
- package/dist/themes/ramp.js +0 -122
- package/dist/themes/registry.d.ts +0 -43
- package/dist/themes/registry.js +0 -82
- package/dist/themes/terminalThemes.d.ts +0 -54
- package/dist/themes/terminalThemes.js +0 -68
- package/dist/themes/transpose.d.ts +0 -87
- package/dist/themes/transpose.js +0 -138
- package/dist/widgets/button.d.ts +0 -49
- package/dist/widgets/button.js +0 -206
- package/dist/widgets/checkbox.d.ts +0 -41
- package/dist/widgets/checkbox.js +0 -155
- package/dist/widgets/dropdown.d.ts +0 -67
- package/dist/widgets/dropdown.js +0 -453
- package/dist/widgets/event-router.d.ts +0 -78
- package/dist/widgets/event-router.js +0 -622
- package/dist/widgets/focus-manager.d.ts +0 -26
- package/dist/widgets/focus-manager.js +0 -193
- package/dist/widgets/index.d.ts +0 -22
- package/dist/widgets/index.js +0 -15
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
- package/dist/widgets/slider.d.ts +0 -57
- package/dist/widgets/slider.js +0 -270
- package/dist/widgets/static-item.d.ts +0 -40
- package/dist/widgets/static-item.js +0 -60
- package/dist/widgets/text-input.d.ts +0 -274
- package/dist/widgets/text-input.js +0 -1162
- package/dist/widgets/toggle.d.ts +0 -44
- package/dist/widgets/toggle.js +0 -179
- package/dist/widgets/types.d.ts +0 -108
- package/dist/widgets/types.js +0 -31
- package/dist/widgets/widget-base.d.ts +0 -42
- package/dist/widgets/widget-base.js +0 -173
package/dist/core/color.js
DELETED
|
@@ -1,1029 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal color representation, parsing, downgrading, and ANSI code generation.
|
|
3
|
-
*/
|
|
4
|
-
// --- ColorRgba ---
|
|
5
|
-
function hex2(byte) {
|
|
6
|
-
return byte.toString(16).padStart(2, "0");
|
|
7
|
-
}
|
|
8
|
-
function assertChannel(name, v) {
|
|
9
|
-
if (!Number.isInteger(v) || v < 0 || v > 255) {
|
|
10
|
-
throw new RangeError(`ColorRgba.${name} must be an integer in [0, 255]; got ${v}`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Immutable RGBA color value. Alpha defaults to 1 (fully opaque).
|
|
15
|
-
*
|
|
16
|
-
* [LAW:single-enforcer] The constructor is the sole validation site for
|
|
17
|
-
* channel/alpha invariants. Float-arithmetic callers (HSL roundtrips, blends,
|
|
18
|
-
* lerps) are responsible for `Math.round` + clamp before construction; the
|
|
19
|
-
* constructor throws rather than silently masking out-of-range input.
|
|
20
|
-
*/
|
|
21
|
-
export class ColorRgba {
|
|
22
|
-
red;
|
|
23
|
-
green;
|
|
24
|
-
blue;
|
|
25
|
-
alpha;
|
|
26
|
-
constructor(red, green, blue, alpha = 1) {
|
|
27
|
-
this.red = red;
|
|
28
|
-
this.green = green;
|
|
29
|
-
this.blue = blue;
|
|
30
|
-
this.alpha = alpha;
|
|
31
|
-
assertChannel("red", red);
|
|
32
|
-
assertChannel("green", green);
|
|
33
|
-
assertChannel("blue", blue);
|
|
34
|
-
if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
|
|
35
|
-
throw new RangeError(`ColorRgba.alpha must be a finite number in [0, 1]; got ${alpha}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** 6-char `#RRGGBB` when fully opaque, 8-char `#RRGGBBAA` otherwise. */
|
|
39
|
-
get hex() {
|
|
40
|
-
const rgb = "#" + hex2(this.red) + hex2(this.green) + hex2(this.blue);
|
|
41
|
-
return this.alpha === 1 ? rgb : rgb + hex2(Math.round(this.alpha * 255));
|
|
42
|
-
}
|
|
43
|
-
/** `rgb(r,g,b)` when fully opaque, `rgba(r,g,b,a)` otherwise. */
|
|
44
|
-
get rgb() {
|
|
45
|
-
return this.alpha === 1
|
|
46
|
-
? `rgb(${this.red},${this.green},${this.blue})`
|
|
47
|
-
: `rgba(${this.red},${this.green},${this.blue},${this.alpha})`;
|
|
48
|
-
}
|
|
49
|
-
/** [r/255, g/255, b/255, alpha] — alpha is already 0..1, no division. */
|
|
50
|
-
get normalized() {
|
|
51
|
-
return [this.red / 255, this.green / 255, this.blue / 255, this.alpha];
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Composite this color over an opaque background. Returns a fully opaque
|
|
55
|
-
* ColorRgba (alpha=1). Per-channel linear interpolation by alpha.
|
|
56
|
-
*
|
|
57
|
-
* [LAW:dataflow-not-control-flow] alpha=1 short-circuits to `this`, so
|
|
58
|
-
* callers can invoke this unconditionally; the data decides whether work
|
|
59
|
-
* happens.
|
|
60
|
-
*/
|
|
61
|
-
compositeOver(bg) {
|
|
62
|
-
if (this.alpha === 1)
|
|
63
|
-
return this;
|
|
64
|
-
const t = this.alpha;
|
|
65
|
-
return new ColorRgba(Math.round(bg.red + (this.red - bg.red) * t), Math.round(bg.green + (this.green - bg.green) * t), Math.round(bg.blue + (this.blue - bg.blue) * t), 1);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The surface a terminal draws a translucent colour over. A terminal cannot
|
|
70
|
-
* know what lies under its cells, so the SGR writer (`Style.toSgrCodes`), the
|
|
71
|
-
* strip's seam test and the contrast choosers (`themes/colorMath`) all
|
|
72
|
-
* composite over this one colour. [LAW:one-source-of-truth] One constant, so
|
|
73
|
-
* the colour text is chosen against is the colour the writer draws.
|
|
74
|
-
*/
|
|
75
|
-
export const SURFACE_BLACK = new ColorRgba(0, 0, 0);
|
|
76
|
-
/**
|
|
77
|
-
* WCAG 2.x relative luminance (0..1) of an opaque color. The single
|
|
78
|
-
* luminance function in the codebase — `contrastFor`, `contrastRatio`, and
|
|
79
|
-
* any caller that needs to reason about readability all funnel through it.
|
|
80
|
-
* [LAW:one-source-of-truth]
|
|
81
|
-
*/
|
|
82
|
-
export function relativeLuminance(c) {
|
|
83
|
-
const ch = (v) => {
|
|
84
|
-
const x = v / 255;
|
|
85
|
-
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
|
|
86
|
-
};
|
|
87
|
-
return 0.2126 * ch(c.red) + 0.7152 * ch(c.green) + 0.0722 * ch(c.blue);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* WCAG 2.x contrast ratio between two colors, in [1, 21]. Symmetric — the
|
|
91
|
-
* order of arguments does not matter. 4.5 is the AA threshold for normal
|
|
92
|
-
* text, 3.0 for large text.
|
|
93
|
-
*
|
|
94
|
-
* Assumes opaque inputs: alpha is ignored, since the displayed contrast of a
|
|
95
|
-
* translucent color depends on what it composites over. For a translucent
|
|
96
|
-
* foreground, flatten it first (or use `ensureContrast`, which does).
|
|
97
|
-
*/
|
|
98
|
-
export function contrastRatio(a, b) {
|
|
99
|
-
return luminanceRatio(relativeLuminance(a), relativeLuminance(b));
|
|
100
|
-
}
|
|
101
|
-
// [LAW:single-enforcer] The WCAG ratio over two relative luminances — the one
|
|
102
|
-
// formula `contrastRatio` and `ColorTable.matchReadable` (which caches its
|
|
103
|
-
// entries' luminances) both measure with.
|
|
104
|
-
function luminanceRatio(la, lb) {
|
|
105
|
-
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
106
|
-
}
|
|
107
|
-
// --- ColorTable ---
|
|
108
|
-
/**
|
|
109
|
-
* An indexed palette: entry `i` of `colors` is terminal index `firstIndex + i`.
|
|
110
|
-
* `firstIndex` lets a table hold only the part of a palette a downgrade may
|
|
111
|
-
* choose (the 256-colour cube and grey ramp start at 16) while every index it
|
|
112
|
-
* reports is the terminal's own.
|
|
113
|
-
*/
|
|
114
|
-
// [LAW:single-enforcer] The one size policy for ColorTable's memos: a key is
|
|
115
|
-
// derived from colours a long-running host computes without end (ramp stops,
|
|
116
|
-
// mixes), so an unbounded map grows with every render. Clearing at the cap is
|
|
117
|
-
// the policy `cellLen` already uses; a refill costs one table scan per key.
|
|
118
|
-
const TABLE_CACHE_MAX = 4096;
|
|
119
|
-
function remember(cache, key, index) {
|
|
120
|
-
if (cache.size >= TABLE_CACHE_MAX)
|
|
121
|
-
cache.clear();
|
|
122
|
-
cache.set(key, index);
|
|
123
|
-
return index;
|
|
124
|
-
}
|
|
125
|
-
// [LAW:one-source-of-truth] The one distance every table scan ranks by, so
|
|
126
|
-
// `matchWhere`'s "nearest by the distance `match` uses" cannot drift.
|
|
127
|
-
function rgbDistance(a, b) {
|
|
128
|
-
const dr = a.red - b.red;
|
|
129
|
-
const dg = a.green - b.green;
|
|
130
|
-
const db = a.blue - b.blue;
|
|
131
|
-
return dr * dr + dg * dg + db * db;
|
|
132
|
-
}
|
|
133
|
-
export class ColorTable {
|
|
134
|
-
colors;
|
|
135
|
-
firstIndex;
|
|
136
|
-
matchCache = new Map();
|
|
137
|
-
readableCache = new Map();
|
|
138
|
-
constructor(colors, firstIndex = 0) {
|
|
139
|
-
this.colors = colors;
|
|
140
|
-
this.firstIndex = firstIndex;
|
|
141
|
-
}
|
|
142
|
-
get(index) {
|
|
143
|
-
return this.colors[index - this.firstIndex];
|
|
144
|
-
}
|
|
145
|
-
luminanceCache;
|
|
146
|
-
/** Each entry's relative luminance, computed once per table. */
|
|
147
|
-
luminances() {
|
|
148
|
-
this.luminanceCache ??= this.colors.map(relativeLuminance);
|
|
149
|
-
return this.luminanceCache;
|
|
150
|
-
}
|
|
151
|
-
/** How many entries the table holds (terminal indices `firstIndex`…). */
|
|
152
|
-
get size() {
|
|
153
|
-
return this.colors.length;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Finds the nearest table index to the given color (Euclidean RGB distance, cached).
|
|
157
|
-
* Alpha is part of the cache key so two values that differ only in alpha
|
|
158
|
-
* don't collide, but is not used in the distance metric.
|
|
159
|
-
*/
|
|
160
|
-
match(value) {
|
|
161
|
-
const key = `${value.red},${value.green},${value.blue},${value.alpha}`;
|
|
162
|
-
const cached = this.matchCache.get(key);
|
|
163
|
-
if (cached !== undefined)
|
|
164
|
-
return cached;
|
|
165
|
-
let bestIndex = 0;
|
|
166
|
-
let bestDist = Infinity;
|
|
167
|
-
for (let i = 0; i < this.colors.length; i++) {
|
|
168
|
-
const c = this.colors[i];
|
|
169
|
-
const dist = rgbDistance(c, value);
|
|
170
|
-
if (dist < bestDist) {
|
|
171
|
-
bestDist = dist;
|
|
172
|
-
bestIndex = i;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return remember(this.matchCache, key, this.firstIndex + bestIndex);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* The nearest entry to `value` (the distance `match` uses) among those that
|
|
179
|
-
* clear `minRatio` against `on`; when none does, the entry with the most
|
|
180
|
-
* contrast against `on`. Text downgraded on its own background: `match`
|
|
181
|
-
* moves text and background independently, and two independent roundings
|
|
182
|
-
* can meet in the middle, so a pair that read at 4.5:1 in truecolor can
|
|
183
|
-
* draw at 2:1. [LAW:dataflow-not-control-flow] One scan scores every entry;
|
|
184
|
-
* the ratio decides which one wins.
|
|
185
|
-
*/
|
|
186
|
-
matchReadable(value, on, minRatio) {
|
|
187
|
-
const key = `${value.red},${value.green},${value.blue}|${on.red},${on.green},${on.blue}|${minRatio}`;
|
|
188
|
-
const cached = this.readableCache.get(key);
|
|
189
|
-
if (cached !== undefined)
|
|
190
|
-
return cached;
|
|
191
|
-
const lOn = relativeLuminance(on);
|
|
192
|
-
let best = 0;
|
|
193
|
-
let bestPasses = false;
|
|
194
|
-
let bestScore = -Infinity;
|
|
195
|
-
for (let i = 0; i < this.colors.length; i++) {
|
|
196
|
-
const c = this.colors[i];
|
|
197
|
-
const lc = this.luminances()[i];
|
|
198
|
-
const ratio = luminanceRatio(lc, lOn);
|
|
199
|
-
const passes = ratio >= minRatio;
|
|
200
|
-
// A passing entry scores by closeness; a failing one only by contrast,
|
|
201
|
-
// and loses to every passing one.
|
|
202
|
-
const score = passes ? -rgbDistance(c, value) : ratio;
|
|
203
|
-
if ((passes && !bestPasses) ||
|
|
204
|
-
(passes === bestPasses && score > bestScore)) {
|
|
205
|
-
best = i;
|
|
206
|
-
bestPasses = passes;
|
|
207
|
-
bestScore = score;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return remember(this.readableCache, key, this.firstIndex + best);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* The nearest entry to `value` (the distance `match` uses) among those
|
|
214
|
-
* `accept` takes, given each entry's colour and terminal index; `undefined`
|
|
215
|
-
* when it takes none. Entries are offered nearest-first (ties to the lower
|
|
216
|
-
* index) and the first taken wins, so an expensive predicate runs only as
|
|
217
|
-
* far out as the answer. Uncached: the predicate is the caller's, and a
|
|
218
|
-
* closure has no key.
|
|
219
|
-
*/
|
|
220
|
-
matchWhere(value, accept) {
|
|
221
|
-
const dist = this.colors.map((c) => rgbDistance(c, value));
|
|
222
|
-
const nearestFirst = [...dist.keys()].sort((a, b) => dist[a] - dist[b]);
|
|
223
|
-
const found = nearestFirst.find((i) => accept(this.colors[i], this.firstIndex + i));
|
|
224
|
-
return found === undefined ? undefined : this.firstIndex + found;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
// --- Enums ---
|
|
228
|
-
export var ColorDepth;
|
|
229
|
-
(function (ColorDepth) {
|
|
230
|
-
ColorDepth[ColorDepth["DEFAULT"] = 0] = "DEFAULT";
|
|
231
|
-
ColorDepth[ColorDepth["STANDARD"] = 1] = "STANDARD";
|
|
232
|
-
ColorDepth[ColorDepth["EIGHT_BIT"] = 2] = "EIGHT_BIT";
|
|
233
|
-
ColorDepth[ColorDepth["TRUECOLOR"] = 3] = "TRUECOLOR";
|
|
234
|
-
ColorDepth[ColorDepth["WINDOWS"] = 4] = "WINDOWS";
|
|
235
|
-
})(ColorDepth || (ColorDepth = {}));
|
|
236
|
-
// [LAW:one-source-of-truth] One table covers every recognized string form
|
|
237
|
-
// that resolves to a ColorDepth: user-facing CLI/config specs, FORCE_COLOR
|
|
238
|
-
// values (the chalk/supports-color convention), TERM_PROGRAM identifiers,
|
|
239
|
-
// and exact TERM names whose color capability is known. Keys are disjoint
|
|
240
|
-
// across these sources, so one table is honest. `null` means "no color".
|
|
241
|
-
// "auto" is intentionally absent — it routes to env detection.
|
|
242
|
-
const STRING_TO_DEPTH = {
|
|
243
|
-
// CLI/config specs
|
|
244
|
-
truecolor: ColorDepth.TRUECOLOR,
|
|
245
|
-
"256": ColorDepth.EIGHT_BIT,
|
|
246
|
-
ansi: ColorDepth.STANDARD,
|
|
247
|
-
none: null,
|
|
248
|
-
// FORCE_COLOR values
|
|
249
|
-
"0": null,
|
|
250
|
-
false: null,
|
|
251
|
-
"1": ColorDepth.STANDARD,
|
|
252
|
-
true: ColorDepth.STANDARD,
|
|
253
|
-
"2": ColorDepth.EIGHT_BIT,
|
|
254
|
-
"3": ColorDepth.TRUECOLOR,
|
|
255
|
-
// TERM_PROGRAM identifiers
|
|
256
|
-
"iTerm.app": ColorDepth.TRUECOLOR,
|
|
257
|
-
Apple_Terminal: ColorDepth.EIGHT_BIT,
|
|
258
|
-
vscode: ColorDepth.TRUECOLOR,
|
|
259
|
-
Tabby: ColorDepth.TRUECOLOR,
|
|
260
|
-
// Known truecolor TERM names
|
|
261
|
-
"xterm-kitty": ColorDepth.TRUECOLOR,
|
|
262
|
-
"xterm-ghostty": ColorDepth.TRUECOLOR,
|
|
263
|
-
wezterm: ColorDepth.TRUECOLOR,
|
|
264
|
-
alacritty: ColorDepth.TRUECOLOR,
|
|
265
|
-
foot: ColorDepth.TRUECOLOR,
|
|
266
|
-
contour: ColorDepth.TRUECOLOR,
|
|
267
|
-
};
|
|
268
|
-
/**
|
|
269
|
-
* Detect the terminal's color capability from env + TTY state.
|
|
270
|
-
*
|
|
271
|
-
* Probes (in priority order): NO_COLOR (per https://no-color.org —
|
|
272
|
-
* any non-empty value disables color), FORCE_COLOR (numeric/boolean override),
|
|
273
|
-
* TTY presence, TERM=dumb/unknown, COLORTERM, known terminal/TERM_PROGRAM
|
|
274
|
-
* names, and TERM regex patterns. Returns `null` for "no color".
|
|
275
|
-
*
|
|
276
|
-
* [LAW:dataflow-not-control-flow] Same probes run every call; variability is
|
|
277
|
-
* in the env values, not in which checks execute.
|
|
278
|
-
*/
|
|
279
|
-
export function detectColorSystem(options = {}) {
|
|
280
|
-
// NO_COLOR and FORCE_COLOR are preferences about colour, not facts about the
|
|
281
|
-
// terminal, so they sit outside the probe. NO_COLOR (any non-empty value)
|
|
282
|
-
// beats FORCE_COLOR; FORCE_COLOR beats TTY/TERM detection.
|
|
283
|
-
const noColor = envOf(options)["NO_COLOR"];
|
|
284
|
-
if (noColor !== undefined && noColor !== "")
|
|
285
|
-
return null;
|
|
286
|
-
const forced = forcedColorSystem(envOf(options));
|
|
287
|
-
return forced !== undefined ? forced : terminalColorSystem(options);
|
|
288
|
-
}
|
|
289
|
-
// FORCE_COLOR's depth: `undefined` when unset, `null` for its off values.
|
|
290
|
-
function forcedColorSystem(env) {
|
|
291
|
-
const force = env["FORCE_COLOR"];
|
|
292
|
-
if (force === undefined || force === "")
|
|
293
|
-
return undefined;
|
|
294
|
-
const mapped = STRING_TO_DEPTH[force];
|
|
295
|
-
return mapped !== undefined ? mapped : ColorDepth.STANDARD;
|
|
296
|
-
}
|
|
297
|
-
function envOf(options) {
|
|
298
|
-
return options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
299
|
-
}
|
|
300
|
-
// What the destination can draw, preferences aside: TTY presence,
|
|
301
|
-
// TERM=dumb/unknown, COLORTERM, known terminal names, TERM patterns. `null`
|
|
302
|
-
// here means the destination takes no escapes at all.
|
|
303
|
-
function terminalColorSystem(options) {
|
|
304
|
-
const env = envOf(options);
|
|
305
|
-
const isTTY = options.isTTY ??
|
|
306
|
-
(typeof process !== "undefined" ? (process.stdout?.isTTY ?? false) : false);
|
|
307
|
-
if (!isTTY)
|
|
308
|
-
return null;
|
|
309
|
-
const term = env["TERM"] ?? "";
|
|
310
|
-
if (term === "dumb" || term === "unknown")
|
|
311
|
-
return null;
|
|
312
|
-
const colorterm = env["COLORTERM"];
|
|
313
|
-
if (colorterm === "truecolor" || colorterm === "24bit") {
|
|
314
|
-
return ColorDepth.TRUECOLOR;
|
|
315
|
-
}
|
|
316
|
-
const termDepth = STRING_TO_DEPTH[term];
|
|
317
|
-
if (termDepth !== undefined)
|
|
318
|
-
return termDepth;
|
|
319
|
-
const termProgram = env["TERM_PROGRAM"];
|
|
320
|
-
if (termProgram !== undefined) {
|
|
321
|
-
const mapped = STRING_TO_DEPTH[termProgram];
|
|
322
|
-
if (mapped !== undefined)
|
|
323
|
-
return mapped;
|
|
324
|
-
}
|
|
325
|
-
if (/-256(color)?$/i.test(term))
|
|
326
|
-
return ColorDepth.EIGHT_BIT;
|
|
327
|
-
if (/-truecolor$/i.test(term))
|
|
328
|
-
return ColorDepth.TRUECOLOR;
|
|
329
|
-
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(term)) {
|
|
330
|
-
return ColorDepth.STANDARD;
|
|
331
|
-
}
|
|
332
|
-
if (colorterm !== undefined && colorterm !== "")
|
|
333
|
-
return ColorDepth.STANDARD;
|
|
334
|
-
// Default: a terminal exists (isTTY=true) but we couldn't classify it.
|
|
335
|
-
// Assume safe baseline rather than disabling color.
|
|
336
|
-
return ColorDepth.STANDARD;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Resolve a colour spec into a `Destination`.
|
|
340
|
-
*
|
|
341
|
-
* [LAW:single-enforcer] The one place a spec becomes both facts. A hyperlink is
|
|
342
|
-
* not a colour: an explicit depth — `"none"` and `null` included — states a
|
|
343
|
-
* colour choice and keeps links, and under `"auto"` links follow what the
|
|
344
|
-
* terminal can take (no TTY, TERM=dumb: none) while NO_COLOR and FORCE_COLOR=0,
|
|
345
|
-
* colour preferences, leave them alone. A FORCE_COLOR that forces colour on
|
|
346
|
-
* declares the destination takes escapes, so it keeps links on a pipe too.
|
|
347
|
-
*/
|
|
348
|
-
export function resolveDestination(spec, options) {
|
|
349
|
-
if (spec !== "auto") {
|
|
350
|
-
return {
|
|
351
|
-
colorSystem: typeof spec === "string" ? resolveColorSystem(spec) : spec,
|
|
352
|
-
hyperlinks: true,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
return {
|
|
356
|
-
colorSystem: detectColorSystem(options),
|
|
357
|
-
hyperlinks: (forcedColorSystem(envOf(options ?? {})) ?? null) !== null ||
|
|
358
|
-
terminalColorSystem(options ?? {}) !== null,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Resolve a string spec into a `ColorDepth` (or `null` for no color).
|
|
363
|
-
*
|
|
364
|
-
* `"auto"` triggers env-based detection; all other recognized specs are
|
|
365
|
-
* direct table lookups against `STRING_TO_DEPTH`. Throws on unknown specs
|
|
366
|
-
* — silent fallback would mask user typos.
|
|
367
|
-
*
|
|
368
|
-
* [LAW:single-enforcer] All string→ColorDepth resolution flows through here.
|
|
369
|
-
*/
|
|
370
|
-
export function resolveColorSystem(spec, options) {
|
|
371
|
-
if (spec === "auto")
|
|
372
|
-
return detectColorSystem(options);
|
|
373
|
-
if (Object.hasOwn(STRING_TO_DEPTH, spec))
|
|
374
|
-
return STRING_TO_DEPTH[spec];
|
|
375
|
-
throw new ColorParseError(`Unknown color depth spec: ${JSON.stringify(spec)} (expected "auto", "truecolor", "256", "ansi", or "none")`);
|
|
376
|
-
}
|
|
377
|
-
// --- ColorSpec ---
|
|
378
|
-
export class ColorParseError extends Error {
|
|
379
|
-
constructor(message) {
|
|
380
|
-
super(message);
|
|
381
|
-
this.name = "ColorParseError";
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// [LAW:one-source-of-truth] Parse cache is the single source for parsed ColorSpec instances
|
|
385
|
-
const parseCache = new Map();
|
|
386
|
-
export class ColorSpec {
|
|
387
|
-
name;
|
|
388
|
-
type;
|
|
389
|
-
number;
|
|
390
|
-
value;
|
|
391
|
-
downgradeCache = new Map();
|
|
392
|
-
constructor(name, type, number, value) {
|
|
393
|
-
this.name = name;
|
|
394
|
-
this.type = type;
|
|
395
|
-
this.number = number;
|
|
396
|
-
this.value = value;
|
|
397
|
-
}
|
|
398
|
-
get isDefault() {
|
|
399
|
-
return this.type === ColorDepth.DEFAULT;
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* The colour every terminal draws this spec as, where that is fixed — before
|
|
403
|
-
* any alpha is flattened, so a translucent value is drawn composited over
|
|
404
|
-
* its ground (`flattenAlpha`) and should be measured that way: a
|
|
405
|
-
* truecolor value, or a 256-colour cube or grey-ramp entry (xterm fixes
|
|
406
|
-
* indices 16–255, and `fromAnsi` types only those as EIGHT_BIT). ANSI 0–15
|
|
407
|
-
* and the default colour are the terminal theme's own, so they have none.
|
|
408
|
-
*/
|
|
409
|
-
get fixedValue() {
|
|
410
|
-
switch (this.type) {
|
|
411
|
-
case ColorDepth.TRUECOLOR:
|
|
412
|
-
return this.value;
|
|
413
|
-
case ColorDepth.EIGHT_BIT:
|
|
414
|
-
// The constructor admits an EIGHT_BIT spec at 0–15; those are the
|
|
415
|
-
// theme's own slots whatever depth names them.
|
|
416
|
-
return this.number < 16 ? undefined : EIGHT_BIT_TABLE.get(this.number);
|
|
417
|
-
case ColorDepth.DEFAULT:
|
|
418
|
-
case ColorDepth.STANDARD:
|
|
419
|
-
case ColorDepth.WINDOWS:
|
|
420
|
-
return undefined;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
get isSystemDefined() {
|
|
424
|
-
return (this.type === ColorDepth.STANDARD || this.type === ColorDepth.WINDOWS);
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Generates SGR parameter strings for this color.
|
|
428
|
-
*/
|
|
429
|
-
getAnsiCodes(foreground = true) {
|
|
430
|
-
switch (this.type) {
|
|
431
|
-
case ColorDepth.DEFAULT:
|
|
432
|
-
return [foreground ? "39" : "49"];
|
|
433
|
-
case ColorDepth.STANDARD: {
|
|
434
|
-
const n = this.number;
|
|
435
|
-
if (foreground) {
|
|
436
|
-
return n < 8 ? [`${30 + n}`] : [`${90 + n - 8}`];
|
|
437
|
-
}
|
|
438
|
-
return n < 8 ? [`${40 + n}`] : [`${100 + n - 8}`];
|
|
439
|
-
}
|
|
440
|
-
case ColorDepth.EIGHT_BIT:
|
|
441
|
-
return [foreground ? "38" : "48", "5", `${this.number}`];
|
|
442
|
-
case ColorDepth.TRUECOLOR: {
|
|
443
|
-
const t = this.value;
|
|
444
|
-
return [
|
|
445
|
-
foreground ? "38" : "48",
|
|
446
|
-
"2",
|
|
447
|
-
`${t.red}`,
|
|
448
|
-
`${t.green}`,
|
|
449
|
-
`${t.blue}`,
|
|
450
|
-
];
|
|
451
|
-
}
|
|
452
|
-
case ColorDepth.WINDOWS: {
|
|
453
|
-
const n = this.number;
|
|
454
|
-
if (foreground) {
|
|
455
|
-
return n < 8 ? [`${30 + n}`] : [`${90 + n - 8}`];
|
|
456
|
-
}
|
|
457
|
-
return n < 8 ? [`${40 + n}`] : [`${100 + n - 8}`];
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Composite this color's alpha (if any) against an opaque background,
|
|
463
|
-
* returning a fully-opaque ColorSpec. Non-truecolor specs (palette
|
|
464
|
-
* indices, default) have no alpha and pass through unchanged.
|
|
465
|
-
*
|
|
466
|
-
* [LAW:single-enforcer] Sole alpha-flattening site for the render
|
|
467
|
-
* pipeline. Run *before* downgrade — otherwise palette matching on the
|
|
468
|
-
* still-translucent RGB silently drops alpha (the "flatten then lose
|
|
469
|
-
* alpha" path the unification ticket forbids).
|
|
470
|
-
*
|
|
471
|
-
* [LAW:dataflow-not-control-flow] `ColorRgba.compositeOver` is a no-op
|
|
472
|
-
* when alpha=1, so callers invoke this unconditionally; the data
|
|
473
|
-
* (alpha value) decides whether work happens.
|
|
474
|
-
*/
|
|
475
|
-
flattenAlpha(bg) {
|
|
476
|
-
if (this.type !== ColorDepth.TRUECOLOR || !this.value)
|
|
477
|
-
return this;
|
|
478
|
-
const flat = this.value.compositeOver(bg);
|
|
479
|
-
return flat === this.value ? this : ColorSpec.fromRgba(flat);
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Downgrade to a lower-fidelity color depth. Cached.
|
|
483
|
-
*/
|
|
484
|
-
downgrade(targetSystem) {
|
|
485
|
-
if (this.type === ColorDepth.DEFAULT)
|
|
486
|
-
return this;
|
|
487
|
-
if (this.type <= targetSystem)
|
|
488
|
-
return this;
|
|
489
|
-
const cached = this.downgradeCache.get(targetSystem);
|
|
490
|
-
if (cached)
|
|
491
|
-
return cached;
|
|
492
|
-
const result = this.performDowngrade(targetSystem);
|
|
493
|
-
this.downgradeCache.set(targetSystem, result);
|
|
494
|
-
return result;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Resolves to an actual RGB(A) value for any color type.
|
|
498
|
-
*/
|
|
499
|
-
getTruecolor(theme, foreground = true) {
|
|
500
|
-
switch (this.type) {
|
|
501
|
-
case ColorDepth.TRUECOLOR:
|
|
502
|
-
return this.value;
|
|
503
|
-
case ColorDepth.EIGHT_BIT:
|
|
504
|
-
return EIGHT_BIT_TABLE.get(this.number);
|
|
505
|
-
case ColorDepth.STANDARD: {
|
|
506
|
-
const t = theme ?? INTERNAL_DEFAULT_THEME;
|
|
507
|
-
return t.ansiColors.get(this.number);
|
|
508
|
-
}
|
|
509
|
-
case ColorDepth.DEFAULT: {
|
|
510
|
-
const t = theme ?? INTERNAL_DEFAULT_THEME;
|
|
511
|
-
return foreground ? t.foregroundColor : t.backgroundColor;
|
|
512
|
-
}
|
|
513
|
-
case ColorDepth.WINDOWS: {
|
|
514
|
-
const t = theme ?? INTERNAL_DEFAULT_THEME;
|
|
515
|
-
return t.ansiColors.get(this.number);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// --- Static factories ---
|
|
520
|
-
static default() {
|
|
521
|
-
return new ColorSpec("default", ColorDepth.DEFAULT);
|
|
522
|
-
}
|
|
523
|
-
static fromAnsi(n) {
|
|
524
|
-
const type = n < 16 ? ColorDepth.STANDARD : ColorDepth.EIGHT_BIT;
|
|
525
|
-
return new ColorSpec(`color(${n})`, type, n);
|
|
526
|
-
}
|
|
527
|
-
static fromRgba(value) {
|
|
528
|
-
return new ColorSpec(value.hex, ColorDepth.TRUECOLOR, undefined, value);
|
|
529
|
-
}
|
|
530
|
-
static fromRgb(r, g, b) {
|
|
531
|
-
return ColorSpec.fromRgba(new ColorRgba(r, g, b));
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Parse a color string. Cached — identical strings return the same instance.
|
|
535
|
-
*/
|
|
536
|
-
static parse(colorString) {
|
|
537
|
-
const key = colorString.toLowerCase().trim();
|
|
538
|
-
const cached = parseCache.get(key);
|
|
539
|
-
if (cached)
|
|
540
|
-
return cached;
|
|
541
|
-
const result = parseSingle(key);
|
|
542
|
-
parseCache.set(key, result);
|
|
543
|
-
return result;
|
|
544
|
-
}
|
|
545
|
-
// --- private ---
|
|
546
|
-
performDowngrade(targetSystem) {
|
|
547
|
-
// Get the true RGB of this color
|
|
548
|
-
const triplet = this.getTruecolor();
|
|
549
|
-
switch (targetSystem) {
|
|
550
|
-
case ColorDepth.EIGHT_BIT: {
|
|
551
|
-
const index = EIGHT_BIT_DOWNGRADE_TABLE.match(triplet);
|
|
552
|
-
return ColorSpec.fromAnsi(index);
|
|
553
|
-
}
|
|
554
|
-
case ColorDepth.STANDARD: {
|
|
555
|
-
const index = STANDARD_TABLE.match(triplet);
|
|
556
|
-
return new ColorSpec(`color(${index})`, ColorDepth.STANDARD, index);
|
|
557
|
-
}
|
|
558
|
-
// WINDOWS is a detection result (max enum value), not a downgrade target.
|
|
559
|
-
// The `this.type <= targetSystem` guard in downgrade() always short-circuits
|
|
560
|
-
// before reaching this method with WINDOWS, so this case is unreachable.
|
|
561
|
-
case ColorDepth.WINDOWS:
|
|
562
|
-
case ColorDepth.TRUECOLOR:
|
|
563
|
-
return this;
|
|
564
|
-
case ColorDepth.DEFAULT:
|
|
565
|
-
return ColorSpec.default();
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
// --- Parsing internals ---
|
|
570
|
-
const HEX_RE = /^#([0-9a-f]{6})$/;
|
|
571
|
-
const HEX_RGBA_RE = /^#([0-9a-f]{8})$/;
|
|
572
|
-
const RGB_RE = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
|
|
573
|
-
const COLOR_NUMBER_RE = /^color\((\d{1,3})\)$/;
|
|
574
|
-
function parseSingle(key) {
|
|
575
|
-
if (key === "default" || key === "") {
|
|
576
|
-
return ColorSpec.default();
|
|
577
|
-
}
|
|
578
|
-
// Named color
|
|
579
|
-
const namedIndex = ANSI_COLOR_NAMES[key];
|
|
580
|
-
if (namedIndex !== undefined) {
|
|
581
|
-
const type = namedIndex < 16 ? ColorDepth.STANDARD : ColorDepth.EIGHT_BIT;
|
|
582
|
-
return new ColorSpec(key, type, namedIndex);
|
|
583
|
-
}
|
|
584
|
-
// Hex (8-char with alpha takes precedence over 6-char to avoid the 6-char
|
|
585
|
-
// regex matching a prefix of an 8-char string).
|
|
586
|
-
const hexRgbaMatch = HEX_RGBA_RE.exec(key);
|
|
587
|
-
if (hexRgbaMatch) {
|
|
588
|
-
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, parseRgbaHex(hexRgbaMatch[1]));
|
|
589
|
-
}
|
|
590
|
-
const hexMatch = HEX_RE.exec(key);
|
|
591
|
-
if (hexMatch) {
|
|
592
|
-
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, parseRgbHex(hexMatch[1]));
|
|
593
|
-
}
|
|
594
|
-
// rgb()
|
|
595
|
-
const rgbMatch = RGB_RE.exec(key);
|
|
596
|
-
if (rgbMatch) {
|
|
597
|
-
const r = parseInt(rgbMatch[1], 10);
|
|
598
|
-
const g = parseInt(rgbMatch[2], 10);
|
|
599
|
-
const b = parseInt(rgbMatch[3], 10);
|
|
600
|
-
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, new ColorRgba(r, g, b));
|
|
601
|
-
}
|
|
602
|
-
// color(N)
|
|
603
|
-
const numMatch = COLOR_NUMBER_RE.exec(key);
|
|
604
|
-
if (numMatch) {
|
|
605
|
-
const n = parseInt(numMatch[1], 10);
|
|
606
|
-
if (n > 255) {
|
|
607
|
-
throw new ColorParseError(`ColorSpec number ${n} is out of range (0-255)`);
|
|
608
|
-
}
|
|
609
|
-
const type = n < 16 ? ColorDepth.STANDARD : ColorDepth.EIGHT_BIT;
|
|
610
|
-
return new ColorSpec(key, type, n);
|
|
611
|
-
}
|
|
612
|
-
throw new ColorParseError(`Failed to parse color: "${key}"`);
|
|
613
|
-
}
|
|
614
|
-
// --- Utility functions ---
|
|
615
|
-
export function parseRgbHex(hex) {
|
|
616
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
617
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
618
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
619
|
-
return new ColorRgba(r, g, b);
|
|
620
|
-
}
|
|
621
|
-
export function parseRgbaHex(hex) {
|
|
622
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
623
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
624
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
625
|
-
const a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
626
|
-
return new ColorRgba(r, g, b, a);
|
|
627
|
-
}
|
|
628
|
-
export function blendRgb(color1, color2, crossFade = 0.5) {
|
|
629
|
-
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);
|
|
630
|
-
}
|
|
631
|
-
// --- TerminalTheme ---
|
|
632
|
-
/**
|
|
633
|
-
* A terminal theme — surface/foreground baseline, the ANSI 16/256 LUT, and a
|
|
634
|
-
* semantic palette.
|
|
635
|
-
*
|
|
636
|
-
* **`ansiColors` is the canonical ANSI table, not a per-theme override.** All
|
|
637
|
-
* preset themes in this codebase use `STANDARD_TABLE` for `ansiColors`. Theme
|
|
638
|
-
* identity lives entirely in `palette` (primary, accent, text-*, surface, ...);
|
|
639
|
-
* `ColorSpec.parse("red")` is intentionally consistent across themes — same
|
|
640
|
-
* design as Textual itself, which models theming at the truecolor/semantic
|
|
641
|
-
* layer rather than overloading the ANSI 16. Custom callers building bespoke
|
|
642
|
-
* themes that *do* want to retint ANSI 16 are free to pass their own
|
|
643
|
-
* `ColorTable`; that path is supported but not how the built-in presets work.
|
|
644
|
-
*/
|
|
645
|
-
export class TerminalTheme {
|
|
646
|
-
backgroundColor;
|
|
647
|
-
foregroundColor;
|
|
648
|
-
ansiColors;
|
|
649
|
-
palette;
|
|
650
|
-
constructor(backgroundColor, foregroundColor, ansiColors, palette) {
|
|
651
|
-
this.backgroundColor = backgroundColor;
|
|
652
|
-
this.foregroundColor = foregroundColor;
|
|
653
|
-
this.ansiColors = ansiColors;
|
|
654
|
-
this.palette = palette;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
// --- ColorTable data ---
|
|
658
|
-
function buildStandard16() {
|
|
659
|
-
return [
|
|
660
|
-
new ColorRgba(0, 0, 0), // 0 black
|
|
661
|
-
new ColorRgba(128, 0, 0), // 1 red
|
|
662
|
-
new ColorRgba(0, 128, 0), // 2 green
|
|
663
|
-
new ColorRgba(128, 128, 0), // 3 yellow
|
|
664
|
-
new ColorRgba(0, 0, 128), // 4 blue
|
|
665
|
-
new ColorRgba(128, 0, 128), // 5 magenta
|
|
666
|
-
new ColorRgba(0, 128, 128), // 6 cyan
|
|
667
|
-
new ColorRgba(192, 192, 192), // 7 white
|
|
668
|
-
new ColorRgba(128, 128, 128), // 8 bright_black
|
|
669
|
-
new ColorRgba(255, 0, 0), // 9 bright_red
|
|
670
|
-
new ColorRgba(0, 255, 0), // 10 bright_green
|
|
671
|
-
new ColorRgba(255, 255, 0), // 11 bright_yellow
|
|
672
|
-
new ColorRgba(0, 0, 255), // 12 bright_blue
|
|
673
|
-
new ColorRgba(255, 0, 255), // 13 bright_magenta
|
|
674
|
-
new ColorRgba(0, 255, 255), // 14 bright_cyan
|
|
675
|
-
new ColorRgba(255, 255, 255), // 15 bright_white
|
|
676
|
-
];
|
|
677
|
-
}
|
|
678
|
-
function build256Table() {
|
|
679
|
-
const colors = buildStandard16();
|
|
680
|
-
// 6x6x6 color cube (indices 16-231)
|
|
681
|
-
const levels = [0, 95, 135, 175, 215, 255];
|
|
682
|
-
for (let r = 0; r < 6; r++) {
|
|
683
|
-
for (let g = 0; g < 6; g++) {
|
|
684
|
-
for (let b = 0; b < 6; b++) {
|
|
685
|
-
colors.push(new ColorRgba(levels[r], levels[g], levels[b]));
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
// Grayscale ramp (indices 232-255)
|
|
690
|
-
for (let i = 0; i < 24; i++) {
|
|
691
|
-
const grey = 8 + 10 * i;
|
|
692
|
-
colors.push(new ColorRgba(grey, grey, grey));
|
|
693
|
-
}
|
|
694
|
-
return colors;
|
|
695
|
-
}
|
|
696
|
-
function buildWindowsTable() {
|
|
697
|
-
return [
|
|
698
|
-
new ColorRgba(0, 0, 0), // 0
|
|
699
|
-
new ColorRgba(0, 0, 128), // 1
|
|
700
|
-
new ColorRgba(0, 128, 0), // 2
|
|
701
|
-
new ColorRgba(0, 128, 128), // 3
|
|
702
|
-
new ColorRgba(128, 0, 0), // 4
|
|
703
|
-
new ColorRgba(128, 0, 128), // 5
|
|
704
|
-
new ColorRgba(128, 128, 0), // 6
|
|
705
|
-
new ColorRgba(192, 192, 192), // 7
|
|
706
|
-
new ColorRgba(128, 128, 128), // 8
|
|
707
|
-
new ColorRgba(0, 0, 255), // 9
|
|
708
|
-
new ColorRgba(0, 255, 0), // 10
|
|
709
|
-
new ColorRgba(0, 255, 255), // 11
|
|
710
|
-
new ColorRgba(255, 0, 0), // 12
|
|
711
|
-
new ColorRgba(255, 0, 255), // 13
|
|
712
|
-
new ColorRgba(255, 255, 0), // 14
|
|
713
|
-
new ColorRgba(255, 255, 255), // 15
|
|
714
|
-
];
|
|
715
|
-
}
|
|
716
|
-
export const STANDARD_TABLE = new ColorTable(buildStandard16());
|
|
717
|
-
export const EIGHT_BIT_TABLE = new ColorTable(build256Table());
|
|
718
|
-
/**
|
|
719
|
-
* What a downgrade to 256 colours may choose: the cube and the grey ramp,
|
|
720
|
-
* indices 16–255. Indices 0–15 are the terminal's own ANSI colours, which
|
|
721
|
-
* every theme redefines, so their RGB is unknown and a match against them is
|
|
722
|
-
* a guess; Python Rich never picks them either.
|
|
723
|
-
*/
|
|
724
|
-
export const EIGHT_BIT_DOWNGRADE_TABLE = new ColorTable(build256Table().slice(16), 16);
|
|
725
|
-
export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
|
|
726
|
-
// --- Internal fallback theme ---
|
|
727
|
-
//
|
|
728
|
-
// [LAW:one-way-deps] Preset themes (DEFAULT_TERMINAL_THEME, MONOKAI, NORD, ...)
|
|
729
|
-
// live in `src/themes/terminalThemes.ts` because they need `buildPalette` from
|
|
730
|
-
// `themes/`. Keeping them out of `core/color.ts` makes the dependency strictly
|
|
731
|
-
// `themes/* -> core/color`, with no runtime back-edge.
|
|
732
|
-
//
|
|
733
|
-
// `getTruecolor()` retains a fallback for callers that omit `theme`. The
|
|
734
|
-
// fallback uses STANDARD_TABLE + black/white + an empty Palette — sufficient
|
|
735
|
-
// for STANDARD/DEFAULT/WINDOWS lookups, which never read `palette`.
|
|
736
|
-
//
|
|
737
|
-
// Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
|
|
738
|
-
// only `import type`s from `core/color.ts`, so there is no runtime cycle.
|
|
739
|
-
import { Palette } from "../themes/palette.js";
|
|
740
|
-
const INTERNAL_DEFAULT_THEME = new TerminalTheme(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE, new Palette("default", true, new Map()));
|
|
741
|
-
// --- ANSI ColorSpec Names ---
|
|
742
|
-
// [LAW:one-source-of-truth] Single canonical mapping from name → palette index
|
|
743
|
-
/**
|
|
744
|
-
* The table with every `greyNN` name repeated under the `grayNN` spelling.
|
|
745
|
-
*
|
|
746
|
-
* Rich accepts both spellings and the xterm palette only defines one, so the
|
|
747
|
-
* aliases are part of what this table *is* rather than a patch applied to it.
|
|
748
|
-
*
|
|
749
|
-
* [LAW:no-ambient-temporal-coupling] Which is why they arrive as an initializer
|
|
750
|
-
* and not as the loop that used to mutate the finished map: that loop made "has
|
|
751
|
-
* the aliases yet" a fact about module evaluation order, so the table had two
|
|
752
|
-
* shapes and nothing in its type said which one you were holding. It also made
|
|
753
|
-
* this module do work at import time, which is the claim `package.json`'s
|
|
754
|
-
* `sideEffects: false` makes on its behalf — see
|
|
755
|
-
* `test/seam/import-time-effects.ts`.
|
|
756
|
-
*/
|
|
757
|
-
function withGrayAliases(names) {
|
|
758
|
-
const aliases = Object.entries(names)
|
|
759
|
-
.filter(([name]) => name.includes("grey"))
|
|
760
|
-
.map(([name, index]) => [name.replace("grey", "gray"), index]);
|
|
761
|
-
return { ...names, ...Object.fromEntries(aliases) };
|
|
762
|
-
}
|
|
763
|
-
export const ANSI_COLOR_NAMES = withGrayAliases({
|
|
764
|
-
// Standard 16
|
|
765
|
-
black: 0,
|
|
766
|
-
red: 1,
|
|
767
|
-
green: 2,
|
|
768
|
-
yellow: 3,
|
|
769
|
-
blue: 4,
|
|
770
|
-
magenta: 5,
|
|
771
|
-
cyan: 6,
|
|
772
|
-
white: 7,
|
|
773
|
-
bright_black: 8,
|
|
774
|
-
bright_red: 9,
|
|
775
|
-
bright_green: 10,
|
|
776
|
-
bright_yellow: 11,
|
|
777
|
-
bright_blue: 12,
|
|
778
|
-
bright_magenta: 13,
|
|
779
|
-
bright_cyan: 14,
|
|
780
|
-
bright_white: 15,
|
|
781
|
-
// 6x6x6 color cube (16-231)
|
|
782
|
-
grey0: 16,
|
|
783
|
-
navy_blue: 17,
|
|
784
|
-
dark_blue: 18,
|
|
785
|
-
blue3: 19,
|
|
786
|
-
blue2: 20,
|
|
787
|
-
blue1: 21,
|
|
788
|
-
dark_green: 22,
|
|
789
|
-
deep_sky_blue4: 23,
|
|
790
|
-
deep_sky_blue5: 24,
|
|
791
|
-
deep_sky_blue6: 25,
|
|
792
|
-
dodger_blue3: 26,
|
|
793
|
-
dodger_blue2: 27,
|
|
794
|
-
green4: 28,
|
|
795
|
-
spring_green4: 29,
|
|
796
|
-
turquoise4: 30,
|
|
797
|
-
deep_sky_blue3: 31,
|
|
798
|
-
deep_sky_blue7: 32,
|
|
799
|
-
dodger_blue1: 33,
|
|
800
|
-
green3: 34,
|
|
801
|
-
spring_green3: 35,
|
|
802
|
-
dark_cyan: 36,
|
|
803
|
-
light_sea_green: 37,
|
|
804
|
-
deep_sky_blue2: 38,
|
|
805
|
-
deep_sky_blue1: 39,
|
|
806
|
-
green5: 40,
|
|
807
|
-
spring_green5: 41,
|
|
808
|
-
spring_green2: 42,
|
|
809
|
-
cyan3: 43,
|
|
810
|
-
dark_turquoise: 44,
|
|
811
|
-
turquoise2: 45,
|
|
812
|
-
green1: 46,
|
|
813
|
-
spring_green6: 47,
|
|
814
|
-
spring_green1: 48,
|
|
815
|
-
medium_spring_green: 49,
|
|
816
|
-
cyan2: 50,
|
|
817
|
-
cyan1: 51,
|
|
818
|
-
dark_red: 52,
|
|
819
|
-
deep_pink4: 53,
|
|
820
|
-
purple4: 54,
|
|
821
|
-
purple5: 55,
|
|
822
|
-
purple3: 56,
|
|
823
|
-
blue_violet: 57,
|
|
824
|
-
orange4: 58,
|
|
825
|
-
grey37: 59,
|
|
826
|
-
medium_purple4: 60,
|
|
827
|
-
slate_blue3: 61,
|
|
828
|
-
slate_blue4: 62,
|
|
829
|
-
royal_blue1: 63,
|
|
830
|
-
chartreuse4: 64,
|
|
831
|
-
dark_sea_green4: 65,
|
|
832
|
-
pale_turquoise4: 66,
|
|
833
|
-
steel_blue: 67,
|
|
834
|
-
steel_blue3: 68,
|
|
835
|
-
cornflower_blue: 69,
|
|
836
|
-
chartreuse3: 70,
|
|
837
|
-
dark_sea_green5: 71,
|
|
838
|
-
cadet_blue: 72,
|
|
839
|
-
cadet_blue2: 73,
|
|
840
|
-
sky_blue3: 74,
|
|
841
|
-
steel_blue1: 75,
|
|
842
|
-
chartreuse5: 76,
|
|
843
|
-
pale_green3: 77,
|
|
844
|
-
sea_green3: 78,
|
|
845
|
-
aquamarine3: 79,
|
|
846
|
-
medium_turquoise: 80,
|
|
847
|
-
steel_blue2: 81,
|
|
848
|
-
chartreuse2: 82,
|
|
849
|
-
sea_green2: 83,
|
|
850
|
-
sea_green1: 84,
|
|
851
|
-
sea_green4: 85,
|
|
852
|
-
aquamarine1: 86,
|
|
853
|
-
dark_slate_gray2: 87,
|
|
854
|
-
dark_red2: 88,
|
|
855
|
-
deep_pink5: 89,
|
|
856
|
-
dark_magenta: 90,
|
|
857
|
-
dark_magenta2: 91,
|
|
858
|
-
dark_violet: 92,
|
|
859
|
-
purple2: 93,
|
|
860
|
-
orange5: 94,
|
|
861
|
-
light_pink4: 95,
|
|
862
|
-
plum4: 96,
|
|
863
|
-
medium_purple3: 97,
|
|
864
|
-
medium_purple5: 98,
|
|
865
|
-
slate_blue1: 99,
|
|
866
|
-
yellow4: 100,
|
|
867
|
-
wheat4: 101,
|
|
868
|
-
grey53: 102,
|
|
869
|
-
light_slate_grey: 103,
|
|
870
|
-
medium_purple: 104,
|
|
871
|
-
light_slate_blue: 105,
|
|
872
|
-
yellow5: 106,
|
|
873
|
-
dark_olive_green3: 107,
|
|
874
|
-
dark_sea_green: 108,
|
|
875
|
-
light_sky_blue3: 109,
|
|
876
|
-
light_sky_blue4: 110,
|
|
877
|
-
sky_blue2: 111,
|
|
878
|
-
chartreuse6: 112,
|
|
879
|
-
dark_olive_green4: 113,
|
|
880
|
-
pale_green4: 114,
|
|
881
|
-
dark_sea_green3: 115,
|
|
882
|
-
dark_slate_gray3: 116,
|
|
883
|
-
sky_blue1: 117,
|
|
884
|
-
chartreuse1: 118,
|
|
885
|
-
light_green: 119,
|
|
886
|
-
light_green2: 120,
|
|
887
|
-
pale_green1: 121,
|
|
888
|
-
aquamarine2: 122,
|
|
889
|
-
dark_slate_gray1: 123,
|
|
890
|
-
red3: 124,
|
|
891
|
-
deep_pink6: 125,
|
|
892
|
-
medium_violet_red: 126,
|
|
893
|
-
magenta3: 127,
|
|
894
|
-
dark_violet2: 128,
|
|
895
|
-
purple: 129,
|
|
896
|
-
dark_orange3: 130,
|
|
897
|
-
indian_red: 131,
|
|
898
|
-
hot_pink3: 132,
|
|
899
|
-
medium_orchid3: 133,
|
|
900
|
-
medium_orchid: 134,
|
|
901
|
-
medium_purple2: 135,
|
|
902
|
-
dark_goldenrod: 136,
|
|
903
|
-
light_salmon3: 137,
|
|
904
|
-
rosy_brown: 138,
|
|
905
|
-
grey63: 139,
|
|
906
|
-
medium_purple6: 140,
|
|
907
|
-
medium_purple1: 141,
|
|
908
|
-
gold3: 142,
|
|
909
|
-
dark_khaki: 143,
|
|
910
|
-
navajo_white3: 144,
|
|
911
|
-
grey69: 145,
|
|
912
|
-
light_steel_blue3: 146,
|
|
913
|
-
light_steel_blue: 147,
|
|
914
|
-
yellow3: 148,
|
|
915
|
-
dark_olive_green5: 149,
|
|
916
|
-
dark_sea_green6: 150,
|
|
917
|
-
dark_sea_green2: 151,
|
|
918
|
-
light_cyan3: 152,
|
|
919
|
-
light_sky_blue1: 153,
|
|
920
|
-
green_yellow: 154,
|
|
921
|
-
dark_olive_green2: 155,
|
|
922
|
-
pale_green2: 156,
|
|
923
|
-
dark_sea_green7: 157,
|
|
924
|
-
dark_sea_green1: 158,
|
|
925
|
-
pale_turquoise1: 159,
|
|
926
|
-
red4: 160,
|
|
927
|
-
deep_pink3: 161,
|
|
928
|
-
deep_pink8: 162,
|
|
929
|
-
magenta4: 163,
|
|
930
|
-
magenta5: 164,
|
|
931
|
-
magenta2: 165,
|
|
932
|
-
dark_orange4: 166,
|
|
933
|
-
indian_red2: 167,
|
|
934
|
-
hot_pink4: 168,
|
|
935
|
-
hot_pink2: 169,
|
|
936
|
-
orchid: 170,
|
|
937
|
-
medium_orchid1: 171,
|
|
938
|
-
orange3: 172,
|
|
939
|
-
light_salmon4: 173,
|
|
940
|
-
light_pink3: 174,
|
|
941
|
-
pink3: 175,
|
|
942
|
-
plum3: 176,
|
|
943
|
-
violet: 177,
|
|
944
|
-
gold4: 178,
|
|
945
|
-
light_goldenrod3: 179,
|
|
946
|
-
tan: 180,
|
|
947
|
-
misty_rose3: 181,
|
|
948
|
-
thistle3: 182,
|
|
949
|
-
plum2: 183,
|
|
950
|
-
yellow6: 184,
|
|
951
|
-
khaki3: 185,
|
|
952
|
-
light_goldenrod2: 186,
|
|
953
|
-
light_yellow3: 187,
|
|
954
|
-
grey84: 188,
|
|
955
|
-
light_steel_blue1: 189,
|
|
956
|
-
yellow2: 190,
|
|
957
|
-
dark_olive_green1: 191,
|
|
958
|
-
dark_olive_green6: 192,
|
|
959
|
-
dark_sea_green8: 193,
|
|
960
|
-
honeydew2: 194,
|
|
961
|
-
light_cyan1: 195,
|
|
962
|
-
red1: 196,
|
|
963
|
-
deep_pink2: 197,
|
|
964
|
-
deep_pink1: 198,
|
|
965
|
-
deep_pink9: 199,
|
|
966
|
-
magenta6: 200,
|
|
967
|
-
magenta1: 201,
|
|
968
|
-
orange_red1: 202,
|
|
969
|
-
indian_red1: 203,
|
|
970
|
-
indian_red3: 204,
|
|
971
|
-
hot_pink5: 205,
|
|
972
|
-
hot_pink: 206,
|
|
973
|
-
medium_orchid2: 207,
|
|
974
|
-
dark_orange: 208,
|
|
975
|
-
salmon1: 209,
|
|
976
|
-
light_coral: 210,
|
|
977
|
-
pale_violet_red1: 211,
|
|
978
|
-
orchid2: 212,
|
|
979
|
-
orchid1: 213,
|
|
980
|
-
orange1: 214,
|
|
981
|
-
sandy_brown: 215,
|
|
982
|
-
light_salmon1: 216,
|
|
983
|
-
light_pink1: 217,
|
|
984
|
-
pink1: 218,
|
|
985
|
-
plum1: 219,
|
|
986
|
-
gold1: 220,
|
|
987
|
-
light_goldenrod4: 221,
|
|
988
|
-
light_goldenrod5: 222,
|
|
989
|
-
navajo_white1: 223,
|
|
990
|
-
misty_rose1: 224,
|
|
991
|
-
thistle1: 225,
|
|
992
|
-
yellow1: 226,
|
|
993
|
-
light_goldenrod1: 227,
|
|
994
|
-
khaki1: 228,
|
|
995
|
-
wheat1: 229,
|
|
996
|
-
cornsilk1: 230,
|
|
997
|
-
grey100: 231,
|
|
998
|
-
// Grayscale ramp (232-255)
|
|
999
|
-
grey3: 232,
|
|
1000
|
-
grey7: 233,
|
|
1001
|
-
grey11: 234,
|
|
1002
|
-
grey15: 235,
|
|
1003
|
-
grey19: 236,
|
|
1004
|
-
grey23: 237,
|
|
1005
|
-
grey27: 238,
|
|
1006
|
-
grey30: 239,
|
|
1007
|
-
grey35: 240,
|
|
1008
|
-
grey39: 241,
|
|
1009
|
-
grey42: 242,
|
|
1010
|
-
grey46: 243,
|
|
1011
|
-
grey50: 244,
|
|
1012
|
-
grey54: 245,
|
|
1013
|
-
grey58: 246,
|
|
1014
|
-
grey62: 247,
|
|
1015
|
-
grey66: 248,
|
|
1016
|
-
grey70: 249,
|
|
1017
|
-
grey74: 250,
|
|
1018
|
-
grey78: 251,
|
|
1019
|
-
grey82: 252,
|
|
1020
|
-
grey85: 253,
|
|
1021
|
-
grey89: 254,
|
|
1022
|
-
grey93: 255,
|
|
1023
|
-
});
|
|
1024
|
-
/**
|
|
1025
|
-
* Every word `ColorSpec.parse` accepts as a name rather than a `#hex`,
|
|
1026
|
-
* `rgb()` or `color(N)` form: `default` and the named palette entries, which
|
|
1027
|
-
* are exactly the two name checks `parseSingle` makes.
|
|
1028
|
-
*/
|
|
1029
|
-
export const COLOR_NAMES = ["default", ...Object.keys(ANSI_COLOR_NAMES)];
|