@promptctl/rich-js 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -5
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Easing and phase: the one vocabulary for "how far along" — every function
3
- * here returns progress in [0, 1].
2
+ * Easing and phase: the one vocabulary for "how far along" — a time in
3
+ * seconds becomes progress in [0, 1], and an ease reshapes that progress,
4
+ * usually but not always within [0, 1].
4
5
  *
5
6
  * Two kinds of curve, composed as `ease(phase(t))`:
6
7
  *
@@ -13,15 +14,26 @@
13
14
  * built-ins are CSS's easing functions (CSS Easing Functions Level 1):
14
15
  * `linear`, `cubic-bezier()` with its named curves, and `steps()` with its
15
16
  * jump terms, plus `sine` for pulses — a half-cosine, so a `pingPong`
16
- * through it brightens and dims with no corner at either end.
17
+ * through it brightens and dims with no corner at either end — and the
18
+ * physics-flavoured curves a cursor snapping, a counter settling or a
19
+ * panel sliding in want: `spring`, a damped harmonic oscillator released
20
+ * toward its rest, which is the one of them with parameters and so the one
21
+ * that also says how long it takes; `bounce`, `back-in`, `back-out` and
22
+ * `elastic`, Robert Penner's curves at the constants every animation
23
+ * library since has shipped. An ease that leaves [0, 1] — a spring or a
24
+ * back curve overshooting its rest — is still an ease: a `ColorRamp` holds
25
+ * its end colours where it does, and an effect's `Curve` saturates at its
26
+ * swing, so the overshoot is read by what can show it (a position, a
27
+ * count) and clipped by what cannot.
17
28
  *
18
29
  * [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. `t` is an
19
30
  * argument; whoever draws frames owns the clock and samples these at
20
31
  * whatever rate it likes, so the same `t` always gives the same progress.
21
32
  *
22
33
  * [LAW:one-type-per-behavior] An ease is a plain function value. CSS's named
23
- * curves are four instances of `cubicBezier`, not four implementations, and
24
- * `ColorRamp` and every effect take the same `Ease`.
34
+ * curves are four instances of `cubicBezier`, not four implementations, the
35
+ * spring in `EASES` is one `spring()`, and `ColorRamp` and every effect take
36
+ * the same `Ease`.
25
37
  *
26
38
  * Tier 0 of `src/core/`: imports nothing, so `themes/` and every later
27
39
  * `core/` module reach it downhill. [LAW:one-way-deps]
@@ -79,7 +91,7 @@ export function cubicBezier(x1, y1, x2, y2) {
79
91
  }
80
92
  return s;
81
93
  };
82
- return (x) => yAt(solve(Math.min(1, Math.max(0, x))));
94
+ return settled((p) => yAt(solve(p)));
83
95
  }
84
96
  /**
85
97
  * CSS `steps(n, position)`: progress held flat in `n` equal intervals, jumping
@@ -103,22 +115,183 @@ export function steps(n, position = "jump-end") {
103
115
  "jump-none": n - 1,
104
116
  }[position];
105
117
  const lift = jumpsAtStart ? 1 : 0;
106
- return (x) => {
107
- const p = Math.min(1, Math.max(0, x));
108
- return Math.min(jumps, Math.floor(p * n) + lift) / jumps;
109
- };
118
+ return settled((p) => Math.min(jumps, Math.floor(p * n) + lift) / jumps);
110
119
  }
120
+ /**
121
+ * The curve `shape` over [0, 1) as an ease that has arrived: its argument
122
+ * held to [0, 1], and exactly 1 at 1. Every built-in ease is one, so the two
123
+ * halves of the `Ease` contract — an argument in [0, 1], the end landed on —
124
+ * are enforced here and nowhere else. [LAW:single-enforcer] The end matters
125
+ * because a curve is 1 at its end only in the limit or up to a rounding — a
126
+ * spring settles to within `SPRING_REST`, the elastic's ringing never quite
127
+ * stops, a back curve's cubic or a bezier's Newton step lands an ulp off —
128
+ * and a count or a width eased by it must land on its target, not a
129
+ * thousandth or an ulp short.
130
+ */
131
+ const settled = (shape) => (x) => {
132
+ const p = Math.min(1, Math.max(0, x));
133
+ return p === 1 ? 1 : shape(p);
134
+ };
111
135
  /** The identity ease: progress unchanged. */
112
- const linear = (x) => x;
136
+ const linear = settled((p) => p);
113
137
  /** CSS `step-end`: 0 until the very end. */
114
138
  const stepEnd = steps(1, "jump-end");
