@promptctl/rich-js 0.24.0 → 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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  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 +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Terminal cell width calculation.
3
- * Handles ASCII, CJK (double-width), and emoji characters.
2
+ * Terminal cell width calculation: Rich's `cell_len`, ported.
4
3
  */
5
4
  /**
6
5
  * Returns the terminal cell width of a string.
@@ -72,13 +71,20 @@ export declare function setCellSize(text: string, totalWidth: CellCol): string;
72
71
  */
73
72
  export declare function splitText(text: string, position: CellCol): [string, string];
74
73
  /**
75
- * Cuts `text` into exactly `position` cells and the rest, as the reference's
76
- * `Segment.split_cells` does: a wide glyph the cut goes through fits neither
77
- * side, so it leaves a space in each of its cells, where `splitText` keeps it
78
- * whole on the right. Every other character lands on exactly one side.
79
- * `position` must fall inside `text`, between 0 and its width.
74
+ * Cuts `text` at each of `positions`, as the reference's `Segment.split_cells`
75
+ * cuts at one: a wide glyph a cut goes through fits neither side, so it leaves
76
+ * a space in each of its cells, where `splitText` keeps it whole on the right.
77
+ * Every other character lands in exactly one piece, so each piece is exactly
78
+ * as wide as the distance between its cuts. `positions` are non-decreasing and
79
+ * fall inside `text`: from 0 to one short of its width.
80
+ *
81
+ * One walk over `text` however many positions there are, ending at the last
82
+ * of them: cutting what is left again at every position re-measures and copies
83
+ * the rest each time, which is quadratic in a long enough string, and walking
84
+ * past the last position makes one cut near the start of a long string pay for
85
+ * all of it.
80
86
  */
81
- export declare function cutCells(text: string, position: CellCol): [string, string];
87
+ export declare function cutCells(text: string, positions: readonly CellCol[]): string[];
82
88
  /**
83
89
  * Wraps text into lines of at most `maxWidth` cells, preserving every code
84
90
  * point: `chopCells(t, w).join("") === t`. Unlike `splitText` this never pads,
@@ -90,6 +96,22 @@ export declare function cutCells(text: string, position: CellCol): [string, stri
90
96
  * or less is no budget at all, so the text comes back as one line.
91
97
  */
92
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
+ }>;
93
115
  /**
94
116
  * `line` split at each of `cuts`, in one walk over it. Converting each cut to a
95
117
  * code-unit offset on its own rescans the line from the start every time, which
@@ -97,13 +119,20 @@ export declare function chopCells(text: string, maxWidth: CellCol): string[];
97
119
  */
98
120
  export declare function splitAtCells(line: string, cuts: readonly CellCol[]): string[];
99
121
  /**
100
- * The grapheme clusters of `text`, in order: the unit `string-width` measures,
101
- * and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
102
- * over code points instead disagrees with it on every glyph built from several:
122
+ * The grapheme clusters of `text`, in order: the unit `cellLen` of a whole
123
+ * string is the sum of. Summing `cellLen` over code points instead disagrees
124
+ * with it on every glyph built from several:
103
125
  * `❤️` is 1 + 0 by code point and 2 as a cluster, `👨‍👩‍👧` is 6 and 2, and a cut
104
126
  * between two of its code points leaves half a glyph.
105
127
  */
106
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>;
107
136
  /**
108
137
  * Returns the largest prefix of `text` whose cell width fits within `cap` cells,
109
138
  * cut between grapheme clusters. No padding — the returned string may be
@@ -126,19 +155,6 @@ export declare function cellFitEnd(text: string, cap: CellCol): string;
126
155
  * grapheme clusters, and every cluster boundary is a code-point boundary.
127
156
  */
128
157
  export declare function cellFitFrom(text: string, startCU: CodePoint, cap: CellCol): CodePoint;
