@promptctl/rich-js 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Terminal cell width calculation.
3
+ * Handles ASCII, CJK (double-width), and emoji characters.
4
+ */
5
+ import stringWidth from "string-width";
6
+ import { Memo } from "./memo.js";
7
+ // [LAW:one-source-of-truth] string-width is the single authority for cell width
8
+ const cellLenMemo = new Memo();
9
+ /**
10
+ * Returns the terminal cell width of a string.
11
+ */
12
+ export function cellLen(text) {
13
+ if (text.length === 0)
14
+ return 0;
15
+ // Short strings recur (words, cells, labels); a long one is rarely measured twice.
16
+ return text.length <= 64 ? cellLenMemo.get(text, stringWidth) : stringWidth(text);
17
+ }
18
+ /** Brand a raw number as a CellCol. Use only at trust boundaries. */
19
+ export function asCellCol(n) { return n; }
20
+ /** Brand a raw number as a CodeUnit. Use only at trust boundaries. */
21
+ export function asCodeUnit(n) { return n; }
22
+ /** Brand a raw number as a CodePoint. Use only when the value is known to be
23
+ * on a Unicode code-point boundary. */
24
+ export function asCodePoint(n) { return n; }
25
+ /**
26
+ * [LAW:parse-dont-validate] A caller's number as a count of cells — the checked
27
+ * counterpart to `asCellCol`'s unchecked brand, and the one place the rule "a
28
+ * cell count is a non-negative integer" is written.
29
+ *
30
+ * [LAW:one-source-of-truth] Every renderable is handed `RenderOptions.maxWidth`
31
+ * as a plain `number`, so every renderable used to answer this for itself and
32
+ * they disagreed: at NaN, `Table` collapsed to one cell of garbage, `Columns`
33
+ * and `Layout` threw `Invalid array length`, and `RichText` ignored the request
34
+ * and emitted its full natural width. Parse at the entry to `render`/`measure`
35
+ * and nothing downstream re-asks.
36
+ *
37
+ * The comparison is the point, not `Math.max(0, n)`: NaN fails every comparison
38
+ * and so floors here, where `Math.max` would return it and let it poison every
39
+ * bounds check downstream. Integral because a cell is not divisible — a
40
+ * fractional budget reaches a largest-remainder division, which grants a whole
41
+ * cell against a fractional residue and hands out more width than it was given.
42
+ *
43
+ * `Infinity` is outside what the comparison floors, so it passes through
44
+ * unchanged, and no clamp belongs here: the finite answer to an unbounded offer
45
+ * is the renderable's own natural width, which this function cannot see from
46
+ * the number alone. `withBoundedWidth` in `protocol.ts` is the second half of
47
+ * the parse and the only place that value is resolved.
48
+ */
49
+ export function cellCount(n) {
50
+ return (n > 0 ? Math.floor(n) : 0);
51
+ }
52
+ // ── Cell-aware string utilities ──────────────────────────────────────────────
53
+ /**
54
+ * Pads or crops a string to exactly `totalWidth` terminal cells.
55
+ * Invariant: cellLen(setCellSize(text, n)) === n (unless n is 0)
56
+ */
57
+ export function setCellSize(text, totalWidth) {
58
+ if (totalWidth === 0)
59
+ return "";
60
+ const currentWidth = cellLen(text);
61
+ if (currentWidth === totalWidth)
62
+ return text;
63
+ if (currentWidth < totalWidth) {
64
+ return text + " ".repeat(totalWidth - currentWidth);
65
+ }
66
+ return splitText(text, totalWidth)[0];
67
+ }
68
+ /**
69
+ * Splits text at a cell position. Returns [left, right].
70
+ * When the position falls mid-wide-character, the left side is padded
71
+ * to reach exactly `position` cells. The wide char remains in the right side.
72
+ *
73
+ * [LAW:one-source-of-truth] The left side is `cellFit`'s, so the cut falls
74
+ * between the grapheme clusters `cellLen` measures and never inside one.
75
+ */
76
+ export function splitText(text, position) {
77
+ if (position <= 0)
78
+ return ["", text];
79
+ if (position >= cellLen(text))
80
+ return [text, ""];
81
+ const left = cellFit(text, position);
82
+ return [left + " ".repeat(position - cellLen(left)), text.slice(left.length)];
83
+ }
84
+ /**
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.
90
+ */
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
+ ];
101
+ }
102
+ /**
103
+ * Wraps text into lines of at most `maxWidth` cells, preserving every code
104
+ * point: `chopCells(t, w).join("") === t`. Unlike `splitText` this never pads,
105
+ * so a line ending before a wide glyph is narrower than `maxWidth` rather than
106
+ * padded out to it.
107
+ *
108
+ * A line overflows exactly where the budget cannot be met: a glyph wider than
109
+ * `maxWidth` is force-taken whole rather than dropped, and a `maxWidth` of zero
110
+ * or less is no budget at all, so the text comes back as one line.
111
+ */
112
+ export function chopCells(text, maxWidth) {
113
+ if (maxWidth <= 0 || text.length === 0)
114
+ 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;
121
+ }
122
+ return lines;
123
+ }
124
+ /**
125
+ * `line` split at each of `cuts`, in one walk over it. Converting each cut to a
126
+ * code-unit offset on its own rescans the line from the start every time, which
127
+ * is quadratic in a long enough string.
128
+ */
129
+ export function splitAtCells(line, cuts) {
130
+ const pieces = [];
131
+ let start = 0;
132
+ let offset = 0;
133
+ let cells = 0;
134
+ for (const ch of graphemes(line)) {
135
+ if (pieces.length < cuts.length && cells >= cuts[pieces.length]) {
136
+ pieces.push(line.slice(start, offset));
137
+ start = offset;
138
+ }
139
+ cells += cellLen(ch);
140
+ offset += ch.length;
141
+ }
142
+ return [...pieces, line.slice(start)];
143
+ }
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
+ /**
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:
161
+ * `❤️` is 1 + 0 by code point and 2 as a cluster, `👨‍👩‍👧` is 6 and 2, and a cut
162
+ * between two of its code points leaves half a glyph.
163
+ */
164
+ export function graphemes(text) {
165
+ return Array.from(clustersFrom(text, asCodePoint(0)));
166
+ }
167
+ /**
168
+ * The clusters of `text` from code unit `start` on, lazily, so a walk that
169
+ * stops early pays only for the clusters it reads.
170
+ */
171
+ function* clustersFrom(text, start) {
172
+ const segments = segmentsOf(text);
173
+ for (let at = start, s = segments.containing(at); s; s = segments.containing(at)) {
174
+ const end = s.index + s.segment.length;
175
+ yield text.slice(at, end);
176
+ at = asCodePoint(end);
177
+ }
178
+ }
179
+ /**
180
+ * Returns the largest prefix of `text` whose cell width fits within `cap` cells,
181
+ * cut between grapheme clusters. No padding — the returned string may be
182
+ * narrower than `cap` when the next glyph is wide and would overshoot. Never
183
+ * wider than `cap` cells.
184
+ *
185
+ * When the first glyph already exceeds `cap` cells, returns "" (the caller
186
+ * must decide whether to force-take the glyph or skip it).
187
+ */
188
+ export function cellFit(text, cap) {
189
+ return text.slice(0, fitLength(clustersFrom(text, asCodePoint(0)), cap));
190
+ }
191
+ /** `cellFit` from the other end: the largest suffix of `text` within `cap` cells. */
192
+ export function cellFitEnd(text, cap) {
193
+ return text.slice(text.length - fitLength(graphemes(text).reverse(), cap));
194
+ }
195
+ /** The code units of the leading `clusters` that fit within `cap` cells. */
196
+ function fitLength(clusters, cap) {
197
+ let w = 0;
198
+ let n = 0;
199
+ for (const cluster of clusters) {
200
+ const cw = cellLen(cluster);
201
+ if (w + cw > cap)
202
+ break;
203
+ w += cw;
204
+ n += cluster.length;
205
+ }
206
+ return n;
207
+ }
208
+ /**
209
+ * Returns the largest code-unit end offset starting from `startCU` whose
210
+ * prefix (from `startCU`) has cell width ≤ `cap`. Iterates from the given
211
+ * offset without slicing the tail, avoiding O(N²) allocation when called
212
+ * repeatedly across a long string.
213
+ *
214
+ * [LAW:types-are-the-program] returns CodePoint because it stops between
215
+ * grapheme clusters, and every cluster boundary is a code-point boundary.
216
+ */
217
+ export function cellFitFrom(text, startCU, cap) {
218
+ return asCodePoint(startCU + fitLength(clustersFrom(text, startCU), cap));
219
+ }
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
+ /**
237
+ * The code-unit offset a visual column falls at: the end of the largest
238
+ * prefix of `content` within `cellCol` cells, so a column inside a wide glyph
239
+ * lands before it. The inverse of `cellLen(content.slice(0, offset))`.
240
+ */
241
+ export function cellColToCodeUnitOffset(content, cellCol) {
242
+ return cellFitFrom(content, asCodePoint(0), cellCol);
243
+ }
244
+ /**
245
+ * The offset just past the grapheme cluster at `cu`: one step of a cursor, a
246
+ * Delete, a transposition, so `👍🏽` or `👨‍👩‍👧` moves and goes as the one
247
+ * glyph `cellLen` measures, never a code point at a time. `cu` is a cluster
248
+ * boundary in `s` (see `graphemeBoundary`); `s.length` gives `s.length`.
249
+ */
250
+ export function nextGrapheme(s, cu) {
251
+ const [cluster = ""] = clustersFrom(s, cu);
252
+ return asCodePoint(cu + cluster.length);
253
+ }
254
+ /** `nextGrapheme` backwards: the start of the cluster ending at `cu`; 0 at the start. */
255
+ export function prevGrapheme(s, cu) {
256
+ if (cu <= 0)
257
+ return asCodePoint(0);
258
+ return asCodePoint(segmentsOf(s).containing(cu - 1).index);
259
+ }
260
+ /**
261
+ * Where an offset into `s` comes to rest as a cursor: itself when it is a
262
+ * cluster boundary, else the end of the cluster it falls inside, and never
263
+ * past `s.length`. The one parse from any number to the boundaries
264
+ * `nextGrapheme` and `prevGrapheme` step between.
265
+ */
266
+ export function graphemeBoundary(s, cu) {
267
+ const at = Math.min(cu, s.length);
268
+ const cluster = segmentsOf(s).containing(at);
269
+ return asCodePoint(cluster === undefined || cluster.index === at ? at : cluster.index + cluster.segment.length);
270
+ }
271
+ /**
272
+ * `n` as a `TabSize`, or a `RangeError` naming it. Rich divides by the tab
273
+ * size and raises on 0; here the value is refused where it is given, not at
274
+ * the first tab it meets.
275
+ */
276
+ export function parseTabSize(n) {
277
+ if (!Number.isInteger(n) || n < 1) {
278
+ throw new RangeError(`tabSize must be a positive integer, got ${n}`);
279
+ }
280
+ return n;
281
+ }
282
+ /**
283
+ * `text` with each tab widened to the next tab stop — a multiple of `tabSize`
284
+ * cells from the start of its line — as Rich's `Text.expand_tabs` widens it, and
285
+ * `offsetOf`, where each code-unit offset into `text` lands in the result.
286
+ *
287
+ * A tab is a column position, not a run of spaces: one that already sits a
288
+ * cell short of a stop widens to a single space. `offsetOf` is there because a
289
+ * caller that annotates the text by offset — a `RichText`'s spans — has to move
290
+ * its offsets with the characters, or every annotation after a tab lands short
291
+ * of what it styled.
292
+ */
293
+ export function expandTabs(text, tabSize) {
294
+ const [head, ...rest] = text.split("\t");
295
+ const tabAt = []; // offset of each tab in `text`
296
+ const grownBy = []; // code units added by this tab and every one before it
297
+ const columnAfter = (piece, column) => {
298
+ const newline = piece.lastIndexOf("\n");
299
+ return (newline < 0 ? column : 0) + cellLen(piece.slice(newline + 1));
300
+ };
301
+ // A piece's column is read only when a tab follows it, so text with no tab
302
+ // costs one `split` and no measuring.
303
+ let expanded = head;
304
+ let before = head; // the piece the next tab follows
305
+ let column = 0; // the column `before` starts at
306
+ let offset = head.length;
307
+ let grown = 0;
308
+ for (const piece of rest) {
309
+ column = columnAfter(before, column);
310
+ const spaces = tabSize - (column % tabSize);
311
+ grown += spaces - 1;
312
+ tabAt.push(offset);
313
+ grownBy.push(grown);
314
+ expanded += " ".repeat(spaces) + piece;
315
+ column += spaces;
316
+ before = piece;
317
+ offset += 1 + piece.length;
318
+ }
319
+ // An offset moves by what every tab before it grew: the count of tabs at
320
+ // offsets below it, found by bisection since `tabAt` ascends.
321
+ const offsetOf = (at) => {
322
+ let low = 0;
323
+ let high = tabAt.length;
324
+ while (low < high) {
325
+ const mid = (low + high) >> 1;
326
+ if (tabAt[mid] < at)
327
+ low = mid + 1;
328
+ else
329
+ high = mid;
330
+ }
331
+ return at + (low === 0 ? 0 : grownBy[low - 1]);
332
+ };
333
+ return { text: expanded, offsetOf };
334
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Clock — the time source and timer whoever draws frames is handed, and the
3
+ * frame rate it ticks at.
4
+ *
5
+ * [LAW:no-ambient-temporal-coupling] Frame rate belongs to whoever draws the
6
+ * frames — an `App`, a `Live` — never to what is drawn, and each ticks on the
7
+ * `Clock` it was given. An `App` reads the frame's time through that clock and
8
+ * hands it to its view as data, so nothing rendered reads a clock, and an
9
+ * effect sampled thirty times a second or once every two seconds runs the
10
+ * same code.
11
+ *
12
+ * [LAW:effects-at-boundaries] The clock is a capability passed in, like the
13
+ * `TerminalHost`: a test hands over a clock whose time it moves by hand and
14
+ * counts frames with no real timer, and nothing it starts can outlive the
15
+ * thing that started it. `systemClock` is the one place the library reads the
16
+ * platform's clock and timers, and it reads them only when called.
17
+ */
18
+ import type { Unsubscribe } from "./subscription.js";
19
+ declare const parsed: unique symbol;
20
+ /**
21
+ * How often frames are drawn: a positive, finite number of frames a second,
22
+ * fractional included — 0.5 is one frame every two seconds.
23
+ *
24
+ * [LAW:parse-dont-validate] Only `frameRate` makes one, so holding a
25
+ * `FrameRate` is the proof the number was checked, and nothing that takes one
26
+ * checks it again.
27
+ */
28
+ export interface FrameRate {
29
+ readonly perSecond: number;
30
+ /** Seconds from one frame to the next. */
31
+ readonly interval: number;
32
+ readonly [parsed]: true;
33
+ }
34
+ /**
35
+ * `perSecond` as a `FrameRate`; anything but a positive, finite number throws,
36
+ * and so does a rate whose frames are further apart than a timer can wait —
37
+ * about 24.8 days.
38
+ */
39
+ export declare function frameRate(perSecond: number): FrameRate;
40
+ export interface Clock {
41
+ /** The time now, in seconds, on this clock's own timeline, which never runs backwards. */
42
+ now(): number;
43
+ /** Call `tick` once a frame at `rate` until the returned function is called. */
44
+ every(rate: FrameRate, tick: () => void): Unsubscribe;
45
+ }
46
+ /** The platform's monotonic clock and its interval timer. */
47
+ export declare function systemClock(): Clock;
48
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Clock — the time source and timer whoever draws frames is handed, and the
3
+ * frame rate it ticks at.
4
+ *
5
+ * [LAW:no-ambient-temporal-coupling] Frame rate belongs to whoever draws the
6
+ * frames — an `App`, a `Live` — never to what is drawn, and each ticks on the
7
+ * `Clock` it was given. An `App` reads the frame's time through that clock and
8
+ * hands it to its view as data, so nothing rendered reads a clock, and an
9
+ * effect sampled thirty times a second or once every two seconds runs the
10
+ * same code.
11
+ *
12
+ * [LAW:effects-at-boundaries] The clock is a capability passed in, like the
13
+ * `TerminalHost`: a test hands over a clock whose time it moves by hand and
14
+ * counts frames with no real timer, and nothing it starts can outlive the
15
+ * thing that started it. `systemClock` is the one place the library reads the
16
+ * platform's clock and timers, and it reads them only when called.
17
+ */
18
+ // The longest delay a platform timer holds, in seconds: setInterval clamps a
19
+ // longer one to a millisecond, which would turn the slowest rate into the
20
+ // fastest.
21
+ const LONGEST_INTERVAL = (2 ** 31 - 1) / 1000;
22
+ /**
23
+ * `perSecond` as a `FrameRate`; anything but a positive, finite number throws,
24
+ * and so does a rate whose frames are further apart than a timer can wait —
25
+ * about 24.8 days.
26
+ */
27
+ export function frameRate(perSecond) {
28
+ if (!(Number.isFinite(perSecond) && perSecond > 0 && 1 / perSecond <= LONGEST_INTERVAL)) {
29
+ throw new RangeError(`a frame rate is a positive, finite number of frames a second, at most ${LONGEST_INTERVAL} s apart, not ${perSecond}`);
30
+ }
31
+ return { perSecond, interval: 1 / perSecond };
32
+ }
33
+ /** The platform's monotonic clock and its interval timer. */
34
+ export function systemClock() {
35
+ return {
36
+ now: () => performance.now() / 1000,
37
+ every: (rate, tick) => {
38
+ const timer = setInterval(tick, rate.interval * 1000);
39
+ return () => clearInterval(timer);
40
+ },
41
+ };
42
+ }