115
139
  /** A half-cosine, flat at both ends: the curve a breathing pulse wants. */
116
- const sine = (x) => (1 - Math.cos(Math.PI * x)) / 2;
140
+ const sine = settled((p) => (1 - Math.cos(Math.PI * p)) / 2);
141
+ /**
142
+ * How close to its rest a spring must come to count as settled: one part in a
143
+ * thousand of the way it moves, under a colour channel's step (1/255) and
144
+ * under any cell, so what rounds the spring's value to show it draws the rest
145
+ * from there on.
146
+ */
147
+ export const SPRING_REST = 0.001;
148
+ const DEFAULT_SPRING = { stiffness: 100, damping: 10, mass: 1 };
149
+ /**
150
+ * A damped harmonic oscillator, `m·x″ = −k·x − c·x′`, released at rest one
151
+ * unit from its equilibrium: the spring every animation library means. Its
152
+ * progress at `s` seconds is `1 − displacement(s)`, the closed-form solution
153
+ * of that equation in the regime its damping ratio `ζ = c / 2√(k·m)` puts it
154
+ * in — ringing past its rest for `ζ < 1`, creeping up to it for `ζ ≥ 1` — and
155
+ * its `seconds` is its settling time: the first moment after which the
156
+ * displacement stays within `SPRING_REST`, found once here by `crossing` on
157
+ * the bracket each regime knows its last crossing lies in, so a sample costs
158
+ * an exponential and a cosine whatever `x` is. [LAW:no-ambient-temporal-coupling]
159
+ *
160
+ * The ease is exactly 1 at `x = 1` (`settled`): settled means at rest. The
161
+ * `seconds` are the spring's to say, not the caller's: how long it rings is
162
+ * a fact about its physics, which is why this is the one ease that comes as
163
+ * a `Timing` — released at rest a unit from where it is going, at 1 when it
164
+ * has settled there.
165
+ */
166
+ export function spring(options = {}) {
167
+ const { stiffness, damping, mass } = { ...DEFAULT_SPRING, ...options };
168
+ for (const [name, v] of [["stiffness", stiffness], ["damping", damping], ["mass", mass]])
169
+ positive(`spring ${name}`, v);
170
+ const w0 = Math.sqrt(stiffness / mass);
171
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
172
+ const decay = zeta * w0;
173
+ const { displacement, seconds } = zeta < 1 ? ringing(w0, zeta, decay) : creeping(w0, zeta, decay);
174
+ return { seconds, ease: settled((p) => 1 - displacement(p * seconds)) };
175
+ }
176
+ /**
177
+ * Where `|displacement|` falls through `SPRING_REST` between `lo` and `hi`,
178
+ * by bisection: the caller brackets the last such crossing, the displacement
179
+ * outside rest at `lo`, inside it at `hi` and falling in size between, and
180
+ * gets the settling time back to the bits 64 halvings leave.
181
+ * [LAW:single-enforcer] The one place the rest threshold is read against a
182
+ * motion.
183
+ */
184
+ function crossing(displacement, lo, hi) {
185
+ for (let i = 0; i < 64; i++) {
186
+ const mid = (lo + hi) / 2;
187
+ if (Math.abs(displacement(mid)) > SPRING_REST)
188
+ lo = mid;
189
+ else
190
+ hi = mid;
191
+ }
192
+ return hi;
193
+ }
194
+ /**
195
+ * Underdamped: `e^(−ζω₀s)·(cos ω_d s + (ζω₀/ω_d) sin ω_d s)` at the damped
196
+ * frequency `ω_d = ω₀√(1 − ζ²)`. Released at rest, its velocity is
197
+ * `−(ω₀²/ω_d)·e^(−ζω₀s)·sin ω_d s`, so its extrema sit at `s_k = kπ/ω_d`,
198
+ * where `|displacement| = e^(−ζω₀·s_k)`, each the same fraction of the last.
199
+ * The first extremum inside rest is the `K` with `e^(−ζω₀·s_K) ≤ SPRING_REST`;
200
+ * the spring never leaves rest after it, and before it the displacement
201
+ * falls in size from the extremum at `s_(K−1)` to its zero at
202
+ * `(Kπ − atan(ω_d/ζω₀)) / ω_d`, so the settling time is the crossing in that
203
+ * bracket. The decay envelope control theory reads settling time off would
204
+ * put it later — by up to half a swing, and without bound as `ζ → 1` — so
205
+ * the true crossing is found instead, and `seconds` is continuous in `ζ`.
206
+ */
207
+ function ringing(w0, zeta, decay) {
208
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
209
+ const ratio = decay / wd;
210
+ const displacement = (s) => Math.exp(-decay * s) * (Math.cos(wd * s) + ratio * Math.sin(wd * s));
211
+ const K = Math.ceil(Math.log(1 / SPRING_REST) / ((decay * Math.PI) / wd));
212
+ const extremum = ((K - 1) * Math.PI) / wd;
213
+ const zero = (K * Math.PI - Math.atan(1 / ratio)) / wd;
214
+ return { displacement, seconds: crossing(displacement, extremum, zero) };
215
+ }
216
+ /**
217
+ * Critically damped (`ζ = 1`): `e^(−ω₀s)·(1 + ω₀s)`. Overdamped (`ζ > 1`):
218
+ * two decaying exponentials at the rates `ζω₀ ∓ ω₀√(ζ² − 1)`, written as such
219
+ * rather than as `cosh + sinh` so neither grows before the other cancels it.
220
+ * Both only fall, so the settling time is the one crossing of `SPRING_REST`,
221
+ * bracketed between zero and a time the slower exponential alone puts it under.
222
+ */
223
+ function creeping(w0, zeta, decay) {
224
+ const { displacement, far } = zeta === 1 ? critical(w0) : overdamped(w0, zeta, decay);
225
+ return { displacement, seconds: crossing(displacement, 0, far) };
226
+ }
227
+ /** `(1 + u)·e^(−u)` in `u = ω₀s`, which is under 10⁻³ from u ≈ 9.24. */
228
+ function critical(w0) {
229
+ return { displacement: (s) => Math.exp(-w0 * s) * (1 + w0 * s), far: 10 / w0 };
230
+ }
231
+ /**
232
+ * The slower exponential alone bounds the sum, since the faster one's weight
233
+ * `1 − ratio` is negative. Its rate `ζω₀ − ω₀√(ζ² − 1)` is written as
234
+ * `ω₀ / (ζ + √(ζ² − 1))`, the same number without the subtraction, which for
235
+ * a large `ζ` cancels to zero and would make `far` infinite.
236
+ */
237
+ function overdamped(w0, zeta, decay) {
238
+ const root = Math.sqrt(zeta * zeta - 1);
239
+ const wh = w0 * root;
240
+ const slow = w0 / (zeta + root);
241
+ const fast = decay + wh;
242
+ const ratio = decay / wh;
243
+ return {
244
+ displacement: (s) => ((1 + ratio) * Math.exp(-slow * s) + (1 - ratio) * Math.exp(-fast * s)) / 2,
245
+ far: Math.log((1 + ratio) / (2 * SPRING_REST)) / slow,
246
+ };
247
+ }
248
+ /**
249
+ * Robert Penner's bounce (his `easeOutBounce`): a ball dropped onto its rest,
250
+ * four parabolic arcs each a quarter the height of the last, the constants
251
+ * his and every library's since. Each arc is the same parabola,
252
+ * `7.5625·(x − centre)² + floor`, set over its own span of `x`; the spans
253
+ * are in elevenths of 2.75, where the first arc meets the ground.
254
+ */
255
+ const BOUNCE_ARCS = [
256
+ { until: 1 / 2.75, centre: 0, floor: 0 },
257
+ { until: 2 / 2.75, centre: 1.5 / 2.75, floor: 0.75 },
258
+ { until: 2.5 / 2.75, centre: 2.25 / 2.75, floor: 0.9375 },
259
+ { until: Number.POSITIVE_INFINITY, centre: 2.625 / 2.75, floor: 0.984375 },
260
+ ];
261
+ const bounce = settled((p) => {
262
+ // The last arc's `until` is unbounded, so an arc is always found.
263
+ const arc = BOUNCE_ARCS.find(({ until }) => p < until);
264
+ return 7.5625 * (p - arc.centre) ** 2 + arc.floor;
265
+ });
266
+ /**
267
+ * Penner's back curves: a cubic that pulls back past its start before going
268
+ * (`back-in`), or overshoots its end before settling (`back-out`), by his
269
+ * overshoot constant 1.70158 — about a tenth of the way. His `(c + 1)·x³ −
270
+ * c·x²` is written as `x³ + c·x²·(x − 1)`, the same cubic with the overshoot
271
+ * term a product that is exactly zero at both ends, so the curve starts on 0
272
+ * and lands on 1 to the bit.
273
+ */
274
+ const BACK_OVERSHOOT = 1.70158;
275
+ const backIn = settled((p) => p ** 3 + BACK_OVERSHOOT * p ** 2 * (p - 1));
276
+ const backOut = settled((p) => {
277
+ const q = p - 1;
278
+ return 1 + q ** 3 + BACK_OVERSHOOT * q ** 2 * (q + 1);
279
+ });
280
+ /**
281
+ * Penner's elastic (his `easeOutElastic`): overshoots its end and rings back
282
+ * through it, with a period of 0.3 of the way under an amplitude of
283
+ * `2^(−10x)`, so each swing is `2^(−1.5)`, a little over a third, of the
284
+ * last — `2^(−10x)·sin((10x − 0.75)·2π/3) + 1`.
285
+ */
286
+ const ELASTIC_PERIOD = (2 * Math.PI) / 3;
287
+ const elastic = settled((p) => 2 ** (-10 * p) * Math.sin((10 * p - 0.75) * ELASTIC_PERIOD) + 1);
117
288
  /**
118
289
  * Every built-in ease by the name a template spells it with — CSS's keyword
119
290
  * names, plus `sine`, plus `step`: the spelling `ColorRamp` used before this
120
291
  * vocabulary existed, kept so every template written against it renders the
121
- * same. It is CSS's `step-end`, the same function value.
292
+ * same. It is CSS's `step-end`, the same function value. Then the
293
+ * physics-flavoured curves: `spring` is the default `spring()`, whose
294
+ * `seconds` are `DEFAULT_SPRING`'s; the rest are Penner's at his constants.
122
295
  */