129
- /**
130
- * Like `cellFitFrom`, but for any `startCU` inside `text` the result is
131
- * strictly past it: a glyph too wide for `cap` is force-taken whole and
132
- * overflows `cap` by its own width. So a `while (i < text.length)` loop driven
133
- * by this terminates by construction, and the re-brand is honest — both
134
- * operands land on code-point boundaries, so their max does too.
135
- *
136
- * [LAW:single-enforcer] `cellFit` documents that its caller must choose between
137
- * force-taking and skipping; this is that choice, made once. It was made twice
138
- * before and the copies drifted — the textarea's soft wrap force-took,
139
- * `chopCells` did neither and hung.
140
- */
141
- export declare function cellStepFrom(text: string, startCU: CodePoint, cap: CellCol): CodePoint;
142
158
  /**
143
159
  * The code-unit offset a visual column falls at: the end of the largest
144
160
  * prefix of `content` within `cellCol` cells, so a column inside a wide glyph
@@ -1,11 +1,62 @@
1
1
  /**
2
- * Terminal cell width calculation.
3
- * Handles ASCII, CJK (double-width), and emoji characters.
2
+ * Terminal cell width calculation: Rich's `cell_len`, ported.
4
3
  */
5
- import stringWidth from "string-width";
4
+ import { codePointWidth, widensToEmoji } from "./cell-widths.js";
6
5
  import { Memo } from "./memo.js";
7
- // [LAW:one-source-of-truth] string-width is the single authority for cell width
6
+ import { pattern } from "./pattern.js";
8
7
  const cellLenMemo = new Memo();
8
+ const ZWJ = 0x200d;
9
+ const VS16 = 0xfe0f;
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" });
16
+ // Rich's `cell_len` over a run of text: each code point the cells Rich's table
17
+ // gives it, but that a ZWJ takes the code point after it into the glyph before
18
+ // so neither adds a cell, and VS16 widens the last code point measured to two
19
+ // cells when the table lists it as one that emoji presentation widens.
20
+ function richCells(text) {
21
+ let cells = 0;
22
+ let widenable = false;
23
+ let joined = false;
24
+ for (const ch of text) {
25
+ const codePoint = ch.codePointAt(0);
26
+ if (joined) {
27
+ joined = false;
28
+ }
29
+ else if (codePoint === ZWJ) {
30
+ joined = true;
31
+ }
32
+ else if (codePoint === VS16) {
33
+ cells += widenable ? 1 : 0;
34
+ widenable = false;
35
+ }
36
+ else {
37
+ const width = codePointWidth(codePoint);
38
+ cells += width;
39
+ widenable = width > 0 ? widensToEmoji(codePoint) : widenable;
40
+ }
41
+ }
42
+ return cells;
43
+ }
44
+ // [LAW:one-source-of-truth] the single authority for cell width. A string is
45
+ // the sum of its grapheme clusters, each measured as Rich measures it, so a cut
46
+ // between clusters splits the width exactly; the two part only where a ZWJ or
47
+ // VS16 reaches across a cluster boundary, as in `a` ZWJ `b`, which Rich counts
48
+ // as one cell and two clusters count as two. Without either, every code point
49
+ // counts alone and no segmentation is needed.
50
+ function measure(text) {
51
+ if (PRINTABLE_ASCII.test(text))
52
+ return text.length;
53
+ if (!JOINERS.test(text))
54
+ return richCells(text);
55
+ let cells = 0;
56
+ for (const { segment } of SEGMENTER.segment(text))
57
+ cells += richCells(segment);
58
+ return cells;
59
+ }
9
60
  /**
10
61
  * Returns the terminal cell width of a string.
11
62
  */
@@ -13,7 +64,7 @@ export function cellLen(text) {
13
64
  if (text.length === 0)
14
65
  return 0;
15
66
  // Short strings recur (words, cells, labels); a long one is rarely measured twice.
16
- return text.length <= 64 ? cellLenMemo.get(text, stringWidth) : stringWidth(text);
67
+ return text.length <= 64 ? cellLenMemo.get(text, measure) : measure(text);
17
68
  }
18
69
  /** Brand a raw number as a CellCol. Use only at trust boundaries. */
19
70
  export function asCellCol(n) { return n; }
@@ -82,22 +133,49 @@ export function splitText(text, position) {
82
133
  return [left + " ".repeat(position - cellLen(left)), text.slice(left.length)];
83
134
  }
