@promptctl/rich-js 0.24.1 → 0.25.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 (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 +21 -29
  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 +13 -16
  47. package/dist/core/strip.js +119 -110
  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 +132 -54
  73. package/dist/renderables/effect.js +188 -115
  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,7 @@
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
5
  // --- ColorRgba ---
6
6
  function hex2(byte) {
7
7
  return byte.toString(16).padStart(2, "0");
@@ -129,23 +129,20 @@ export function rgbDistance(a, b) {
129
129
  */
130
130
  export class ColorTable {
131
131
  colors;
132
+ /** Each entry's relative luminance. */
133
+ luminances;
132
134
  firstIndex;
133
135
  // Keyed by colours a host computes without end (ramp stops, mixes), so bounded.
134
136
  matchMemo = new Memo();
135
137
  readableMemo = new Memo();
136
138
  constructor(colors, firstIndex = 0) {
137
139
  this.colors = colors;
140
+ this.luminances = colors.map(relativeLuminance);
138
141
  this.firstIndex = firstIndex;
139
142
  }
140
143
  get(index) {
141
144
  return this.colors[index - this.firstIndex];
142
145
  }
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
146
  /** How many entries the table holds (terminal indices `firstIndex`…). */
150
147
  get size() {
151
148
  return this.colors.length;
@@ -192,7 +189,7 @@ export class ColorTable {
192
189
  let bestScore = -Infinity;
193
190
  for (let i = 0; i < this.colors.length; i++) {
194
191
  const c = this.colors[i];
195
- const lc = this.luminances()[i];
192
+ const lc = this.luminances[i];
196
193
  const ratio = luminanceRatio(lc, lOn);
197
194
  const passes = ratio >= minRatio;
198
195
  // A passing entry scores by closeness; a failing one only by contrast,
@@ -376,17 +373,20 @@ function terminalColorSystem(options) {
376
373
  * colour preferences, leave them alone. A FORCE_COLOR that forces colour on
377
374
  * declares the destination takes escapes, so it keeps links on a pipe too.
378
375
  */
379
- export function resolveDestination(spec, options) {
376
+ export function resolveDestination(spec, options = {}) {
377
+ const reducedMotion = options.reducedMotion ?? false;
380
378
  if (spec !== "auto") {
381
379
  return {
382
380
  colorSystem: typeof spec === "string" ? resolveColorSystem(spec) : spec,
383
381
  hyperlinks: true,
382
+ reducedMotion,
384
383
  };
385
384
  }
386
385
  return {
387
386
  colorSystem: detectColorSystem(options),
388
- hyperlinks: (forcedColorSystem(envOf(options ?? {})) ?? null) !== null ||
389
- terminalColorSystem(options ?? {}) !== null,
387
+ hyperlinks: (forcedColorSystem(envOf(options)) ?? null) !== null ||
388
+ terminalColorSystem(options) !== null,
389
+ reducedMotion,
390
390
  };
391
391
  }
392
392
  /**
@@ -419,7 +419,7 @@ export class ColorSpec {
419
419
  type;
420
420
  number;
421
421
  value;
422
- downgradeCache = new Map();
422
+ downgrades = new Memo();
423
423
  constructor(name, type, number, value) {
424
424
  this.name = name;
425
425
  this.type = type;
@@ -518,12 +518,7 @@ export class ColorSpec {
518
518
  return this;
519
519
  if (this.type === targetSystem || fidelity(this.type) < fidelity(targetSystem))
520
520
  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;
521
+ return this.downgrades.get(`${targetSystem}`, () => this.performDowngrade(targetSystem));
527
522
  }
528
523
  /**
529
524
  * Resolves to an actual RGB(A) value for any color type.
@@ -636,11 +631,7 @@ export class ColorSpec {
636
631
  * `matchOn` rounds to, but for the default, read off the same two tables.
637
632
  */
638
633
  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;
634
+ return indexedMemo.get(theme, () => new Memo()).get(`${depth}`, () => listIndexed(depth, theme));
644
635
  }
645
636
  /**
646
637
  * The spec a slot is written in once its colour moves from `was`, what its
@@ -689,7 +680,7 @@ function cubeCandidate(value) {
689
680
  return [ColorSpec.fromAnsi(index), EIGHT_BIT_DOWNGRADE_TABLE.get(index)];
690
681
  }
691
682
  /** `ColorSpec.indexed`'s list, made once for each theme and depth. */
692
- const indexedMemo = new WeakMap();
683
+ const indexedMemo = new WeakMemo();
693
684
  function listIndexed(depth, theme) {
694
685
  const slots = () => Array.from({ length: 16 }, (_, slot) => [new ColorSpec(`color(${slot})`, depth === ColorDepth.WINDOWS ? depth : ColorDepth.STANDARD, slot), theme.ansiColors.get(slot)]);
695
686
  switch (depth) {
@@ -736,11 +727,11 @@ class ColorBlend extends ColorSpec {
736
727
  }
737
728
  }
738
729
  // --- 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\((.*)\)$/;
730
+ const RGB_RE = pattern(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
731
+ const COLOR_NUMBER_RE = pattern(/^color\((\d+)\)$/);
732
+ const BLEND_RE = pattern(/^blend\((.*)\)$/);
742
733
  // A decimal as `String(number)` spells one, exponent included; `blend` owns its range.
743
- const FRACTION_RE = /^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?\s*$/;
734
+ const FRACTION_RE = pattern(/^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?\s*$/);
744
735
  function parseSingle(key) {
745
736
  if (key === "default" || key === "") {
746
737
  return ColorSpec.default();
@@ -817,7 +808,7 @@ function parseByte(key, digits, field) {
817
808
  // reference, a template argument — becomes one through the three parsers below,
818
809
  // and each is this check at the lengths it admits. Each digit pair is a byte, so
819
810
  // the grammar holding is what keeps `ColorRgba`'s channels in range.
820
- const HEX_DIGITS_RE = /^[0-9a-fA-F]+$/;
811
+ const HEX_DIGITS_RE = pattern(/^[0-9a-fA-F]+$/);
821
812
  // Each admitted digit count, by the grammar it spells: the message a failure
822
813
  // carries is read off the lengths a parser admits, so the two cannot disagree.
823
814
  const HEX_GRAMMARS = { 6: "RRGGBB", 8: "RRGGBBAA" };
@@ -963,6 +954,7 @@ export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
963
954
  // Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
964
955
  // only `import type`s from `core/color.ts`, so there is no runtime cycle.
965
956
  import { Palette } from "../themes/palette.js";
957
+ import { pattern } from "./pattern.js";
966
958
  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
959
  /** The Windows console in its default Campbell scheme: a WINDOWS colour's terminal. */
968
960
  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;