123
296
  export const EASES = {
124
297
  linear,
@@ -130,6 +303,11 @@ export const EASES = {
130
303
  "step-end": stepEnd,
131
304
  step: stepEnd,
132
305
  sine,
306
+ spring: spring().ease,
307
+ bounce,
308
+ "back-in": backIn,
309
+ "back-out": backOut,
310
+ elastic,
133
311
  };
134
312
  /**
135
313
  * The gate a spelled ease crosses. [LAW:parse-dont-validate] — a name in, the
@@ -143,10 +321,170 @@ export function parseEase(name) {
143
321
  }
144
322
  return EASES[name];
145
323
  }
146
- function positiveSeconds(what, seconds) {
147
- if (!Number.isFinite(seconds) || seconds <= 0) {
148
- throw new RangeError(`${what} must be a positive number of seconds, got ${seconds}`);
324
+ /** `v` is a positive, finite number, or this throws naming `what`; `unit` is the number's noun, as in `" of seconds"`. [LAW:single-enforcer] */
325
+ function positive(what, v, unit = "") {
326
+ if (!(Number.isFinite(v) && v > 0))
327
+ throw new RangeError(`${what} must be a positive number${unit}, got ${v}`);
328
+ }
329
+ /** Throws, naming `what`, unless `seconds` is a positive, finite number of seconds. */
330
+ export function positiveSeconds(what, seconds) {
331
+ positive(what, seconds, " of seconds");
332
+ }
333
+ /**
334
+ * The seconds into the current pass of a `period` at `t`: `t` taken modulo
335
+ * the period, into [0, period), before zero too. [LAW:single-enforcer] The
336
+ * one wrap `Phase.loop` and `beat` both read time through, so a `t` that is
337
+ * not a finite number of seconds fails here, loudly, and nowhere else. A
338
+ * `t` at or past zero is `t % period` alone, which is exact; only a negative
339
+ * `t` is carried up by a period, so a boundary a host samples exactly lands
340
+ * on the pass that begins there, and the answer is always under the period.
341
+ */
342
+ function wrapSeconds(what, t, period) {
343
+ if (!Number.isFinite(t))
344
+ throw new RangeError(`${what} needs a finite t, got ${t}`);
345
+ const into = t % period;
346
+ if (into >= 0)
347
+ return into;
348
+ // A negative epsilon carried up rounds to the period itself; that is the
349
+ // start of the next pass, never a point past the end of this one.
350
+ const carried = into + period;
351
+ return carried >= period ? 0 : carried;
352
+ }
353
+ /**
354
+ * The float after `x`: one ulp up, away from zero for a positive `x` and
355
+ * toward it for a negative one, read through the bits of the double.
356
+ */
357
+ function nextUp(x) {
358
+ if (x === 0)
359
+ return Number.MIN_VALUE;
360
+ DOUBLE[0] = x;
361
+ BITS[0] = BITS[0] + (x > 0 ? 1n : -1n);
362
+ return DOUBLE[0];
363
+ }
364
+ const DOUBLE = new Float64Array(1);
365
+ const BITS = new BigUint64Array(DOUBLE.buffer);
366
+ /** How many ulps the roundings in a sum naming a boundary can put it under that boundary; a walk past this is a boundary finer than time resolves there. */
367
+ const ULPS_OF_ROUNDING = 8;
368
+ /**
369
+ * The first float from `sum` up at which `reached` holds: a time built as a
370
+ * sum to land on a boundary — where a hold begins, where an animation's next
371
+ * part changes — that rounding put an ulp or two under it, walked up to the
372
+ * first moment on the far side. [LAW:single-enforcer] The one walk, so every
373
+ * boundary a time is carried onto is the moment the change shows, to the
374
+ * float. A walk longer than rounding can cost throws `finer()`'s message: the
375
+ * boundary is narrower than the floats around it are apart.
376
+ */
377
+ export function walkUp(sum, reached, finer) {
378
+ let x = sum;
379
+ for (let walked = 0; !reached(x); walked++) {
380
+ if (walked === ULPS_OF_ROUNDING)
381
+ throw new RangeError(finer());
382
+ x = nextUp(x);
383
+ }
384
+ return x;
385
+ }
386
+ /**
387
+ * The least float at which `reached` holds, `reached` being false up to some
388
+ * float and true from it on, found from `near`, a guess at it on either
389
+ * side. Where `walkUp` carries a sum onto a boundary on its own clock, and
390
+ * an ulp or two is all rounding can cost, this carries a moment across
391
+ * clocks (`./timeline.ts`): a host float is a step of `rate` on the
392
+ * animation's clock, so the host floats between the guess and the first one
393
+ * past the boundary are as many as the two clocks' resolutions are apart,
394
+ * which no cap bounds and is no error. The bracket is found by doubling the
395
+ * step out from `near`, and halved to two adjacent floats, so the answer is
396
+ * exact to the float and the cost is logarithmic in that distance.
397
+ */
398
+ export function firstReaching(near, reached) {
399
+ // [LAW:no-silent-failure] A step out from an infinite guess is NaN, and a
400
+ // bracket of NaN never closes.
401
+ if (!Number.isFinite(near))
402
+ throw new RangeError(`firstReaching needs a finite guess, got ${near}`);
403
+ let lo = near;
404
+ let hi = near;
405
+ // Out from `near` until the bracket is `lo` short of the boundary and
406
+ // `hi` past it, within the finite floats: reached at the least of them,
407
+ // it is reached always; not reached at the greatest, never.
408
+ for (let step = nextUp(Math.abs(near)) - Math.abs(near); reached(lo); step *= 2) {
409
+ if (lo === -Number.MAX_VALUE)
410
+ return Number.NEGATIVE_INFINITY;
411
+ hi = lo;
412
+ lo = Math.max(near - step, -Number.MAX_VALUE);
149
413
  }
414
+ for (let step = nextUp(Math.abs(near)) - Math.abs(near); !reached(hi); step *= 2) {
415
+ if (hi === Number.MAX_VALUE)
416
+ return Number.POSITIVE_INFINITY;
417
+ lo = hi;
418
+ hi = Math.min(near + step, Number.MAX_VALUE);
419
+ }
420
+ for (;;) {
421
+ const mid = lo + (hi - lo) / 2;
422
+ if (mid <= lo || mid >= hi)
423
+ return hi;
424
+ if (reached(mid))
425
+ hi = mid;
426
+ else
427
+ lo = mid;
428
+ }
429
+ }
430
+ /** `holds` as a `Beat`; a beat with no hold, or a hold that is not a positive number of seconds, throws. */
431
+ export function beat(holds) {
432
+ if (holds.length === 0)
433
+ throw new RangeError("a beat needs at least one hold");
434
+ holds.forEach((hold, i) => positiveSeconds(`beat hold ${i}`, hold));
435
+ const period = holds.reduce((sum, hold) => sum + hold, 0);
436
+ // Where each hold begins, in seconds into the period: the unit
437
+ // `wrapSeconds` answers in, so the sum a boundary sits at is the number
438
+ // the wrap of a `t` on that boundary produces, and the hold that begins
439
+ // there is the one in progress, at any rate the host samples.
440
+ const starts = [];
441
+ let elapsed = 0;
442
+ for (const hold of holds) {
443
+ starts.push(elapsed);
444
+ elapsed += hold;
445
+ }
446
+ // A copy, and the one `at` walks: what `at` and `period` were built from
447
+ // stays what `holds` says whatever the caller does to its array.
448
+ const own = [...holds];
449
+ // The last hold whose start the time has reached. The first starts at
450
+ // 0, so the walk always stops, and its length is the count of holds,
451
+ // never a function of `t`.
452
+ const index = (into) => {
453
+ let i = own.length - 1;
454
+ while (starts[i] > into)
455
+ i--;
456
+ return i;
457
+ };
458
+ // Where hold `i` of pass `pass` begins: the pass's start plus the hold's,
459
+ // two integers and two constants, so every `t` in the hold builds the one
460
+ // float. That sum can land an ulp or two under the boundary when the holds
461
+ // are not exact in binary — a tenth of a second is not — and `at` would
462
+ // then read it as the hold before, so it is walked up to the first float
463
+ // `at` places in the hold. The walk is a function of the pass and the hold
464
+ // alone, so every `t` in the hold still answers one float.
465
+ const boundary = (what, pass, i, t) => walkUp(pass * period + starts[i], (began) => index(wrapSeconds(what, began, period)) === i, () => `${what}: hold ${i} (${own[i]} s) is finer than time resolves near t = ${t}`);
466
+ // Which pass `t` is in, as a whole number: `t - into` is a whole number of
467
+ // periods but for rounding, which `round` takes off; carrying `starts[i]`
468
+ // out of `t` itself would leave each sample its own rounding.
469
+ const pass = (t, into) => Math.round((t - into) / period);
470
+ return {
471
+ holds: own,
472
+ period,
473
+ at(t) {
474
+ return index(wrapSeconds("Beat.at", t, period));
475
+ },
476
+ lastChange(t) {
477
+ const into = wrapSeconds("Beat.lastChange", t, period);
478
+ return boundary("Beat.lastChange", pass(t, into), index(into), t);
479
+ },
480
+ // The next hold's start, in the next pass when this hold is the last.
481
+ nextChange(t) {
482
+ const into = wrapSeconds("Beat.nextChange", t, period);
483
+ const i = index(into);
484
+ const last = i === own.length - 1;
485
+ return boundary("Beat.nextChange", pass(t, into) + (last ? 1 : 0), last ? 0 : i + 1, t);
486
+ },
487
+ };
150
488
  }
151
489
  /** The three phase shapes. Each takes seconds and returns a `Phase`. */
152
490
  export const Phase = {
@@ -160,7 +498,7 @@ export const Phase = {
160
498
  /** 0 → 1 every `period` seconds, wrapping back to 0; negative `t` wraps too. */
161
499
  loop(period) {
162
500
  positiveSeconds("Phase.loop period", period);
163
- return (t) => (((t % period) + period) % period) / period;
501
+ return (t) => wrapSeconds("Phase.loop", t, period) / period;
164
502
  },
165
503
  /** 0 → 1 → 0 every `period` seconds: there in the first half, back in the second. */
166
504
  pingPong(period) {
package/dist/core/json.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { RichText } from "./text.js";
10
10
  import { JSONHighlighter } from "./highlighter.js";
11
+ import { render } from "./protocol.js";
11
12
  const highlighter = new JSONHighlighter();
12
13
  export class JSONRenderable {
13
14
  text;
@@ -45,7 +46,7 @@ export class JSONRenderable {
45
46
  return new JSONRenderable(text);
46
47
  }
47
48
  *render(options) {
48
- yield* this.text.render(options);
49
+ yield* render(this.text, options);
49
50
  }
50
51
  measure(options) {
51
52
  return this.text.measure(options);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Measurement — min/max cell width calculation for renderables.
3
3
  */
4
- import { isMeasurable, measuring } from "./protocol.js";
4
+ import { drawsNothing, isMeasurable, measuring } from "./protocol.js";
5
5
  export class Measurement {
6
6
  minimum;
7
7
  maximum;
@@ -45,7 +45,7 @@ export class Measurement {
45
45
  // `Measurement.get` answers for it — so under an unbounded offer it asks for
46
46
  // `Infinity`, and `withBoundedWidth` refuses rather than guessing a width.
47
47
  static get(options, renderable) {
48
- if (options.maxWidth < 1)
48
+ if (drawsNothing(options))
49
49
  return new Measurement(0, 0);
50
50
  if (!isMeasurable(renderable))
51
51
  return new Measurement(0, options.maxWidth);
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { Segment } from "./segment.js";
21
21
  import { Measurement } from "./measure.js";
22
+ import { render } from "./protocol.js";
22
23
  // The share of the spare cells that goes on the left; the rest go on the right.
23
24
  // Right-alignment pads no right side at all, which is the reference's choice
24
25
  // and not a zero: a line that ends at the edge carries no trailing blanks.
@@ -39,7 +40,7 @@ export function placeBlock(renderable, align, options) {
39
40
  // (`Measurement.get` answers for it), and the block is then only as wide as
40
41
  // what it actually drew.
41
42
  const width = Measurement.get(options, renderable).maximum;
42
- const drawn = Segment.splitLines(renderable.render({ ...options, maxWidth: width, height: undefined }));
43
+ const drawn = Segment.splitLines(render(renderable, { ...options, maxWidth: width, height: undefined }));
43
44
  const [blockWidth, height] = Segment.getShape(drawn);
44
45
  const [left, right] = PADS[align](Math.max(0, options.maxWidth - blockWidth));
45
46
  return Segment.setShape(drawn, blockWidth, height).map((line) => [...pad(left), ...line, ...pad(right)]);
@@ -21,7 +21,7 @@ import { divideLine } from "./wrap.js";
21
21
  import { Segment } from "./segment.js";
22
22
  import { RichText, stripControlChars } from "./text.js";
23
23
  import { ReprHighlighter } from "./highlighter.js";
24
- import { drawable } from "./protocol.js";
24
+ import { drawable, render } from "./protocol.js";
25
25
  const reprHighlighter = new ReprHighlighter();
26
26
  /**
27
27
  * What a value is bounded by when whoever formats it has not seen it:
@@ -491,7 +491,7 @@ export class Pretty {
491
491
  *render(options) {
492
492
  // `toText` has no `end`, for callers that set it inside a line; rendered,
493
493
  // a `Pretty` is a block of its own, as the reference's is.
494
- yield* this.toText(options).render(options);
494
+ yield* render(this.toText(options), options);
495
495
  yield Segment.line();
496
496
  }
497
497
  /** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
@@ -3,7 +3,7 @@
3
3
  * [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
4
4
  */
5
5
  import type { Segment } from "./segment.js";
6
- import { type Style, type StyleSyntaxError, type Theme } from "./style.js";
6
+ import { Style, type StyleSyntaxError, type Theme } from "./style.js";
7
7
  import { ColorDepth } from "./color.js";
8
8
  import type { Highlighter } from "./highlighter.js";
9
9
  /**
@@ -87,15 +87,21 @@ export interface RenderOptions {
87
87
  */
88
88
  onStyleError?: StyleErrorHandler;
89
89
  /**
90
- * Told of each owner that stamps its output (`core/anchor`) as it starts to
91
- * render, so the calls arrive in document order: a container's children in
92
- * the order it renders them, an owner before the owners nested in it. A
93
- * container forwards it by passing its options on, which every container
94
- * does. An owner rendered twice is told twice. A render made to measure is
95
- * not drawing and is not told (`measuring`).
90
+ * Told of every renderable `render` is asked for, as it is asked, so the
91
+ * calls arrive in document order: a container before what it holds, its
92
+ * contents in the order it renders them. `"drawn"` is a renderable drawn at
93
+ * the width it was handed; `"held"` is one handed no column, which is still
94
+ * in the view but has nothing drawn for it (`render`). A renderable rendered
95
+ * twice is told twice. A render made to measure is not drawing and is not
96
+ * told (`measuring`).
96
97
  */
97
- onDraw?: (owner: object) => void;
98
+ onPresent?: (renderable: Renderable, presence: Presence) => void;
98
99
  }
100
+ /**
101
+ * Whether a renderable the view holds was drawn, or held: in the view, handed
102
+ * no column, and so not drawn (`render`).
103
+ */
104
+ export type Presence = "drawn" | "held";
99
105
  /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
100
106
  export declare const isAscii: (text: string) => boolean;
101
107
  /**
@@ -210,7 +216,10 @@ export declare function getStyle(options: RenderOptions, style: string | Style):
210
216
  * every bundled theme. So where the depth would turn a fixed colour of the
211
217
  * pair into a slot, the pair is dropped and the run is drawn on the
212
218
  * terminal's own foreground and background, which its theme does make
213
- * readable; its attributes still set it apart. A style whose colours are
219
+ * readable; its attributes still set it apart. Both are named, not left
220
+ * unset, so the colour of whatever the run sits in — a quote, a link — does
221
+ * not show through: Rich's code keeps its own colours inside a quote, and the
222
+ * terminal's are the ones it keeps here. A style whose colours are
214
223
  * slots already (`white on blue`), or that paints no ground (`bold magenta`),
215
224
  * was written for the terminal's own colours and is drawn as written. Rich
216
225
  * draws a highlighted line number the same way below 256 colours
@@ -238,6 +247,40 @@ export declare function groundedStyle(options: RenderOptions, style: Style): Sty
238
247
  * ignored the request and emitted its full natural width.
239
248
  */
240
249
  export declare function withCellWidth(options: RenderOptions): RenderOptions;
250
+ /** Under one column there is nothing to draw, or to measure. */
251
+ export declare function drawsNothing(options: {
252
+ readonly maxWidth: number;
253
+ }): boolean;
254
+ /**
255
+ * `renderable` drawn under `options`: Rich's `Console.render`, which every
256
+ * renderable there is drawn through, so under one column nothing is.
257
+ *
258
+ * [LAW:single-enforcer] The one place that rule is kept. Every call of a
259
+ * `Renderable`'s `render` in `src/` goes through here — a container drawing
260
+ * its child, a renderable drawing the text it built, a host drawing a frame —
261
+ * and `test/seam/render-calls.test.ts` fails on one that does not. Before
262
+ * this, each container asked `drawsNothing` itself, and the ones that forgot
263
+ * drew a child handed no column anyway.
264
+ *
265
+ * It is also the one reporter of what the view holds (`onPresent`). Under one
266
+ * column a renderable is held, not drawn: what it holds (`Composite`) is
267
+ * walked and reported held in turn, and nothing renders, so no renderable
268
+ * that keeps state while it renders — a `Viewport`'s offset, a `TextInput`'s
269
+ * scroll window — ever commits it at a width that is not shown. The walk runs
270
+ * whether or not anyone listens. Drawing the squeezed subtree at one column
271
+ * instead, and dropping what it drew, did commit that state.
272
+ */
273
+ export declare function render(renderable: Renderable, options: RenderOptions): Iterable<Segment>;
274
+ /**
275
+ * `options` as a measurement is asked with. A `measure` that renders to learn
276
+ * its shape draws nothing on the frame, so it reports nothing to `onPresent` —
277
+ * one that did would put a widget in the view at the moment a container
278
+ * measured it, and keep one the frame then cropped away.
279
+ *
280
+ * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
281
+ * `withBoundedWidth` ask through it, and each `measure` passes it on.
282
+ */
283
+ export declare function measuring(options: RenderOptions): RenderOptions;
241
284
  /**
242
285
  * The options a renderable should *lay out against*: `withCellWidth`, with an
243
286
  * unbounded offer resolved to the renderable's own natural width.
@@ -273,16 +316,6 @@ export declare function withCellWidth(options: RenderOptions): RenderOptions;
273
316
  * rather than zero, and saying so names the cause — where `String.repeat` and
274
317
  * `new Array` only ever name their own argument.
275
318
  */
276
- /**
277
- * `options` as a measurement is asked with. A `measure` that renders to learn
278
- * its shape draws nothing on the frame, so it reports no owner to `onDraw` —
279
- * one that did would put a widget in document order at the moment a
280
- * container measured it, and keep one the frame then cropped away.
281
- *
282
- * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
283
- * `withBoundedWidth` ask through it, and each `measure` passes it on.
284
- */
285
- export declare function measuring(options: RenderOptions): RenderOptions;
286
319
  export declare function withBoundedWidth(options: RenderOptions, self: Measurable): RenderOptions;
287
320
  export interface Renderable {
288
321
  render(options: RenderOptions): Iterable<Segment>;
@@ -293,6 +326,26 @@ export interface Measurable {
293
326
  maximum: number;
294
327
  };
295
328
  }
329
+ /**
330
+ * A container: a renderable that draws other renderables. `contents` is what
331
+ * its `render` hands to `render`, in that order where the order is not a
332
+ * width's, decided as the render decides it — and so it is how a container
333
+ * handed no column still reports what it holds (`render`). It takes no
334
+ * options: what a container holds may depend on its state (a hidden `Layout`
335
+ * holds nothing) but never on a width, because under one column there is none.
336
+ * It commits no state, and yields only renderables: text a container builds
337
+ * from a string it holds holds nothing.
338
+ *
339
+ * [LAW:one-source-of-truth] A second description of each container's
340
+ * traversal, so `test/renderables/contents.test.ts` holds every exported
341
+ * renderable's `contents` to what its `render` reports.
342
+ *
343
+ * A consumer's container that does not implement it reports nothing it holds
344
+ * while it is handed no column.
345
+ */
346
+ export interface Composite {
347
+ contents(): Iterable<Renderable>;
348
+ }
296
349
  /**
297
350
  * What the wheel scrolls: an owner of anchors on the frame (`./anchor.ts`)
298
351
  * that moves by lines. `canScrollBy` says whether `scrollBy` with the same
@@ -305,3 +358,4 @@ export interface Scrollable {
305
358
  export declare function isRenderable(obj: unknown): obj is Renderable;
306
359
  export declare function isMeasurable(obj: unknown): obj is Measurable;
307
360
  export declare function isScrollable(obj: object): obj is Scrollable;
361
+ export declare function isComposite(obj: object): obj is Composite;