84
135
  /**
85
- * Cuts `text` into exactly `position` cells and the rest, as the reference's
86
- * `Segment.split_cells` does: a wide glyph the cut goes through fits neither
87
- * side, so it leaves a space in each of its cells, where `splitText` keeps it
88
- * whole on the right. Every other character lands on exactly one side.
89
- * `position` must fall inside `text`, between 0 and its width.
136
+ * Cuts `text` at each of `positions`, as the reference's `Segment.split_cells`
137
+ * cuts at one: a wide glyph a cut goes through fits neither side, so it leaves
138
+ * a space in each of its cells, where `splitText` keeps it whole on the right.
139
+ * Every other character lands in exactly one piece, so each piece is exactly
140
+ * as wide as the distance between its cuts. `positions` are non-decreasing and
141
+ * fall inside `text`: from 0 to one short of its width.
142
+ *
143
+ * One walk over `text` however many positions there are, ending at the last
144
+ * of them: cutting what is left again at every position re-measures and copies
145
+ * the rest each time, which is quadratic in a long enough string, and walking
146
+ * past the last position makes one cut near the start of a long string pay for
147
+ * all of it.
90
148
  */
91
- export function cutCells(text, position) {
92
- const head = cellFit(text, position);
93
- // Cells of the glyph the cut goes through that lie left of it; 0 when the
94
- // cut falls between glyphs, and then there is no such glyph.
95
- const straddle = position - cellLen(head);
96
- const [glyph = ""] = straddle > 0 ? clustersFrom(text, asCodePoint(head.length)) : [];
97
- return [
98
- head + " ".repeat(straddle),
99
- " ".repeat(cellLen(glyph) - straddle) + text.slice(head.length + glyph.length),
100
- ];
149
+ export function cutCells(text, positions) {
150
+ const pieces = [];
151
+ // The current piece is `lead` blank cells, then `text` from `start` to `at`.
152
+ let start = 0;
153
+ let lead = 0;
154
+ let at = 0;
155
+ let col = 0;
156
+ let next = 0;
157
+ for (const glyph of clustersFrom(text, asCodePoint(0))) {
158
+ if (next === positions.length)
159
+ break;
160
+ const end = col + cellLen(glyph);
161
+ // The first cell of this glyph no piece has taken yet.
162
+ let from = col;
163
+ for (; next < positions.length && positions[next] < end; next++) {
164
+ const cut = positions[next];
165
+ pieces.push(" ".repeat(lead) + text.slice(start, at) + " ".repeat(cut - from));
166
+ lead = 0;
167
+ // A cut between glyphs leaves this one to the next piece; a cut through
168
+ // it blanks it, and the pieces after the cut start past it.
169
+ start = cut === col ? at : at + glyph.length;
170
+ from = cut;
171
+ }
172
+ if (from > col)
173
+ lead = end - from;
174
+ at += glyph.length;
175
+ col = end;
176
+ }
177
+ pieces.push(" ".repeat(lead) + text.slice(start));
178
+ return pieces;
101
179
  }
102
180
  /**
103
181
  * Wraps text into lines of at most `maxWidth` cells, preserving every code
@@ -112,14 +190,38 @@ export function cutCells(text, position) {
112
190
  export function chopCells(text, maxWidth) {
113
191
  if (maxWidth <= 0 || text.length === 0)
114
192
  return [text];
115
- const lines = [];
116
- let start = asCodePoint(0);
117
- while (start < text.length) {
118
- const end = cellStepFrom(text, start, maxWidth);
119
- lines.push(text.slice(start, end));
120
- 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;
121
222
  }
122
- return lines;
223
+ if (at > start)
224
+ yield { start: asCodePoint(start), end: asCodePoint(at) };
123
225
  }
124
226
  /**
125
227
  * `line` split at each of `cuts`, in one walk over it. Converting each cut to a
@@ -141,41 +243,41 @@ export function splitAtCells(line, cuts) {
141
243
  }
142
244
  return [...pieces, line.slice(start)];
143
245
  }
144
- // [LAW:no-shared-mutable-globals] Private memos, written only by `segmentsOf`:
145
- // the segmenter, and the segmentation of the last string walked, so a caller
146
- // stepping along one string does not re-segment the whole of it every step.
147
- let segmenter;
148
- let lastSegmented;
149
- /** The grapheme segmentation of `text`, reusing the last one when it is the same string. */
150
- function segmentsOf(text) {
151
- if (lastSegmented?.text !== text) {
152
- segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
153
- lastSegmented = { text, segments: segmenter.segment(text) };
154
- }
155
- return lastSegmented.segments;
156
- }
157
246
  /**
158
- * The grapheme clusters of `text`, in order: the unit `string-width` measures,
159
- * and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
160
- * over code points instead disagrees with it on every glyph built from several:
247
+ * The grapheme clusters of `text`, in order: the unit `cellLen` of a whole
248
+ * string is the sum of. Summing `cellLen` over code points instead disagrees
249
+ * with it on every glyph built from several:
161
250
  * `❤️` is 1 + 0 by code point and 2 as a cluster, `👨‍👩‍👧` is 6 and 2, and a cut
162
251
  * between two of its code points leaves half a glyph.
163
252
  */
164
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("");
165
258
  return Array.from(clustersFrom(text, asCodePoint(0)));
166
259
  }
