@promptctl/rich-js 0.14.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 +18 -0
- package/dist/core/color.js +46 -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 +31 -17
- package/dist/core/style.d.ts +11 -0
- package/dist/core/style.js +27 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/themes/colorMath.d.ts +28 -2
- package/dist/themes/colorMath.js +59 -14
- package/dist/widgets/screen.js +4 -0
- package/package.json +1 -1
package/dist/core/color.d.ts
CHANGED
|
@@ -84,6 +84,15 @@ export declare class ColorTable {
|
|
|
84
84
|
* the ratio decides which one wins.
|
|
85
85
|
*/
|
|
86
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;
|
|
87
96
|
}
|
|
88
97
|
export declare enum ColorDepth {
|
|
89
98
|
DEFAULT = 0,
|
|
@@ -131,6 +140,15 @@ export declare class ColorSpec {
|
|
|
131
140
|
private downgradeCache;
|
|
132
141
|
constructor(name: string, type: ColorDepth, number?: number, value?: ColorRgba);
|
|
133
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;
|
|
134
152
|
get isSystemDefined(): boolean;
|
|
135
153
|
/**
|
|
136
154
|
* Generates SGR parameter strings for this color.
|
package/dist/core/color.js
CHANGED
|
@@ -122,6 +122,14 @@ function remember(cache, key, index) {
|
|
|
122
122
|
cache.set(key, index);
|
|
123
123
|
return index;
|
|
124
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
|
+
}
|
|
125
133
|
export class ColorTable {
|
|
126
134
|
colors;
|
|
127
135
|
firstIndex;
|
|
@@ -158,10 +166,7 @@ export class ColorTable {
|
|
|
158
166
|
let bestDist = Infinity;
|
|
159
167
|
for (let i = 0; i < this.colors.length; i++) {
|
|
160
168
|
const c = this.colors[i];
|
|
161
|
-
const
|
|
162
|
-
const dg = c.green - value.green;
|
|
163
|
-
const db = c.blue - value.blue;
|
|
164
|
-
const dist = dr * dr + dg * dg + db * db;
|
|
169
|
+
const dist = rgbDistance(c, value);
|
|
165
170
|
if (dist < bestDist) {
|
|
166
171
|
bestDist = dist;
|
|
167
172
|
bestIndex = i;
|
|
@@ -192,12 +197,9 @@ export class ColorTable {
|
|
|
192
197
|
const lc = this.luminances()[i];
|
|
193
198
|
const ratio = luminanceRatio(lc, lOn);
|
|
194
199
|
const passes = ratio >= minRatio;
|
|
195
|
-
const dr = c.red - value.red;
|
|
196
|
-
const dg = c.green - value.green;
|
|
197
|
-
const db = c.blue - value.blue;
|
|
198
200
|
// A passing entry scores by closeness; a failing one only by contrast,
|
|
199
201
|
// and loses to every passing one.
|
|
200
|
-
const score = passes ? -(
|
|
202
|
+
const score = passes ? -rgbDistance(c, value) : ratio;
|
|
201
203
|
if ((passes && !bestPasses) ||
|
|
202
204
|
(passes === bestPasses && score > bestScore)) {
|
|
203
205
|
best = i;
|
|
@@ -207,6 +209,20 @@ export class ColorTable {
|
|
|
207
209
|
}
|
|
208
210
|
return remember(this.readableCache, key, this.firstIndex + best);
|
|
209
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
|
+
}
|
|
210
226
|
}
|
|
211
227
|
// --- Enums ---
|
|
212
228
|
export var ColorDepth;
|
|
@@ -345,6 +361,28 @@ export class ColorSpec {
|
|
|
345
361
|
get isDefault() {
|
|
346
362
|
return this.type === ColorDepth.DEFAULT;
|
|
347
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
|
+
}
|
|
348
386
|
get isSystemDefined() {
|
|
349
387
|
return (this.type === ColorDepth.STANDARD || this.type === ColorDepth.WINDOWS);
|
|
350
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
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { Segment } from "./segment.js";
|
|
29
29
|
import { Style } from "./style.js";
|
|
30
|
-
import {
|
|
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
|
@@ -69,6 +69,17 @@ export declare class Style {
|
|
|
69
69
|
*/
|
|
70
70
|
add(other: Style | undefined): Style;
|
|
71
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
|
+
};
|
|
72
83
|
/**
|
|
73
84
|
* Returns the SGR parameter list this style emits (e.g. `"1;31;48;2;0;0;255"`),
|
|
74
85
|
* or `""` when the style has no SGR contribution. Excludes OSC 8 link bytes —
|
package/dist/core/style.js
CHANGED
|
@@ -264,6 +264,28 @@ export class Style {
|
|
|
264
264
|
this.overline === other.overline &&
|
|
265
265
|
this.link === other.link);
|
|
266
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
|
+
}
|
|
267
289
|
/**
|
|
268
290
|
* Returns the SGR parameter list this style emits (e.g. `"1;31;48;2;0;0;255"`),
|
|
269
291
|
* or `""` when the style has no SGR contribution. Excludes OSC 8 link bytes —
|
|
@@ -278,22 +300,11 @@ export class Style {
|
|
|
278
300
|
if (this.isNull)
|
|
279
301
|
return "";
|
|
280
302
|
const attrs = [];
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const bgFlat = this.bgcolor?.flattenAlpha(surface);
|
|
287
|
-
const fgSubstrate = bgFlat?.getTruecolor(undefined, false) ?? surface;
|
|
288
|
-
const fgFlat = this.color?.flattenAlpha(fgSubstrate);
|
|
289
|
-
if (fgFlat) {
|
|
290
|
-
const c = colorSystem !== undefined ? fgFlat.downgrade(colorSystem) : fgFlat;
|
|
291
|
-
attrs.push(...c.getAnsiCodes(true));
|
|
292
|
-
}
|
|
293
|
-
if (bgFlat) {
|
|
294
|
-
const c = colorSystem !== undefined ? bgFlat.downgrade(colorSystem) : bgFlat;
|
|
295
|
-
attrs.push(...c.getAnsiCodes(false));
|
|
296
|
-
}
|
|
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));
|
|
297
308
|
// An attribute set false writes nothing, as in Python Rich 9d8f9a3's
|
|
298
309
|
// `_make_ansi_codes`. It overrides an inherited attribute when styles
|
|
299
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
|
|
@@ -43,8 +43,11 @@ export { relativeLuminance, contrastRatio };
|
|
|
43
43
|
* the terminal rounds text and background independently, and two roundings
|
|
44
44
|
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
45
45
|
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
46
|
-
* that clears it (whose own rounding is itself).
|
|
47
|
-
*
|
|
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.
|
|
48
51
|
*
|
|
49
52
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
50
53
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
@@ -54,3 +57,26 @@ export { relativeLuminance, contrastRatio };
|
|
|
54
57
|
*/
|
|
55
58
|
export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number, // WCAG AA for normal text
|
|
56
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, SURFACE_BLACK, } 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) {
|
|
@@ -120,8 +120,11 @@ const CONTRAST_ITERS = 20;
|
|
|
120
120
|
* the terminal rounds text and background independently, and two roundings
|
|
121
121
|
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
122
122
|
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
123
|
-
* that clears it (whose own rounding is itself).
|
|
124
|
-
*
|
|
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.
|
|
125
128
|
*
|
|
126
129
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
127
130
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
@@ -134,16 +137,55 @@ drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK) {
|
|
|
134
137
|
const ground = drawnBackground(bg, substrate);
|
|
135
138
|
const chosen = ensureTruecolorContrast(fg, ground, minRatio);
|
|
136
139
|
// [LAW:dataflow-not-control-flow] The depth names the table the terminal
|
|
137
|
-
// draws from;
|
|
138
|
-
const table =
|
|
140
|
+
// draws from; every table is measured the same way.
|
|
141
|
+
const table = DRAWN_FROM[drawnAt];
|
|
139
142
|
if (table === undefined)
|
|
140
143
|
return chosen;
|
|
141
|
-
const drawnBg =
|
|
142
|
-
|
|
143
|
-
if (contrastRatio(drawn, drawnBg) >= minRatio)
|
|
144
|
+
const drawnBg = drawnColour(ground, drawnAt, substrate);
|
|
145
|
+
if (contrastRatio(drawnColour(chosen, drawnAt, substrate), drawnBg) >= minRatio)
|
|
144
146
|
return chosen;
|
|
145
147
|
return table.get(table.matchReadable(chosen, drawnBg, minRatio));
|
|
146
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
|
+
}
|
|
147
189
|
/**
|
|
148
190
|
* A background as it is drawn: composited over the surface beneath it. That
|
|
149
191
|
* surface is a fact about where the pair is drawn, so it arrives as a value:
|
|
@@ -165,14 +207,17 @@ function drawnBackground(bg, substrate) {
|
|
|
165
207
|
return bg.compositeOver(substrate);
|
|
166
208
|
}
|
|
167
209
|
/**
|
|
168
|
-
* The downgrade
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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.
|
|
173
217
|
*/
|
|
174
|
-
const
|
|
218
|
+
const DRAWN_FROM = {
|
|
175
219
|
[ColorDepth.EIGHT_BIT]: EIGHT_BIT_DOWNGRADE_TABLE,
|
|
220
|
+
[ColorDepth.STANDARD]: STANDARD_TABLE,
|
|
176
221
|
};
|
|
177
222
|
function ensureTruecolorContrast(fg, bg, minRatio) {
|
|
178
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 = [];
|