@promptctl/rich-js 0.13.0 → 0.15.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/dist/core/color.d.ts +26 -0
- package/dist/core/color.js +54 -8
- package/dist/core/console.js +1 -0
- package/dist/core/protocol.d.ts +9 -0
- package/dist/core/render.js +3 -0
- package/dist/core/strip.js +32 -18
- package/dist/core/style.d.ts +12 -2
- package/dist/core/style.js +30 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/themes/colorMath.d.ts +37 -9
- package/dist/themes/colorMath.js +91 -23
- package/dist/widgets/screen.js +4 -0
- package/package.json +1 -1
package/dist/core/color.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export declare class ColorRgba {
|
|
|
31
31
|
*/
|
|
32
32
|
compositeOver(bg: ColorRgba): ColorRgba;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The surface a terminal draws a translucent colour over. A terminal cannot
|
|
36
|
+
* know what lies under its cells, so the SGR writer (`Style.toSgrCodes`), the
|
|
37
|
+
* strip's seam test and the contrast choosers (`themes/colorMath`) all
|
|
38
|
+
* composite over this one colour. [LAW:one-source-of-truth] One constant, so
|
|
39
|
+
* the colour text is chosen against is the colour the writer draws.
|
|
40
|
+
*/
|
|
41
|
+
export declare const SURFACE_BLACK: ColorRgba;
|
|
34
42
|
/**
|
|
35
43
|
* WCAG 2.x relative luminance (0..1) of an opaque color. The single
|
|
36
44
|
* luminance function in the codebase — `contrastFor`, `contrastRatio`, and
|
|
@@ -76,6 +84,15 @@ export declare class ColorTable {
|
|
|
76
84
|
* the ratio decides which one wins.
|
|
77
85
|
*/
|
|
78
86
|
matchReadable(value: ColorRgba, on: ColorRgba, minRatio: number): number;
|
|
87
|
+
/**
|
|
88
|
+
* The nearest entry to `value` (the distance `match` uses) among those
|
|
89
|
+
* `accept` takes, given each entry's colour and terminal index; `undefined`
|
|
90
|
+
* when it takes none. Entries are offered nearest-first (ties to the lower
|
|
91
|
+
* index) and the first taken wins, so an expensive predicate runs only as
|
|
92
|
+
* far out as the answer. Uncached: the predicate is the caller's, and a
|
|
93
|
+
* closure has no key.
|
|
94
|
+
*/
|
|
95
|
+
matchWhere(value: ColorRgba, accept: (entry: ColorRgba, index: number) => boolean): number | undefined;
|
|
79
96
|
}
|
|
80
97
|
export declare enum ColorDepth {
|
|
81
98
|
DEFAULT = 0,
|
|
@@ -123,6 +140,15 @@ export declare class ColorSpec {
|
|
|
123
140
|
private downgradeCache;
|
|
124
141
|
constructor(name: string, type: ColorDepth, number?: number, value?: ColorRgba);
|
|
125
142
|
get isDefault(): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* The colour every terminal draws this spec as, where that is fixed — before
|
|
145
|
+
* any alpha is flattened, so a translucent value is drawn composited over
|
|
146
|
+
* its ground (`flattenAlpha`) and should be measured that way: a
|
|
147
|
+
* truecolor value, or a 256-colour cube or grey-ramp entry (xterm fixes
|
|
148
|
+
* indices 16–255, and `fromAnsi` types only those as EIGHT_BIT). ANSI 0–15
|
|
149
|
+
* and the default colour are the terminal theme's own, so they have none.
|
|
150
|
+
*/
|
|
151
|
+
get fixedValue(): ColorRgba | undefined;
|
|
126
152
|
get isSystemDefined(): boolean;
|
|
127
153
|
/**
|
|
128
154
|
* Generates SGR parameter strings for this color.
|
package/dist/core/color.js
CHANGED
|
@@ -65,6 +65,14 @@ export class ColorRgba {
|
|
|
65
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
66
|
}
|
|
67
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);
|
|
68
76
|
/**
|
|
69
77
|
* WCAG 2.x relative luminance (0..1) of an opaque color. The single
|
|
70
78
|
* luminance function in the codebase — `contrastFor`, `contrastRatio`, and
|
|
@@ -114,6 +122,14 @@ function remember(cache, key, index) {
|
|
|
114
122
|
cache.set(key, index);
|
|
115
123
|
return index;
|
|
116
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
|
+
}
|
|
117
133
|
export class ColorTable {
|
|
118
134
|
colors;
|
|
119
135
|
firstIndex;
|
|
@@ -150,10 +166,7 @@ export class ColorTable {
|
|
|
150
166
|
let bestDist = Infinity;
|
|
151
167
|
for (let i = 0; i < this.colors.length; i++) {
|
|
152
168
|
const c = this.colors[i];
|
|
153
|
-
const
|
|
154
|
-
const dg = c.green - value.green;
|
|
155
|
-
const db = c.blue - value.blue;
|
|
156
|
-
const dist = dr * dr + dg * dg + db * db;
|
|
169
|
+
const dist = rgbDistance(c, value);
|
|
157
170
|
if (dist < bestDist) {
|
|
158
171
|
bestDist = dist;
|
|
159
172
|
bestIndex = i;
|
|
@@ -184,12 +197,9 @@ export class ColorTable {
|
|
|
184
197
|
const lc = this.luminances()[i];
|
|
185
198
|
const ratio = luminanceRatio(lc, lOn);
|
|
186
199
|
const passes = ratio >= minRatio;
|
|
187
|
-
const dr = c.red - value.red;
|
|
188
|
-
const dg = c.green - value.green;
|
|
189
|
-
const db = c.blue - value.blue;
|
|
190
200
|
// A passing entry scores by closeness; a failing one only by contrast,
|
|
191
201
|
// and loses to every passing one.
|
|
192
|
-
const score = passes ? -(
|
|
202
|
+
const score = passes ? -rgbDistance(c, value) : ratio;
|
|
193
203
|
if ((passes && !bestPasses) ||
|
|
194
204
|
(passes === bestPasses && score > bestScore)) {
|
|
195
205
|
best = i;
|
|
@@ -199,6 +209,20 @@ export class ColorTable {
|
|
|
199
209
|
}
|
|
200
210
|
return remember(this.readableCache, key, this.firstIndex + best);
|
|
201
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
|
+
}
|
|
202
226
|
}
|
|
203
227
|
// --- Enums ---
|
|
204
228
|
export var ColorDepth;
|
|
@@ -337,6 +361,28 @@ export class ColorSpec {
|
|
|
337
361
|
get isDefault() {
|
|
338
362
|
return this.type === ColorDepth.DEFAULT;
|
|
339
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* The colour every terminal draws this spec as, where that is fixed — before
|
|
366
|
+
* any alpha is flattened, so a translucent value is drawn composited over
|
|
367
|
+
* its ground (`flattenAlpha`) and should be measured that way: a
|
|
368
|
+
* truecolor value, or a 256-colour cube or grey-ramp entry (xterm fixes
|
|
369
|
+
* indices 16–255, and `fromAnsi` types only those as EIGHT_BIT). ANSI 0–15
|
|
370
|
+
* and the default colour are the terminal theme's own, so they have none.
|
|
371
|
+
*/
|
|
372
|
+
get fixedValue() {
|
|
373
|
+
switch (this.type) {
|
|
374
|
+
case ColorDepth.TRUECOLOR:
|
|
375
|
+
return this.value;
|
|
376
|
+
case ColorDepth.EIGHT_BIT:
|
|
377
|
+
// The constructor admits an EIGHT_BIT spec at 0–15; those are the
|
|
378
|
+
// theme's own slots whatever depth names them.
|
|
379
|
+
return this.number < 16 ? undefined : EIGHT_BIT_TABLE.get(this.number);
|
|
380
|
+
case ColorDepth.DEFAULT:
|
|
381
|
+
case ColorDepth.STANDARD:
|
|
382
|
+
case ColorDepth.WINDOWS:
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
340
386
|
get isSystemDefined() {
|
|
341
387
|
return (this.type === ColorDepth.STANDARD || this.type === ColorDepth.WINDOWS);
|
|
342
388
|
}
|
package/dist/core/console.js
CHANGED
package/dist/core/protocol.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Segment } from "./segment.js";
|
|
6
6
|
import { type Style, type StyleSyntaxError, type Theme } from "./style.js";
|
|
7
|
+
import type { ColorDepth } from "./color.js";
|
|
7
8
|
export interface RenderOptions {
|
|
8
9
|
/**
|
|
9
10
|
* The cells this renderable may occupy. A count of cells, so a non-negative
|
|
@@ -25,6 +26,14 @@ export interface RenderOptions {
|
|
|
25
26
|
noWrap?: boolean;
|
|
26
27
|
highlight?: unknown;
|
|
27
28
|
markup?: unknown;
|
|
29
|
+
/**
|
|
30
|
+
* The depth the output will be encoded at, `null` when it carries no colour.
|
|
31
|
+
* A renderable choosing between two ways of drawing something — the strip's
|
|
32
|
+
* arrow or its divider — decides on the colours the terminal will draw at
|
|
33
|
+
* this depth, which at 256 colours or fewer are not the colours it was
|
|
34
|
+
* handed. Absent means truecolor.
|
|
35
|
+
*/
|
|
36
|
+
colorSystem?: ColorDepth | null;
|
|
28
37
|
/**
|
|
29
38
|
* The names a style string may use. A `Console` passes its own; absent, the
|
|
30
39
|
* built-in defaults apply. Read it through `getStyle` rather than directly.
|
package/dist/core/render.js
CHANGED
|
@@ -132,6 +132,9 @@ export function renderToString(renderable, options) {
|
|
|
132
132
|
isTerminal: false,
|
|
133
133
|
encoding: "utf-8",
|
|
134
134
|
asciiOnly: false,
|
|
135
|
+
// [LAW:one-source-of-truth] The depth the segments below are encoded at,
|
|
136
|
+
// so a renderable measures what this very call will draw.
|
|
137
|
+
colorSystem,
|
|
135
138
|
};
|
|
136
139
|
return segmentsToString(renderable.render(renderOptions), colorSystem);
|
|
137
140
|
}
|
package/dist/core/strip.js
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* styling, so the cell type does too.
|
|
27
27
|
*/
|
|
28
28
|
import { Segment } from "./segment.js";
|
|
29
|
-
import { Style
|
|
30
|
-
import { ColorSpec, blendRgb } from "./color.js";
|
|
29
|
+
import { Style } from "./style.js";
|
|
30
|
+
import { ColorDepth, ColorSpec, blendRgb } from "./color.js";
|
|
31
31
|
import { Oklch } from "./oklch.js";
|
|
32
32
|
// --- Strip ---
|
|
33
33
|
export class Strip {
|
|
@@ -110,20 +110,25 @@ function paintableBg(bg) {
|
|
|
110
110
|
* one cell wide.
|
|
111
111
|
*/
|
|
112
112
|
export const SEAM_MIN_DELTA_E = 0.04;
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
// An arrow the eye cannot tell from the ground it is drawn on. What is
|
|
114
|
+
// measured is what is drawn — `Style.drawnColors`, the colours the writer
|
|
115
|
+
// encodes: the arrow's colour flattened onto its ground and the ground onto
|
|
116
|
+
// the terminal's black, both downgraded to the depth the render encodes at,
|
|
117
|
+
// so two grounds 256 colours round to one cube entry are the one entry they
|
|
118
|
+
// render as. A colour whose RGB is the terminal theme's own (ANSI 0–15) has no
|
|
119
|
+
// value here, so two of them are the same only when they are the same palette
|
|
120
|
+
// slot — its `number`, never the name or the depth that spells it ("red",
|
|
121
|
+
// "color(1)", an EIGHT_BIT spec on 0–15 are one slot).
|
|
122
|
+
function vanishes(arrow, colorSystem) {
|
|
123
|
+
// No colour emitted draws nothing to tell apart; measure what was handed.
|
|
124
|
+
const { color, bgcolor } = arrow.drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
|
|
125
|
+
if (color === undefined || bgcolor === undefined)
|
|
121
126
|
return false;
|
|
122
|
-
const av =
|
|
123
|
-
const bv =
|
|
127
|
+
const av = color.fixedValue;
|
|
128
|
+
const bv = bgcolor.fixedValue;
|
|
124
129
|
return av !== undefined && bv !== undefined
|
|
125
130
|
? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
|
|
126
|
-
:
|
|
131
|
+
: color.number === bgcolor.number;
|
|
127
132
|
}
|
|
128
133
|
/** The powerline pair: U+E0B0 (right-arrow) divided by U+E0B1 (thin right-arrow). */
|
|
129
134
|
export const POWERLINE_JOINER_GLYPHS = Object.freeze({ glyph: "\ue0b0", divider: "\ue0b1" });
|
|
@@ -150,8 +155,10 @@ export class PowerlineJoiner {
|
|
|
150
155
|
// structural boundary, never suppressed. The arrow would be drawn in its own
|
|
151
156
|
// background colour there and vanish, so the seam is the DIVIDER instead, in
|
|
152
157
|
// the left item's text colour — the vim-airline convention for neighbours
|
|
153
|
-
// that share a background. "Equal" is perceptual (SEAM_MIN_DELTA_E)
|
|
154
|
-
//
|
|
158
|
+
// that share a background. "Equal" is perceptual (SEAM_MIN_DELTA_E) and
|
|
159
|
+
// measured on the colours the terminal draws at `options.colorSystem`: an
|
|
160
|
+
// arrow a hair off its background is as invisible as one exactly on it,
|
|
161
|
+
// and two backgrounds 256 colours round to one entry are one background.
|
|
155
162
|
// Background colour is paint, not structure; its ABSENCE (nothing to paint)
|
|
156
163
|
// is the only thing that elides the separator, and that is paint logic.
|
|
157
164
|
// "Absent" = no bg OR the terminal default (transparent) — paintableBg folds
|
|
@@ -164,9 +171,16 @@ export class PowerlineJoiner {
|
|
|
164
171
|
if (leftBg === undefined)
|
|
165
172
|
return;
|
|
166
173
|
const rightBg = paintableBg(right?.edgeStyle("left", options).bgcolor);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
// The arrow is the left cell continuing: its ground as the writer draws
|
|
175
|
+
// it, so a translucent ground is not composited a second time over the
|
|
176
|
+
// right one.
|
|
177
|
+
const leftDrawn = new Style({ bgcolor: leftBg }).drawnColors().bgcolor;
|
|
178
|
+
const arrow = new Style({ color: leftDrawn, bgcolor: rightBg });
|
|
179
|
+
// The divider is the left item's text on the left item's own ground,
|
|
180
|
+
// so it reads exactly as well as that item's text does, at any depth.
|
|
181
|
+
yield vanishes(arrow, options.colorSystem)
|
|
182
|
+
? new Segment(divider, new Style({ color: leftEdge?.color, bgcolor: leftBg }))
|
|
183
|
+
: new Segment(glyph, arrow);
|
|
170
184
|
});
|
|
171
185
|
}
|
|
172
186
|
}
|
package/dist/core/style.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Immutable style descriptors — colors, text attributes, links, metadata.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
export declare const SURFACE_BLACK: ColorRgba;
|
|
4
|
+
import { ColorSpec, ColorDepth } from "./color.js";
|
|
6
5
|
/**
|
|
7
6
|
* Canonical text-attribute inventory. Single source of truth consumed by
|
|
8
7
|
* `Style.parse` / `Style.toString` and the template bindings — adding an
|
|
@@ -70,6 +69,17 @@ export declare class Style {
|
|
|
70
69
|
*/
|
|
71
70
|
add(other: Style | undefined): Style;
|
|
72
71
|
equals(other: Style): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The colours this style puts on screen at `colorSystem`: each flattened
|
|
74
|
+
* onto what lies beneath it — the background onto the terminal's black, the
|
|
75
|
+
* foreground onto that background — then downgraded to the depth. The one
|
|
76
|
+
* account of what a style draws: `toSgrCodes` encodes exactly these, and a
|
|
77
|
+
* renderable choosing between two ways of drawing measures them.
|
|
78
|
+
*/
|
|
79
|
+
drawnColors(colorSystem?: ColorDepth): {
|
|
80
|
+
readonly color: ColorSpec | undefined;
|
|
81
|
+
readonly bgcolor: ColorSpec | undefined;
|
|
82
|
+
};
|
|
73
83
|
/**
|
|
74
84
|
* Returns the SGR parameter list this style emits (e.g. `"1;31;48;2;0;0;255"`),
|
|
75
85
|
* or `""` when the style has no SGR contribution. Excludes OSC 8 link bytes —
|
package/dist/core/style.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Immutable style descriptors — colors, text attributes, links, metadata.
|
|
3
3
|
*/
|
|
4
|
-
import { COLOR_NAMES,
|
|
4
|
+
import { COLOR_NAMES, ColorSpec, SURFACE_BLACK, } from "./color.js";
|
|
5
5
|
import { OSC8_CLOSE, osc8Open } from "./osc8.js";
|
|
6
6
|
// [LAW:one-way-deps] `core/style` depends only on `core/color` and the leaf
|
|
7
|
-
// `core/osc8` (the link wire grammar). The substrate
|
|
8
|
-
//
|
|
9
|
-
// theme constants. Preset themes live in `src/themes/` and depend on core,
|
|
10
|
-
// never the reverse.
|
|
11
|
-
export const SURFACE_BLACK = new ColorRgba(0, 0, 0);
|
|
7
|
+
// `core/osc8` (the link wire grammar). The substrate a translucent colour is
|
|
8
|
+
// flattened over is `SURFACE_BLACK`, defined beside `compositeOver`.
|
|
12
9
|
// --- Attribute definitions ---
|
|
13
10
|
/**
|
|
14
11
|
* Canonical text-attribute inventory. Single source of truth consumed by
|
|
@@ -267,6 +264,28 @@ export class Style {
|
|
|
267
264
|
this.overline === other.overline &&
|
|
268
265
|
this.link === other.link);
|
|
269
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* The colours this style puts on screen at `colorSystem`: each flattened
|
|
269
|
+
* onto what lies beneath it — the background onto the terminal's black, the
|
|
270
|
+
* foreground onto that background — then downgraded to the depth. The one
|
|
271
|
+
* account of what a style draws: `toSgrCodes` encodes exactly these, and a
|
|
272
|
+
* renderable choosing between two ways of drawing measures them.
|
|
273
|
+
*/
|
|
274
|
+
drawnColors(colorSystem) {
|
|
275
|
+
// [LAW:dataflow-not-control-flow] Always resolve a substrate and flatten
|
|
276
|
+
// alpha before downgrade. Opaque colors short-circuit inside compositeOver,
|
|
277
|
+
// so the same code path runs every render — the alpha value is the data,
|
|
278
|
+
// not a branch.
|
|
279
|
+
const surface = SURFACE_BLACK;
|
|
280
|
+
const bgFlat = this.bgcolor?.flattenAlpha(surface);
|
|
281
|
+
const fgSubstrate = bgFlat?.getTruecolor(undefined, false) ?? surface;
|
|
282
|
+
const fgFlat = this.color?.flattenAlpha(fgSubstrate);
|
|
283
|
+
const at = (c) => colorSystem !== undefined ? c.downgrade(colorSystem) : c;
|
|
284
|
+
return {
|
|
285
|
+
color: fgFlat === undefined ? undefined : at(fgFlat),
|
|
286
|
+
bgcolor: bgFlat === undefined ? undefined : at(bgFlat),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
270
289
|
/**
|
|
271
290
|
* Returns the SGR parameter list this style emits (e.g. `"1;31;48;2;0;0;255"`),
|
|
272
291
|
* or `""` when the style has no SGR contribution. Excludes OSC 8 link bytes —
|
|
@@ -281,22 +300,11 @@ export class Style {
|
|
|
281
300
|
if (this.isNull)
|
|
282
301
|
return "";
|
|
283
302
|
const attrs = [];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const bgFlat = this.bgcolor?.flattenAlpha(surface);
|
|
290
|
-
const fgSubstrate = bgFlat?.getTruecolor(undefined, false) ?? surface;
|
|
291
|
-
const fgFlat = this.color?.flattenAlpha(fgSubstrate);
|
|
292
|
-
if (fgFlat) {
|
|
293
|
-
const c = colorSystem !== undefined ? fgFlat.downgrade(colorSystem) : fgFlat;
|
|
294
|
-
attrs.push(...c.getAnsiCodes(true));
|
|
295
|
-
}
|
|
296
|
-
if (bgFlat) {
|
|
297
|
-
const c = colorSystem !== undefined ? bgFlat.downgrade(colorSystem) : bgFlat;
|
|
298
|
-
attrs.push(...c.getAnsiCodes(false));
|
|
299
|
-
}
|
|
303
|
+
const drawn = this.drawnColors(colorSystem);
|
|
304
|
+
if (drawn.color)
|
|
305
|
+
attrs.push(...drawn.color.getAnsiCodes(true));
|
|
306
|
+
if (drawn.bgcolor)
|
|
307
|
+
attrs.push(...drawn.bgcolor.getAnsiCodes(false));
|
|
300
308
|
// An attribute set false writes nothing, as in Python Rich 9d8f9a3's
|
|
301
309
|
// `_make_ansi_codes`. It overrides an inherited attribute when styles
|
|
302
310
|
// combine, and every styled run is written after a reset, so no off
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { DEFAULT_TERMINAL_THEME, MONOKAI, SVG_EXPORT_THEME, NORD, GRUVBOX, DRACU
|
|
|
12
12
|
export { getThemePalette, listThemePalettes } from "./themes/registry.js";
|
|
13
13
|
export type { ThemeName } from "./themes/registry.js";
|
|
14
14
|
export { transposePalette, themeKeyForRoot, isAnchored, ANCHORED_ROOTS, } from "./themes/transpose.js";
|
|
15
|
-
export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, } from "./themes/colorMath.js";
|
|
15
|
+
export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, ensureDrawn, drawnColour, } from "./themes/colorMath.js";
|
|
16
16
|
export { ColorRamp, RAMP_EASING_NAMES, parseRampEasing } from "./themes/ramp.js";
|
|
17
17
|
export type { ColorStop, RampEasing } from "./themes/ramp.js";
|
|
18
18
|
export { Style, StyleSyntaxError, StyleStack, Theme, NULL_STYLE, DEFAULT_STYLES, } from "./core/style.js";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export { transposePalette, themeKeyForRoot, isAnchored, ANCHORED_ROOTS, } from "
|
|
|
21
21
|
// reaches for to tint a resolved color against itself (e.g. a focused-while-open
|
|
22
22
|
// menu cell lightening its inherited background) without re-deriving from a
|
|
23
23
|
// palette name. Negative levels invert (lighten(c,-n) === darken(c,n)).
|
|
24
|
-
export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, } from "./themes/colorMath.js";
|
|
24
|
+
export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, ensureDrawn, drawnColour, } from "./themes/colorMath.js";
|
|
25
25
|
// A number → a color through ordered stops, interpolated in OKLCH — the one
|
|
26
26
|
// color operation whose input is a measurement rather than a color, so a
|
|
27
27
|
// gradient (or, with the `step` easing, a threshold cascade) stays inside the
|
|
@@ -17,9 +17,10 @@ export declare function alphaBlend(fg: ColorRgba, bg: ColorRgba, alpha: number):
|
|
|
17
17
|
/**
|
|
18
18
|
* Pick a contrasting foreground (black or white) for a background, using the
|
|
19
19
|
* WCAG relative-luminance threshold of 0.179 (the perceptually correct cutoff
|
|
20
|
-
* where black and white are equally readable).
|
|
20
|
+
* where black and white are equally readable). A translucent `bg` is judged
|
|
21
|
+
* as drawn: composited over `substrate` (see `drawnBackground`).
|
|
21
22
|
*/
|
|
22
|
-
export declare function contrastFor(bg: ColorRgba): ColorRgba;
|
|
23
|
+
export declare function contrastFor(bg: ColorRgba, substrate?: ColorRgba): ColorRgba;
|
|
23
24
|
export { relativeLuminance, contrastRatio };
|
|
24
25
|
/**
|
|
25
26
|
* Return a foreground guaranteed to clear `minRatio` against `bg`, keeping the
|
|
@@ -32,17 +33,21 @@ export { relativeLuminance, contrastRatio };
|
|
|
32
33
|
* background where even pure black-or-white tops out below the target) does it
|
|
33
34
|
* fall back to `contrastFor`'s black/white — the true maximum-contrast pick.
|
|
34
35
|
*
|
|
35
|
-
* A translucent `
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* A translucent `bg` is measured as it is drawn — composited over
|
|
37
|
+
* `substrate`, the SGR writer's black by default — and a translucent `fg` is
|
|
38
|
+
* then flattened over that drawn background, the order the writer composites
|
|
39
|
+
* in, so the ratio is measured on what the eye actually sees and the returned
|
|
40
|
+
* color is opaque.
|
|
39
41
|
*
|
|
40
42
|
* `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
|
|
41
43
|
* the terminal rounds text and background independently, and two roundings
|
|
42
44
|
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
43
45
|
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
44
|
-
* that clears it (whose own rounding is itself).
|
|
45
|
-
*
|
|
46
|
+
* that clears it (whose own rounding is itself). At ANSI the terminal draws
|
|
47
|
+
* its own theme's colours, so the ratio is measured on the table's nominal
|
|
48
|
+
* ones (`DRAWN_FROM`): they say which side of the ground text belongs on, and
|
|
49
|
+
* text on its background's index measures 1:1 and is always replaced.
|
|
50
|
+
* Truecolor draws the colour chosen.
|
|
46
51
|
*
|
|
47
52
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
48
53
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
@@ -51,4 +56,27 @@ export { relativeLuminance, contrastRatio };
|
|
|
51
56
|
* lightness moves — there is no caller-side "should I check contrast" branch.
|
|
52
57
|
*/
|
|
53
58
|
export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number, // WCAG AA for normal text
|
|
54
|
-
drawnAt?: ColorDepth): ColorRgba;
|
|
59
|
+
drawnAt?: ColorDepth, substrate?: ColorRgba): ColorRgba;
|
|
60
|
+
/**
|
|
61
|
+
* `chosen` (composited opaque over `substrate`), or — when the depth the
|
|
62
|
+
* terminal draws at rounds it to a colour `accept` refuses — the nearest colour that depth draws as itself which
|
|
63
|
+
* `accept` takes. `accept` sees the candidate as drawn, and `drawn`, the same
|
|
64
|
+
* rounding for any other colour it measures against, so the caller states a
|
|
65
|
+
* floor once and it holds on the colours the terminal shows — at ANSI, on
|
|
66
|
+
* the table's nominal colours, as `ensureContrast` measures there. Truecolor
|
|
67
|
+
* draws from no table, so a colour refused there has no replacement;
|
|
68
|
+
* `undefined` means nothing the depth draws is accepted.
|
|
69
|
+
*
|
|
70
|
+
* `ensureContrast` is this with a contrast ratio as the floor; this is for a
|
|
71
|
+
* floor that is not text on its background (an open state standing off every
|
|
72
|
+
* closed cell, two planes standing off each other).
|
|
73
|
+
*/
|
|
74
|
+
export declare function ensureDrawn(chosen: ColorRgba, drawnAt: ColorDepth, accept: (candidate: ColorRgba, drawn: (c: ColorRgba) => ColorRgba) => boolean, substrate?: ColorRgba): ColorRgba | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* The colour a ground is shown as at `drawnAt`: composited over `substrate`
|
|
77
|
+
* (the SGR writer's black by default), then rounded to the table that depth
|
|
78
|
+
* draws from — at ANSI, that table's nominal colour stands in for the
|
|
79
|
+
* theme's. The one account `ensureDrawn` and `ensureContrast` measure by, for
|
|
80
|
+
* a caller that measures a floor of its own.
|
|
81
|
+
*/
|
|
82
|
+
export declare function drawnColour(colour: ColorRgba, drawnAt: ColorDepth, substrate?: ColorRgba): ColorRgba;
|
package/dist/themes/colorMath.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorDepth, ColorRgba, EIGHT_BIT_DOWNGRADE_TABLE, blendRgb, contrastRatio, relativeLuminance, } from "../core/color.js";
|
|
1
|
+
import { ColorDepth, ColorRgba, EIGHT_BIT_DOWNGRADE_TABLE, STANDARD_TABLE, blendRgb, contrastRatio, relativeLuminance, SURFACE_BLACK, } from "../core/color.js";
|
|
2
2
|
import { Oklch } from "../core/oklch.js";
|
|
3
3
|
const LEVEL_STEP = 0.1;
|
|
4
4
|
function rgbToHsl(c) {
|
|
@@ -82,10 +82,11 @@ export function alphaBlend(fg, bg, alpha) {
|
|
|
82
82
|
/**
|
|
83
83
|
* Pick a contrasting foreground (black or white) for a background, using the
|
|
84
84
|
* WCAG relative-luminance threshold of 0.179 (the perceptually correct cutoff
|
|
85
|
-
* where black and white are equally readable).
|
|
85
|
+
* where black and white are equally readable). A translucent `bg` is judged
|
|
86
|
+
* as drawn: composited over `substrate` (see `drawnBackground`).
|
|
86
87
|
*/
|
|
87
|
-
export function contrastFor(bg) {
|
|
88
|
-
const lum = relativeLuminance(bg);
|
|
88
|
+
export function contrastFor(bg, substrate = SURFACE_BLACK) {
|
|
89
|
+
const lum = relativeLuminance(drawnBackground(bg, substrate));
|
|
89
90
|
return lum > 0.179
|
|
90
91
|
? new ColorRgba(0, 0, 0)
|
|
91
92
|
: new ColorRgba(255, 255, 255);
|
|
@@ -109,17 +110,21 @@ const CONTRAST_ITERS = 20;
|
|
|
109
110
|
* background where even pure black-or-white tops out below the target) does it
|
|
110
111
|
* fall back to `contrastFor`'s black/white — the true maximum-contrast pick.
|
|
111
112
|
*
|
|
112
|
-
* A translucent `
|
|
113
|
-
* `
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* A translucent `bg` is measured as it is drawn — composited over
|
|
114
|
+
* `substrate`, the SGR writer's black by default — and a translucent `fg` is
|
|
115
|
+
* then flattened over that drawn background, the order the writer composites
|
|
116
|
+
* in, so the ratio is measured on what the eye actually sees and the returned
|
|
117
|
+
* color is opaque.
|
|
116
118
|
*
|
|
117
119
|
* `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
|
|
118
120
|
* the terminal rounds text and background independently, and two roundings
|
|
119
121
|
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
120
122
|
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
121
|
-
* that clears it (whose own rounding is itself).
|
|
122
|
-
*
|
|
123
|
+
* that clears it (whose own rounding is itself). At ANSI the terminal draws
|
|
124
|
+
* its own theme's colours, so the ratio is measured on the table's nominal
|
|
125
|
+
* ones (`DRAWN_FROM`): they say which side of the ground text belongs on, and
|
|
126
|
+
* text on its background's index measures 1:1 and is always replaced.
|
|
127
|
+
* Truecolor draws the colour chosen.
|
|
123
128
|
*
|
|
124
129
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
125
130
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
@@ -128,28 +133,91 @@ const CONTRAST_ITERS = 20;
|
|
|
128
133
|
* lightness moves — there is no caller-side "should I check contrast" branch.
|
|
129
134
|
*/
|
|
130
135
|
export function ensureContrast(fg, bg, minRatio = 4.5, // WCAG AA for normal text
|
|
131
|
-
drawnAt = ColorDepth.TRUECOLOR) {
|
|
132
|
-
const
|
|
136
|
+
drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK) {
|
|
137
|
+
const ground = drawnBackground(bg, substrate);
|
|
138
|
+
const chosen = ensureTruecolorContrast(fg, ground, minRatio);
|
|
133
139
|
// [LAW:dataflow-not-control-flow] The depth names the table the terminal
|
|
134
|
-
// draws from;
|
|
135
|
-
const table =
|
|
140
|
+
// draws from; every table is measured the same way.
|
|
141
|
+
const table = DRAWN_FROM[drawnAt];
|
|
136
142
|
if (table === undefined)
|
|
137
143
|
return chosen;
|
|
138
|
-
const drawnBg =
|
|
139
|
-
|
|
140
|
-
if (contrastRatio(drawn, drawnBg) >= minRatio)
|
|
144
|
+
const drawnBg = drawnColour(ground, drawnAt, substrate);
|
|
145
|
+
if (contrastRatio(drawnColour(chosen, drawnAt, substrate), drawnBg) >= minRatio)
|
|
141
146
|
return chosen;
|
|
142
147
|
return table.get(table.matchReadable(chosen, drawnBg, minRatio));
|
|
143
148
|
}
|
|
144
149
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
+
* `chosen` (composited opaque over `substrate`), or — when the depth the
|
|
151
|
+
* terminal draws at rounds it to a colour `accept` refuses — the nearest colour that depth draws as itself which
|
|
152
|
+
* `accept` takes. `accept` sees the candidate as drawn, and `drawn`, the same
|
|
153
|
+
* rounding for any other colour it measures against, so the caller states a
|
|
154
|
+
* floor once and it holds on the colours the terminal shows — at ANSI, on
|
|
155
|
+
* the table's nominal colours, as `ensureContrast` measures there. Truecolor
|
|
156
|
+
* draws from no table, so a colour refused there has no replacement;
|
|
157
|
+
* `undefined` means nothing the depth draws is accepted.
|
|
158
|
+
*
|
|
159
|
+
* `ensureContrast` is this with a contrast ratio as the floor; this is for a
|
|
160
|
+
* floor that is not text on its background (an open state standing off every
|
|
161
|
+
* closed cell, two planes standing off each other).
|
|
162
|
+
*/
|
|
163
|
+
export function ensureDrawn(chosen, drawnAt, accept, substrate = SURFACE_BLACK) {
|
|
164
|
+
// The floor was measured on `chosen` composited over `substrate`, so that
|
|
165
|
+
// opaque colour is what is returned — never a translucent one a different
|
|
166
|
+
// ground would composite into a colour `accept` never saw.
|
|
167
|
+
const opaque = drawnBackground(chosen, substrate);
|
|
168
|
+
const drawn = (c) => drawnColour(c, drawnAt, substrate);
|
|
169
|
+
if (accept(drawn(opaque), drawn))
|
|
170
|
+
return opaque;
|
|
171
|
+
// [LAW:dataflow-not-control-flow] Truecolor draws from no table, so it has
|
|
172
|
+
// no candidates: a refusal there is `undefined` like any exhausted search.
|
|
173
|
+
const table = DRAWN_FROM[drawnAt];
|
|
174
|
+
const index = table?.matchWhere(opaque, (entry) => accept(entry, drawn));
|
|
175
|
+
return index === undefined ? undefined : table.get(index);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The colour a ground is shown as at `drawnAt`: composited over `substrate`
|
|
179
|
+
* (the SGR writer's black by default), then rounded to the table that depth
|
|
180
|
+
* draws from — at ANSI, that table's nominal colour stands in for the
|
|
181
|
+
* theme's. The one account `ensureDrawn` and `ensureContrast` measure by, for
|
|
182
|
+
* a caller that measures a floor of its own.
|
|
183
|
+
*/
|
|
184
|
+
export function drawnColour(colour, drawnAt, substrate = SURFACE_BLACK) {
|
|
185
|
+
const opaque = drawnBackground(colour, substrate);
|
|
186
|
+
const table = DRAWN_FROM[drawnAt];
|
|
187
|
+
return table === undefined ? opaque : table.get(table.match(opaque));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A background as it is drawn: composited over the surface beneath it. That
|
|
191
|
+
* surface is a fact about where the pair is drawn, so it arrives as a value:
|
|
192
|
+
* the SGR writer (`Style.toSgrCodes`) composites over `SURFACE_BLACK`, the
|
|
193
|
+
* default here; a caller choosing text for a different surface — an export's
|
|
194
|
+
* canvas, `exportCanvas(theme).background` — names that one.
|
|
195
|
+
* [LAW:no-silent-failure] A surface has nothing under it, so a translucent one
|
|
196
|
+
* has no drawn colour to offer; `compositeOver` would read its raw RGB as if
|
|
197
|
+
* it were opaque, so it is refused here rather than measured wrong.
|
|
198
|
+
* [LAW:one-source-of-truth] Text is chosen against the colour the surface will
|
|
199
|
+
* show — measuring the raw RGBA reads a colour that is drawn nowhere, and text
|
|
200
|
+
* that "clears" it can land below the floor. Opaque colours composite to
|
|
201
|
+
* themselves.
|
|
202
|
+
*/
|
|
203
|
+
function drawnBackground(bg, substrate) {
|
|
204
|
+
if (substrate.alpha !== 1) {
|
|
205
|
+
throw new RangeError(`a contrast substrate is the opaque surface under a translucent background; got ${substrate.hex}`);
|
|
206
|
+
}
|
|
207
|
+
return bg.compositeOver(substrate);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The downgrade table the terminal draws from, by depth, measured at each
|
|
211
|
+
* entry's colour. The 256-colour cube and grey ramp are fixed by xterm, so
|
|
212
|
+
* those measurements are exact. ANSI 0–15 are the terminal theme's own, so
|
|
213
|
+
* the table's nominal colours stand in for them: they are wrong in hue from
|
|
214
|
+
* theme to theme, but not about which side of a ground an entry sits on —
|
|
215
|
+
* and two colours on one index are one colour in every theme, which the
|
|
216
|
+
* nominal ratio of 1 refuses. Truecolor draws the chosen colour itself.
|
|
150
217
|
*/
|
|
151
|
-
const
|
|
218
|
+
const DRAWN_FROM = {
|
|
152
219
|
[ColorDepth.EIGHT_BIT]: EIGHT_BIT_DOWNGRADE_TABLE,
|
|
220
|
+
[ColorDepth.STANDARD]: STANDARD_TABLE,
|
|
153
221
|
};
|
|
154
222
|
function ensureTruecolorContrast(fg, bg, minRatio) {
|
|
155
223
|
// Flatten translucency so the guarantee holds for the displayed color, not
|
package/dist/widgets/screen.js
CHANGED
|
@@ -245,6 +245,10 @@ let DefaultScreen = (() => {
|
|
|
245
245
|
maxWidth: width,
|
|
246
246
|
isTerminal: true,
|
|
247
247
|
encoding: "utf-8",
|
|
248
|
+
// [LAW:one-source-of-truth] The depth the frame is encoded at (below,
|
|
249
|
+
// `segmentsToString`), so a renderable that decides on drawn colours
|
|
250
|
+
// decides on the ones this screen draws.
|
|
251
|
+
colorSystem: this.colorSystem,
|
|
248
252
|
};
|
|
249
253
|
const lines = [];
|
|
250
254
|
const boundsList = [];
|