167
260
  /**
168
261
  * The clusters of `text` from code unit `start` on, lazily, so a walk that
169
262
  * stops early pays only for the clusters it reads.
170
263
  */
171
- function* clustersFrom(text, start) {
172
- const segments = segmentsOf(text);
264
+ export function* clustersFrom(text, start) {
265
+ const segments = SEGMENTER.segment(text);
173
266
  for (let at = start, s = segments.containing(at); s; s = segments.containing(at)) {
174
267
  const end = s.index + s.segment.length;
175
268
  yield text.slice(at, end);
176
269
  at = asCodePoint(end);
177
270
  }
178
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
+ }
179
281
  /**
180
282
  * Returns the largest prefix of `text` whose cell width fits within `cap` cells,
181
283
  * cut between grapheme clusters. No padding — the returned string may be
@@ -190,7 +292,7 @@ export function cellFit(text, cap) {
190
292
  }
191
293
  /** `cellFit` from the other end: the largest suffix of `text` within `cap` cells. */
192
294
  export function cellFitEnd(text, cap) {
193
- return text.slice(text.length - fitLength(graphemes(text).reverse(), cap));
295
+ return text.slice(text.length - fitLength(clustersBefore(text, asCodePoint(text.length)), cap));
194
296
  }
195
297
  /** The code units of the leading `clusters` that fit within `cap` cells. */
196
298
  function fitLength(clusters, cap) {
@@ -217,22 +319,6 @@ function fitLength(clusters, cap) {
217
319
  export function cellFitFrom(text, startCU, cap) {
218
320
  return asCodePoint(startCU + fitLength(clustersFrom(text, startCU), cap));
219
321
  }
220
- /**
221
- * Like `cellFitFrom`, but for any `startCU` inside `text` the result is
222
- * strictly past it: a glyph too wide for `cap` is force-taken whole and
223
- * overflows `cap` by its own width. So a `while (i < text.length)` loop driven
224
- * by this terminates by construction, and the re-brand is honest — both
225
- * operands land on code-point boundaries, so their max does too.
226
- *
227
- * [LAW:single-enforcer] `cellFit` documents that its caller must choose between
228
- * force-taking and skipping; this is that choice, made once. It was made twice
229
- * before and the copies drifted — the textarea's soft wrap force-took,
230
- * `chopCells` did neither and hung.
231
- */
232
- export function cellStepFrom(text, startCU, cap) {
233
- const [first = ""] = clustersFrom(text, startCU);
234
- return asCodePoint(Math.max(cellFitFrom(text, startCU, cap), startCU + first.length));
235
- }
236
322
  /**
237
323
  * The code-unit offset a visual column falls at: the end of the largest
238
324
  * prefix of `content` within `cellCol` cells, so a column inside a wide glyph
@@ -253,9 +339,8 @@ export function nextGrapheme(s, cu) {
253
339
  }
254
340
  /** `nextGrapheme` backwards: the start of the cluster ending at `cu`; 0 at the start. */
255
341
  export function prevGrapheme(s, cu) {
256
- if (cu <= 0)
257
- return asCodePoint(0);
258
- return asCodePoint(segmentsOf(s).containing(cu - 1).index);
342
+ const [cluster = ""] = clustersBefore(s, cu);
343
+ return asCodePoint(cu - cluster.length);
259
344
  }
260
345
  /**
261
346
  * Where an offset into `s` comes to rest as a cursor: itself when it is a
@@ -265,7 +350,7 @@ export function prevGrapheme(s, cu) {
265
350
  */
266
351
  export function graphemeBoundary(s, cu) {
267
352
  const at = Math.min(cu, s.length);
268
- const cluster = segmentsOf(s).containing(at);
353
+ const cluster = SEGMENTER.segment(s).containing(at);
269
354
  return asCodePoint(cluster === undefined || cluster.index === at ? at : cluster.index + cluster.segment.length);
270
355
  }
271
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()));