@promptctl/rich-js 0.18.0 → 0.19.0

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