@promptctl/rich-js 0.24.1 → 0.25.1

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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +24 -33
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +98 -29
  47. package/dist/core/strip.js +211 -150
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +158 -63
  73. package/dist/renderables/effect.js +378 -155
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
package/dist/core/ansi.js CHANGED
@@ -51,10 +51,10 @@ import { RichText } from "./text.js";
51
51
  * and one final byte (`\x1b(B`, `\x1b)0`, `\x1b#8`, `\x1bc`). Only the first
52
52
  * three become tokens.
53
53
  */
54
- const ESCAPE = /\r|\x1b\[([0-9;:]*)m|\x1b\[([012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[\]P_^X][\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[ -/]*[0-~]/g;
54
+ const escapes = () => /\r|\x1b\[([0-9;:]*)m|\x1b\[([012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[\]P_^X][\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[ -/]*[0-~]/g;
55
55
  function* escapeTokens(bytes) {
56
56
  let at = 0;
57
- for (const match of bytes.matchAll(ESCAPE)) {
57
+ for (const match of bytes.matchAll(escapes())) {
58
58
  if (match.index > at)
59
59
  yield { kind: "text", text: bytes.slice(at, match.index) };
60
60
  if (match[0] === "\r")
@@ -28,19 +28,19 @@ import { Segment } from "./segment.js";
28
28
  import type { Style } from "./style.js";
29
29
  /** A rule spanning the table: the top and bottom borders, and every separator. */
30
30
  export interface EdgeChars {
31
- left: string;
32
- horizontal: string;
33
- cross: string;
34
- right: string;
31
+ readonly left: string;
32
+ readonly horizontal: string;
33
+ readonly cross: string;
34
+ readonly right: string;
35
35
  }
36
36
  /**
37
37
  * The verticals framing a row of cells. A content line of the grid has no
38
38
  * horizontal of its own — its fill column is a placeholder the cells occupy.
39
39
  */
40
40
  export interface ContentChars {
41
- left: string;
42
- vertical: string;
43
- right: string;
41
+ readonly left: string;
42
+ readonly vertical: string;
43
+ readonly right: string;
44
44
  }
45
45
  export type RowLevel = "head" | "row" | "foot" | "mid";
46
46
  export interface SubstituteOptions {
@@ -96,6 +96,22 @@ export declare function cutCells(text: string, positions: readonly CellCol[]): s
96
96
  * or less is no budget at all, so the text comes back as one line.
97
97
  */
98
98
  export declare function chopCells(text: string, maxWidth: CellCol): string[];
99
+ /**
100
+ * `text` cut into rows in one walk over its clusters, row `i` holding as many
101
+ * whole glyphs as fit in `capOf(i)` cells. A row's first glyph is taken even
102
+ * when it is wider than the budget, overflowing it by its own width, so every
103
+ * row is past the last and a loop over the rows ends by construction.
104
+ *
105
+ * [LAW:single-enforcer] `cellFit` leaves its caller to choose between
106
+ * force-taking and skipping; this is that choice, made once. Made twice, the
107
+ * copies drifted — the textarea's soft wrap force-took, `chopCells` did
108
+ * neither and hung. And the rows are cut in one walk because a row at a time,
109
+ * each segmenting the text again, is quadratic in a long word.
110
+ */
111
+ export declare function cellRows(text: string, capOf: (row: number) => CellCol): Generator<{
112
+ start: CodePoint;
113
+ end: CodePoint;
114
+ }>;
99
115
  /**
100
116
  * `line` split at each of `cuts`, in one walk over it. Converting each cut to a
101
117
  * code-unit offset on its own rescans the line from the start every time, which
@@ -110,6 +126,13 @@ export declare function splitAtCells(line: string, cuts: readonly CellCol[]): st
110
126
  * between two of its code points leaves half a glyph.
111
127
  */
112
128
  export declare function graphemes(text: string): string[];
129
+ /**
130
+ * The clusters of `text` from code unit `start` on, lazily, so a walk that
131
+ * stops early pays only for the clusters it reads.
132
+ */
133
+ export declare function clustersFrom(text: string, start: CodePoint): Generator<string>;
134
+ /** `clustersFrom` backwards: the clusters of `text` before code unit `end`, nearest first. */
135
+ export declare function clustersBefore(text: string, end: CodePoint): Generator<string>;
113
136
  /**
114
137
  * Returns the largest prefix of `text` whose cell width fits within `cap` cells,
115
138
  * cut between grapheme clusters. No padding — the returned string may be
@@ -132,19 +155,6 @@ export declare function cellFitEnd(text: string, cap: CellCol): string;
132
155
  * grapheme clusters, and every cluster boundary is a code-point boundary.
133
156
  */
134
157
  export declare function cellFitFrom(text: string, startCU: CodePoint, cap: CellCol): CodePoint;
135
- /**
136
- * Like `cellFitFrom`, but for any `startCU` inside `text` the result is
137
- * strictly past it: a glyph too wide for `cap` is force-taken whole and
138
- * overflows `cap` by its own width. So a `while (i < text.length)` loop driven
139
- * by this terminates by construction, and the re-brand is honest — both
140
- * operands land on code-point boundaries, so their max does too.
141
- *
142
- * [LAW:single-enforcer] `cellFit` documents that its caller must choose between
143
- * force-taking and skipping; this is that choice, made once. It was made twice
144
- * before and the copies drifted — the textarea's soft wrap force-took,
145
- * `chopCells` did neither and hung.
146
- */
147
- export declare function cellStepFrom(text: string, startCU: CodePoint, cap: CellCol): CodePoint;
148
158
  /**
149
159
  * The code-unit offset a visual column falls at: the end of the largest
150
160
  * prefix of `content` within `cellCol` cells, so a column inside a wide glyph
@@ -3,11 +3,16 @@
3
3
  */
4
4
  import { codePointWidth, widensToEmoji } from "./cell-widths.js";
5
5
  import { Memo } from "./memo.js";
6
+ import { pattern } from "./pattern.js";
6
7
  const cellLenMemo = new Memo();
7
8
  const ZWJ = 0x200d;
8
9
  const VS16 = 0xfe0f;
9
- const JOINERS = /[\u200d\ufe0f]/;
10
- const PRINTABLE_ASCII = /^[ -~]*$/;
10
+ const JOINERS = pattern(/[\u200d\ufe0f]/);
11
+ const PRINTABLE_ASCII = pattern(/^[ -~]*$/);
12
+ // Segmented afresh on every call: a cursor's step costs a few microseconds
13
+ // even in 10k code units, and a cache of segmentations would hold native
14
+ // memory for every string it kept.
15
+ const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
11
16
  // Rich's `cell_len` over a run of text: each code point the cells Rich's table
12
17
  // gives it, but that a ZWJ takes the code point after it into the glyph before
13
18
  // so neither adds a cell, and VS16 widens the last code point measured to two
@@ -48,7 +53,7 @@ function measure(text) {
48
53
  if (!JOINERS.test(text))
49
54
  return richCells(text);
50
55
  let cells = 0;
51
- for (const { segment } of graphemeSegmenter().segment(text))
56
+ for (const { segment } of SEGMENTER.segment(text))
52
57
  cells += richCells(segment);
53
58
  return cells;
54
59
  }
@@ -185,14 +190,38 @@ export function cutCells(text, positions) {
185
190
  export function chopCells(text, maxWidth) {
186
191
  if (maxWidth <= 0 || text.length === 0)
187
192
  return [text];
188
- const lines = [];
189
- let start = asCodePoint(0);
190
- while (start < text.length) {
191
- const end = cellStepFrom(text, start, maxWidth);
192
- lines.push(text.slice(start, end));
193
- start = end;
193
+ return Array.from(cellRows(text, () => maxWidth), ({ start, end }) => text.slice(start, end));
194
+ }
195
+ /**
196
+ * `text` cut into rows in one walk over its clusters, row `i` holding as many
197
+ * whole glyphs as fit in `capOf(i)` cells. A row's first glyph is taken even
198
+ * when it is wider than the budget, overflowing it by its own width, so every
199
+ * row is past the last and a loop over the rows ends by construction.
200
+ *
201
+ * [LAW:single-enforcer] `cellFit` leaves its caller to choose between
202
+ * force-taking and skipping; this is that choice, made once. Made twice, the
203
+ * copies drifted — the textarea's soft wrap force-took, `chopCells` did
204
+ * neither and hung. And the rows are cut in one walk because a row at a time,
205
+ * each segmenting the text again, is quadratic in a long word.
206
+ */
207
+ export function* cellRows(text, capOf) {
208
+ let row = 0;
209
+ let start = 0;
210
+ let at = 0;
211
+ let cells = 0;
212
+ for (const glyph of clustersFrom(text, asCodePoint(0))) {
213
+ const width = cellLen(glyph);
214
+ if (at > start && cells + width > capOf(row)) {
215
+ yield { start: asCodePoint(start), end: asCodePoint(at) };
216
+ row += 1;
217
+ start = at;
218
+ cells = 0;
219
+ }
220
+ cells += width;
221
+ at += glyph.length;
194
222
  }
195
- return lines;
223
+ if (at > start)
224
+ yield { start: asCodePoint(start), end: asCodePoint(at) };
196
225
  }
197
226
  /**
198
227
  * `line` split at each of `cuts`, in one walk over it. Converting each cut to a
@@ -214,21 +243,6 @@ export function splitAtCells(line, cuts) {
214
243
  }
215
244
  return [...pieces, line.slice(start)];
216
245
  }
217
- // [LAW:no-shared-mutable-globals] Private memos, written only by `segmentsOf`:
218
- // the segmenter, and the segmentation of the last string walked, so a caller
219
- // stepping along one string does not re-segment the whole of it every step.
220
- let segmenter;
221
- let lastSegmented;
222
- function graphemeSegmenter() {
223
- return (segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }));
224
- }
225
- /** The grapheme segmentation of `text`, reusing the last one when it is the same string. */
226
- function segmentsOf(text) {
227
- if (lastSegmented?.text !== text) {
228
- lastSegmented = { text, segments: graphemeSegmenter().segment(text) };
229
- }
230
- return lastSegmented.segments;
231
- }
232
246
  /**
233
247
  * The grapheme clusters of `text`, in order: the unit `cellLen` of a whole
234
248
  * string is the sum of. Summing `cellLen` over code points instead disagrees
@@ -237,20 +251,33 @@ function segmentsOf(text) {
237
251
  * between two of its code points leaves half a glyph.
238
252
  */
239
253
  export function graphemes(text) {
254
+ // Printable ASCII joins nothing, so each character is a cluster of its own
255
+ // and no segmentation is needed — the same reach `measure` makes.
256
+ if (PRINTABLE_ASCII.test(text))
257
+ return text.split("");
240
258
  return Array.from(clustersFrom(text, asCodePoint(0)));
241
259
  }
242
260
  /**
243
261
  * The clusters of `text` from code unit `start` on, lazily, so a walk that
244
262
  * stops early pays only for the clusters it reads.
245
263
  */
246
- function* clustersFrom(text, start) {
247
- const segments = segmentsOf(text);
264
+ export function* clustersFrom(text, start) {
265
+ const segments = SEGMENTER.segment(text);
248
266
  for (let at = start, s = segments.containing(at); s; s = segments.containing(at)) {
249
267
  const end = s.index + s.segment.length;
250
268
  yield text.slice(at, end);
251
269
  at = asCodePoint(end);
252
270
  }
253
271
  }
272
+ /** `clustersFrom` backwards: the clusters of `text` before code unit `end`, nearest first. */
273
+ export function* clustersBefore(text, end) {
274
+ const segments = SEGMENTER.segment(text);
275
+ for (let at = end; at > 0;) {
276
+ const { index } = segments.containing(at - 1);
277
+ yield text.slice(index, at);
278
+ at = asCodePoint(index);
279
+ }
280
+ }
254
281
  /**
255
282
  * Returns the largest prefix of `text` whose cell width fits within `cap` cells,
256
283
  * cut between grapheme clusters. No padding — the returned string may be
@@ -265,7 +292,7 @@ export function cellFit(text, cap) {
265
292
  }
266
293
  /** `cellFit` from the other end: the largest suffix of `text` within `cap` cells. */
267
294
  export function cellFitEnd(text, cap) {
268
- return text.slice(text.length - fitLength(graphemes(text).reverse(), cap));
295
+ return text.slice(text.length - fitLength(clustersBefore(text, asCodePoint(text.length)), cap));
269
296
  }
270
297
  /** The code units of the leading `clusters` that fit within `cap` cells. */
271
298
  function fitLength(clusters, cap) {
@@ -292,22 +319,6 @@ function fitLength(clusters, cap) {
292
319
  export function cellFitFrom(text, startCU, cap) {
293
320
  return asCodePoint(startCU + fitLength(clustersFrom(text, startCU), cap));
294
321
  }
295
- /**
296
- * Like `cellFitFrom`, but for any `startCU` inside `text` the result is
297
- * strictly past it: a glyph too wide for `cap` is force-taken whole and
298
- * overflows `cap` by its own width. So a `while (i < text.length)` loop driven
299
- * by this terminates by construction, and the re-brand is honest — both
300
- * operands land on code-point boundaries, so their max does too.
301
- *
302
- * [LAW:single-enforcer] `cellFit` documents that its caller must choose between
303
- * force-taking and skipping; this is that choice, made once. It was made twice
304
- * before and the copies drifted — the textarea's soft wrap force-took,
305
- * `chopCells` did neither and hung.
306
- */
307
- export function cellStepFrom(text, startCU, cap) {
308
- const [first = ""] = clustersFrom(text, startCU);
309
- return asCodePoint(Math.max(cellFitFrom(text, startCU, cap), startCU + first.length));
310
- }
311
322
  /**
312
323
  * The code-unit offset a visual column falls at: the end of the largest
313
324
  * prefix of `content` within `cellCol` cells, so a column inside a wide glyph
@@ -328,9 +339,8 @@ export function nextGrapheme(s, cu) {
328
339
  }
329
340
  /** `nextGrapheme` backwards: the start of the cluster ending at `cu`; 0 at the start. */
330
341
  export function prevGrapheme(s, cu) {
331
- if (cu <= 0)
332
- return asCodePoint(0);
333
- return asCodePoint(segmentsOf(s).containing(cu - 1).index);
342
+ const [cluster = ""] = clustersBefore(s, cu);
343
+ return asCodePoint(cu - cluster.length);
334
344
  }
335
345
  /**
336
346
  * Where an offset into `s` comes to rest as a cursor: itself when it is a
@@ -340,7 +350,7 @@ export function prevGrapheme(s, cu) {
340
350
  */
341
351
  export function graphemeBoundary(s, cu) {
342
352
  const at = Math.min(cu, s.length);
343
- const cluster = segmentsOf(s).containing(at);
353
+ const cluster = SEGMENTER.segment(s).containing(at);
344
354
  return asCodePoint(cluster === undefined || cluster.index === at ? at : cluster.index + cluster.segment.length);
345
355
  }
346
356
  /**
@@ -46,8 +46,8 @@ export interface Clock {
46
46
  * Call `tick` once, when this clock reads `time` — as soon as it can for a
47
47
  * time already past — unless the returned function is called first. What
48
48
  * a host that knows when its frame next changes (`Segment.nextChange`)
49
- * waits on. A `time` further off than a timer can wait, about 24.8 days,
50
- * throws, as `frameRate` refuses the rate.
49
+ * waits on. A `time` that is not a finite number of seconds throws; one
50
+ * further off than a platform timer can wait is waited for in turns.
51
51
  */
52
52
  at(time: number, tick: () => void): Unsubscribe;
53
53
  }
@@ -40,9 +40,8 @@ export function systemClock() {
40
40
  return () => clearInterval(timer);
41
41
  },
42
42
  at: (time, tick) => {
43
- if (!(time - now() <= LONGEST_INTERVAL)) {
44
- throw new RangeError(`a clock waits at most ${LONGEST_INTERVAL} s, not until ${time} s from ${now()} s`);
45
- }
43
+ if (!Number.isFinite(time))
44
+ throw new RangeError(`a clock waits until a finite number of seconds, not ${time}`);
46
45
  // [LAW:single-enforcer] A platform timer is due by a whole millisecond
47
46
  // of the event loop's own time, truncated, so it can run while this
48
47
  // clock still reads under `time` — by most of a millisecond. The
@@ -51,9 +50,12 @@ export function systemClock() {
51
50
  // again for what remains, so `tick` runs when `now()` has reached
52
51
  // `time` and not before. A host re-drawing at a frame's end would
53
52
  // otherwise draw the old frame again and wait again, every boundary.
53
+ // A wait longer than a timer holds is the same re-arming, one longest
54
+ // timer at a time, so a hold a slowed timeline stretches to months is
55
+ // waited out rather than refused.
54
56
  let timer;
55
57
  const arm = () => {
56
- timer = setTimeout(() => (now() < time ? arm() : tick()), Math.ceil(Math.max(0, time - now()) * 1000));
58
+ timer = setTimeout(() => (now() < time ? arm() : tick()), Math.min(LONGEST_INTERVAL * 1000, Math.ceil(Math.max(0, time - now()) * 1000)));
57
59
  };
58
60
  arm();
59
61
  return () => clearTimeout(timer);
@@ -67,14 +67,13 @@ export declare function rgbDistance(a: ColorRgba, b: ColorRgba): number;
67
67
  */
68
68
  export declare class ColorTable {
69
69
  private readonly colors;
70
+ /** Each entry's relative luminance. */
71
+ private readonly luminances;
70
72
  readonly firstIndex: number;
71
73
  private readonly matchMemo;
72
74
  private readonly readableMemo;
73
- constructor(colors: ColorRgba[], firstIndex?: number);
75
+ constructor(colors: readonly ColorRgba[], firstIndex?: number);
74
76
  get(index: number): ColorRgba;
75
- private luminanceCache;
76
- /** Each entry's relative luminance, computed once per table. */
77
- private luminances;
78
77
  /** How many entries the table holds (terminal indices `firstIndex`…). */
79
78
  get size(): number;
80
79
  /**
@@ -150,13 +149,31 @@ export declare function detectColorSystem(options?: DetectColorOptions): ColorDe
150
149
  export declare function isDumbTerminal(env: Env): boolean;
151
150
  /**
152
151
  * Where segments are written, as everything the serializer (`segmentsToString`)
153
- * needs to know about it: the colour depth SGR is drawn at, and whether OSC 8
154
- * hyperlinks are emitted. The serializer takes this value whole, so a fact the
155
- * encoding comes to need joins it here rather than the serializer's signature.
152
+ * needs to know about it: the colour depth SGR is drawn at, whether OSC 8
153
+ * hyperlinks are emitted, and whether the reader there asked for reduced
154
+ * motion — under which the terminal's own blink is not asked for
155
+ * (`shownOn`, `./render.ts`). The serializer takes this value whole, so a
156
+ * fact the encoding comes to need joins it here rather than the serializer's
157
+ * signature.
156
158
  */
157
159
  export interface Destination {
158
160
  readonly colorSystem: ColorDepth | null;
159
161
  readonly hyperlinks: boolean;
162
+ /**
163
+ * The reader asked for reduced motion. A host draws for it with the same
164
+ * value as `RenderOptions.reducedMotion`, read off this field, so what is
165
+ * drawn and how it is encoded take one preference.
166
+ */
167
+ readonly reducedMotion: boolean;
168
+ }
169
+ /**
170
+ * What `resolveDestination` is told about where it writes beyond the colour
171
+ * spec: what detection probes, and the reader's preference for motion, which
172
+ * no spec or terminal reports, so the program hands it in. Absent, everything
173
+ * moves.
174
+ */
175
+ export interface DestinationOptions extends DetectColorOptions {
176
+ reducedMotion?: boolean;
160
177
  }
161
178
  /**
162
179
  * Resolve a colour spec into a `Destination`.
@@ -168,7 +185,7 @@ export interface Destination {
168
185
  * colour preferences, leave them alone. A FORCE_COLOR that forces colour on
169
186
  * declares the destination takes escapes, so it keeps links on a pipe too.
170
187
  */
171
- export declare function resolveDestination(spec: ColorSystemName | ColorDepth | null, options?: DetectColorOptions): Destination;
188
+ export declare function resolveDestination(spec: ColorSystemName | ColorDepth | null, options?: DestinationOptions): Destination;
172
189
  /**
173
190
  * Resolve a colour system name into a `ColorDepth` (or `null` for no color).
174
191
  *
@@ -187,7 +204,7 @@ export declare class ColorSpec {
187
204
  readonly type: ColorDepth;
188
205
  readonly number: number | undefined;
189
206
  readonly value: ColorRgba | undefined;
190
- private downgradeCache;
207
+ private readonly downgrades;
191
208
  constructor(name: string, type: ColorDepth, number?: number, value?: ColorRgba);
192
209
  get isDefault(): boolean;
193
210
  /**
@@ -336,7 +353,7 @@ export declare function resolveTerminal(theme: TerminalTheme | undefined, depth:
336
353
  * writer rounds to those same indices.
337
354
  */
338
355
  export declare function downgradeTable(depth: ColorDepth.EIGHT_BIT | ColorDepth.STANDARD | ColorDepth.WINDOWS): ColorTable;
339
- export declare const ANSI_COLOR_NAMES: Record<string, number>;
356
+ export declare const ANSI_COLOR_NAMES: Readonly<Record<string, number>>;
340
357
  /**
341
358
  * Every word `ColorSpec.parse` accepts as a name rather than a `#hex`,
342
359
  * `rgb()` or `color(N)` form: `default` and the named palette entries, which
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * Terminal color representation, parsing, downgrading, and ANSI code generation.
3
3
  */
4
- import { Memo } from "./memo.js";
4
+ import { Memo, WeakMemo } from "./memo.js";
5
+ import { pattern } from "./pattern.js";
6
+ // Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
7
+ // only `import type`s from `core/color.ts`, so there is no runtime cycle.
8
+ import { Palette } from "../themes/palette.js";
5
9
  // --- ColorRgba ---
6
10
  function hex2(byte) {
7
11
  return byte.toString(16).padStart(2, "0");
@@ -129,23 +133,20 @@ export function rgbDistance(a, b) {
129
133
  */
130
134
  export class ColorTable {
131
135
  colors;
136
+ /** Each entry's relative luminance. */
137
+ luminances;
132
138
  firstIndex;
133
139
  // Keyed by colours a host computes without end (ramp stops, mixes), so bounded.
134
140
  matchMemo = new Memo();
135
141
  readableMemo = new Memo();
136
142
  constructor(colors, firstIndex = 0) {
137
143
  this.colors = colors;
144
+ this.luminances = colors.map(relativeLuminance);
138
145
  this.firstIndex = firstIndex;
139
146
  }
140
147
  get(index) {
141
148
  return this.colors[index - this.firstIndex];
142
149
  }
143
- luminanceCache;
144
- /** Each entry's relative luminance, computed once per table. */
145
- luminances() {
146
- this.luminanceCache ??= this.colors.map(relativeLuminance);
147
- return this.luminanceCache;
148
- }
149
150
  /** How many entries the table holds (terminal indices `firstIndex`…). */
150
151
  get size() {
151
152
  return this.colors.length;
@@ -192,7 +193,7 @@ export class ColorTable {
192
193
  let bestScore = -Infinity;
193
194
  for (let i = 0; i < this.colors.length; i++) {
194
195
  const c = this.colors[i];
195
- const lc = this.luminances()[i];
196
+ const lc = this.luminances[i];
196
197
  const ratio = luminanceRatio(lc, lOn);
197
198
  const passes = ratio >= minRatio;
198
199
  // A passing entry scores by closeness; a failing one only by contrast,
@@ -376,17 +377,20 @@ function terminalColorSystem(options) {
376
377
  * colour preferences, leave them alone. A FORCE_COLOR that forces colour on
377
378
  * declares the destination takes escapes, so it keeps links on a pipe too.
378
379
  */
379
- export function resolveDestination(spec, options) {
380
+ export function resolveDestination(spec, options = {}) {
381
+ const reducedMotion = options.reducedMotion ?? false;
380
382
  if (spec !== "auto") {
381
383
  return {
382
384
  colorSystem: typeof spec === "string" ? resolveColorSystem(spec) : spec,
383
385
  hyperlinks: true,
386
+ reducedMotion,
384
387
  };
385
388
  }
386
389
  return {
387
390
  colorSystem: detectColorSystem(options),
388
- hyperlinks: (forcedColorSystem(envOf(options ?? {})) ?? null) !== null ||
389
- terminalColorSystem(options ?? {}) !== null,
391
+ hyperlinks: (forcedColorSystem(envOf(options)) ?? null) !== null ||
392
+ terminalColorSystem(options) !== null,
393
+ reducedMotion,
390
394
  };
391
395
  }
392
396
  /**
@@ -419,7 +423,7 @@ export class ColorSpec {
419
423
  type;
420
424
  number;
421
425
  value;
422
- downgradeCache = new Map();
426
+ downgrades = new Memo();
423
427
  constructor(name, type, number, value) {
424
428
  this.name = name;
425
429
  this.type = type;
@@ -518,12 +522,7 @@ export class ColorSpec {
518
522
  return this;
519
523
  if (this.type === targetSystem || fidelity(this.type) < fidelity(targetSystem))
520
524
  return this;
521
- const cached = this.downgradeCache.get(targetSystem);
522
- if (cached)
523
- return cached;
524
- const result = this.performDowngrade(targetSystem);
525
- this.downgradeCache.set(targetSystem, result);
526
- return result;
525
+ return this.downgrades.get(`${targetSystem}`, () => this.performDowngrade(targetSystem));
527
526
  }
528
527
  /**
529
528
  * Resolves to an actual RGB(A) value for any color type.
@@ -636,11 +635,7 @@ export class ColorSpec {
636
635
  * `matchOn` rounds to, but for the default, read off the same two tables.
637
636
  */
638
637
  static indexed(depth, theme) {
639
- const byDepth = indexedMemo.get(theme) ?? new Map();
640
- indexedMemo.set(theme, byDepth);
641
- const listed = byDepth.get(depth) ?? listIndexed(depth, theme);
642
- byDepth.set(depth, listed);
643
- return listed;
638
+ return indexedMemo.get(theme, () => new Memo()).get(`${depth}`, () => listIndexed(depth, theme));
644
639
  }
645
640
  /**
646
641
  * The spec a slot is written in once its colour moves from `was`, what its
@@ -689,7 +684,7 @@ function cubeCandidate(value) {
689
684
  return [ColorSpec.fromAnsi(index), EIGHT_BIT_DOWNGRADE_TABLE.get(index)];
690
685
  }
691
686
  /** `ColorSpec.indexed`'s list, made once for each theme and depth. */
692
- const indexedMemo = new WeakMap();
687
+ const indexedMemo = new WeakMemo();
693
688
  function listIndexed(depth, theme) {
694
689
  const slots = () => Array.from({ length: 16 }, (_, slot) => [new ColorSpec(`color(${slot})`, depth === ColorDepth.WINDOWS ? depth : ColorDepth.STANDARD, slot), theme.ansiColors.get(slot)]);
695
690
  switch (depth) {
@@ -736,11 +731,11 @@ class ColorBlend extends ColorSpec {
736
731
  }
737
732
  }
738
733
  // --- Parsing internals ---
739
- const RGB_RE = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
740
- const COLOR_NUMBER_RE = /^color\((\d+)\)$/;
741
- const BLEND_RE = /^blend\((.*)\)$/;
734
+ const RGB_RE = pattern(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
735
+ const COLOR_NUMBER_RE = pattern(/^color\((\d+)\)$/);
736
+ const BLEND_RE = pattern(/^blend\((.*)\)$/);
742
737
  // A decimal as `String(number)` spells one, exponent included; `blend` owns its range.
743
- const FRACTION_RE = /^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?\s*$/;
738
+ const FRACTION_RE = pattern(/^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?\s*$/);
744
739
  function parseSingle(key) {
745
740
  if (key === "default" || key === "") {
746
741
  return ColorSpec.default();
@@ -817,7 +812,7 @@ function parseByte(key, digits, field) {
817
812
  // reference, a template argument — becomes one through the three parsers below,
818
813
  // and each is this check at the lengths it admits. Each digit pair is a byte, so
819
814
  // the grammar holding is what keeps `ColorRgba`'s channels in range.
820
- const HEX_DIGITS_RE = /^[0-9a-fA-F]+$/;
815
+ const HEX_DIGITS_RE = pattern(/^[0-9a-fA-F]+$/);
821
816
  // Each admitted digit count, by the grammar it spells: the message a failure
822
817
  // carries is read off the lengths a parser admits, so the two cannot disagree.
823
818
  const HEX_GRAMMARS = { 6: "RRGGBB", 8: "RRGGBBAA" };
@@ -959,10 +954,6 @@ export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
959
954
  // terminal a colour's depth assumes, built from that depth's own sixteen with
960
955
  // an empty Palette — sufficient for the lookups it serves, which never read
961
956
  // `palette`.
962
- //
963
- // Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
964
- // only `import type`s from `core/color.ts`, so there is no runtime cycle.
965
- import { Palette } from "../themes/palette.js";
966
957
  const INTERNAL_DEFAULT_THEME = new TerminalTheme(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE, new Palette("default", true, new Map()));
967
958
  /** The Windows console in its default Campbell scheme: a WINDOWS colour's terminal. */
968
959
  const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE, new Palette("windows", true, new Map()));
@@ -59,6 +59,15 @@ export interface ConsoleOptions {
59
59
  * renderable this console draws. Default false.
60
60
  */
61
61
  asciiOnly?: boolean;
62
+ /**
63
+ * The reader asked for reduced motion: every animation this console draws
64
+ * — through `print`, a `Live` or an `App` — is drawn with its motion taken
65
+ * out, waking its host only where content changes
66
+ * (`RenderOptions.reducedMotion`). A preference of the
67
+ * person at the terminal, which no terminal reports, so the program hands
68
+ * it in; nothing here reads the environment for it. Default false.
69
+ */
70
+ reducedMotion?: boolean;
62
71
  /**
63
72
  * Static width (cells). Ignored when `getSize` is provided. Falls back to
64
73
  * the `COLUMNS` env var / the bound stream's `columns` / 80.
@@ -127,24 +136,25 @@ export interface PrintOptions {
127
136
  }
128
137
  export declare class Console {
129
138
  private readonly _destination;
130
- private _getSize;
139
+ private readonly _getSize;
131
140
  private readonly _getDatetime;
132
- private _lastLogTime;
133
- private _style;
134
- private _isTerminal;
141
+ private readonly _lastLogTime;
142
+ private readonly _style;
143
+ private readonly _isTerminal;
135
144
  private readonly _interactive;
136
- private _file;
137
- private _stream;
138
- private _record;
139
- private _markup;
140
- private _highlight;
141
- private _theme;
142
- private _onStyleError;
145
+ private readonly _file;
146
+ private readonly _stream;
147
+ private readonly _record;
148
+ private readonly _markup;
149
+ private readonly _highlight;
150
+ private readonly _theme;
151
+ private readonly _onStyleError;
143
152
  private readonly _asciiOnly;
144
- private _highlighter;
145
- private _recorded;
146
- private _capture;
147
- private _liveRegion;
153
+ private readonly _highlighter;
154
+ private readonly _recorded;
155
+ private readonly _capture;
156
+ private readonly _liveRegion;
157
+ private readonly _madeIn;
148
158
  constructor(options?: ConsoleOptions);
149
159
  get size(): {
150
160
  width: number;
@@ -156,8 +166,9 @@ export declare class Console {
156
166
  get isInteractive(): boolean;
157
167
  get colorSystem(): ColorDepth | null;
158
168
  /**
159
- * Where this console writes: the colour depth it draws at and whether it
160
- * emits OSC 8 hyperlinks, overrides applied — the value its output is
169
+ * Where this console writes: the colour depth it draws at, whether it
170
+ * emits OSC 8 hyperlinks, and whether its reader asked for reduced motion,
171
+ * overrides applied — the value its output is
161
172
  * encoded for, for anything that encodes on its behalf (`Live`).
162
173
  */
163
174
  get destination(): Destination;