@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,10 @@
1
1
  /**
2
- * Viewport — a fixed number of rows onto a taller renderable, scrolled by an
3
- * offset that cannot leave the content.
2
+ * Viewport — a scroll position: a fixed number of rows onto taller content,
3
+ * scrolled by an offset that cannot leave it. It holds the position, not the
4
+ * content: content is drawn through it with `showing` (or `showingLines`), so
5
+ * a view rebuilt every frame keeps one viewport under content it builds anew.
6
+ * [LAW:one-source-of-truth] That is the one way to draw through it, so no
7
+ * viewport is built around content it never draws.
4
8
  *
5
9
  * Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
6
10
  * when it is given one; otherwise its own configured rows, or the content's
@@ -19,11 +23,18 @@
19
23
  * against exists before a render: the rows come from the budget the render is
20
24
  * handed, and the content's length from rendering it at the width it is handed.
21
25
  * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
- * are called. Each queues a move, and the next render resolves the queue in
23
- * call order, clamping after every move, against the rows and length that
24
- * render found. Calling an operation before the first render, or three of them
26
+ * are called. Each queues a move, and the next render that draws the content
27
+ * resolves the queue in call order, clamping after every move, against the
28
+ * rows and length that render found. Calling an operation before the first render, or three of them
25
29
  * between two renders, is the same code path as calling one.
26
30
  *
31
+ * A render changes nothing (`State`, `../core/state.ts`), so the render draws
32
+ * the offset it resolved without keeping it, and hands the keeping to its pass
33
+ * (`afterPass`): once the frame has been drawn whole, the offset, the rows and
34
+ * length it was clamped against, and the followed range become the viewport's,
35
+ * and the moves it resolved leave the queue. A measurement, which draws
36
+ * nothing anyone sees, keeps nothing.
37
+ *
27
38
  * Every cell of its rows, gutter included, is stamped as its own
28
39
  * (`Segment.anchorLines`), around whatever anchors its content drew. So the
29
40
  * composed frame names the viewport under any of its cells however deep it is
@@ -35,7 +46,16 @@ import { Segment } from "../core/segment.js";
35
46
  import { Measurement } from "../core/measure.js";
36
47
  import { cellCount, cellLen } from "../core/cells.js";
37
48
  import { NULL_STYLE } from "../core/style.js";
38
- import { drawable, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
49
+ import { drawable, drawsNothing, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
50
+ import { afterPass, render } from "../core/protocol.js";
51
+ import { ListState, State } from "../core/state.js";
52
+ /** `content` drawn whole at the width `options` offers, as lines a viewport slices. */
53
+ function drawnWhole(content) {
54
+ return (options) => {
55
+ const all = Segment.splitLines(render(content, options));
56
+ return { length: all.length, lines: (start, end) => all.slice(start, end) };
57
+ };
58
+ }
39
59
  /** A heavy line for the thumb on a light line for the track. */
40
60
  export const SCROLLBAR = {
41
61
  thumb: { glyph: "┃", style: "scrollbar.thumb" },
@@ -47,30 +67,26 @@ const NO_SCROLLBAR = {
47
67
  track: { glyph: "", style: NULL_STYLE },
48
68
  };
49
69
  export class Viewport {
50
- /**
51
- * What the viewport shows. Replace it to show new content from the same
52
- * scroll position — a view rebuilt every frame keeps one `Viewport`.
53
- */
54
- content;
55
70
  rows;
56
71
  scrollbar;
57
- _offset = 0;
58
- // Nothing rendered, nothing shown: no move can take until the first render.
59
- _extent = { rows: 0, lines: 0 };
60
- _moves = [];
61
- // No range followed yet: NaN equals nothing, so the first `follow` moves.
62
- _followed = { start: NaN, end: NaN, rows: NaN };
63
- constructor(content, options = {}) {
64
- this.content = content;
72
+ _scroll = new State({
73
+ offset: 0,
74
+ // Nothing rendered, nothing shown: no move can take until the first render.
75
+ extent: { rows: 0, lines: 0 },
76
+ // No range followed yet: NaN equals nothing, so the first range followed moves.
77
+ followed: { start: NaN, end: NaN, rows: NaN },
78
+ });
79
+ _moves = new ListState();
80
+ constructor(options = {}) {
65
81
  this.rows = options.rows;
66
82
  this.scrollbar = options.scrollbar ?? NO_SCROLLBAR;
67
83
  }
68
84
  /**
69
- * The first content line the last render showed. A move requested since is
70
- * not reflected until the next render resolves it.
85
+ * The first content line the last frame showed. A move requested since is
86
+ * not reflected until the next frame resolves it.
71
87
  */
72
88
  get offset() {
73
- return this._offset;
89
+ return this._scroll.get().offset;
74
90
  }
75
91
  /** Scroll so `line` is the first line shown. */
76
92
  scrollTo(line) {
@@ -82,12 +98,13 @@ export class Viewport {
82
98
  }
83
99
  /**
84
100
  * Whether `scrollBy(lines)` would move the viewport from where the moves
85
- * queued so far leave it, against the rows and length the last render
101
+ * queued so far leave it, against the rows and length the last frame
86
102
  * found.
87
103
  */
88
104
  canScrollBy(lines) {
89
- const offset = this.resolve(this._extent);
90
- return clampOffset(offset + lines, this._extent) !== offset;
105
+ const { scroll, moves } = this.asked();
106
+ const offset = resolve(scroll.offset, moves, scroll.extent);
107
+ return clampOffset(offset + lines, scroll.extent) !== offset;
91
108
  }
92
109
  /**
93
110
  * Scroll the least distance that shows lines `start` up to but not
@@ -96,23 +113,42 @@ export class Viewport {
96
113
  * the viewport shows its first line at the top.
97
114
  */
98
115
  ensureVisible(start, end) {
99
- this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
116
+ this._moves.push(bringIntoView({ start, end }));
117
+ }
118
+ /**
119
+ * `content` shown through this viewport from its scroll position. What it
120
+ * draws is anchored to this viewport, so the wheel finds it.
121
+ *
122
+ * `follow` is the lines of `content` to keep in view — a selection's — and
123
+ * is brought into view with `ensureVisible` when it differs from the range
124
+ * the last frame followed: when the selection moves, not on every frame,
125
+ * which would undo on its very render each scroll the mouse wheel queues. A
126
+ * followed range the last frame showed whole stays shown when the row count
127
+ * changes: the same lines can fall off the bottom of a viewport that got
128
+ * shorter. One the wheel had scrolled away from stays away. Absent, the
129
+ * range last followed stands.
130
+ *
131
+ * [LAW:effects-at-boundaries] What a frame drawing it resolves becomes the
132
+ * viewport's once that frame has been drawn whole, so it may be built and
133
+ * drawn inside a render — `ListView` draws its items to learn their lines
134
+ * and shows them this way.
135
+ */
136
+ showing(content, follow) {
137
+ return {
138
+ render: (options) => this.draw(content, drawnWhole(content), follow, options),
139
+ measure: (options) => this.measureOf(content, options),
140
+ };
100
141
  }
101
142
  /**
102
- * `ensureVisible(start, end)`, when the range differs from the one the last
103
- * `follow` named. A view rebuilt every frame calls this in every frame with
104
- * its selection's lines, and the selection is brought into view when it
105
- * moves — not on every frame, which would undo on its very render each
106
- * scroll the mouse wheel queues. A followed range the last render showed
107
- * whole stays shown when the row count changes: the same lines can fall off
108
- * the bottom of a viewport that got shorter. One the wheel had scrolled
109
- * away from stays away.
143
+ * `content` shown through this viewport as `showing` shows a renderable,
144
+ * drawing only the lines the frame shows: a frame costs the rows in view,
145
+ * not the content's whole length.
110
146
  */
111
- follow(start, end) {
112
- if (start === this._followed.start && end === this._followed.end)
113
- return;
114
- this._followed = { start, end, rows: NaN };
115
- this.ensureVisible(start, end);
147
+ showingLines(content, follow) {
148
+ return {
149
+ render: (options) => this.draw(content, (o) => content.layout(o), follow, options),
150
+ measure: (options) => this.measureOf(content, options),
151
+ };
116
152
  }
117
153
  /**
118
154
  * The width its content renders at, out of `width`: what the scrollbar's
@@ -125,26 +161,48 @@ export class Viewport {
125
161
  get gutter() {
126
162
  return gutterWidth(this.scrollbar);
127
163
  }
128
- *render(rawOptions) {
129
- const { height, ...options } = withBoundedWidth(rawOptions, this);
164
+ /** The scroll, and the moves queued against it, as they stand. */
165
+ asked() {
166
+ return { scroll: this._scroll.get(), moves: this._moves.get() };
167
+ }
168
+ /**
169
+ * The offset `settled` resolves, with the scroll it leaves kept once the frame
170
+ * is drawn and the `resolved` moves gone from the queue.
171
+ * [LAW:effects-at-boundaries] Kept once the frame is drawn, never while it is.
172
+ * The resolved moves leave by identity, not by count: another frame drawing
173
+ * this viewport in the same pass may already have taken them, and a move
174
+ * queued since stays.
175
+ */
176
+ keep(settled, resolved) {
177
+ // Taken now: the queue is appended to in place, so `resolved` grows with it.
178
+ const taken = new Set(resolved);
179
+ afterPass(() => {
180
+ this._scroll.set(settled.scroll);
181
+ this._moves.set(this._moves.get().filter((move) => !taken.has(move)));
182
+ });
183
+ return settled.offset;
184
+ }
185
+ /** `content`, laid out by `layout`, drawn from the scroll and the moves queued against it, with `follow` followed. */
186
+ *draw(content, layout, follow, rawOptions) {
187
+ const { height, ...options } = withBoundedWidth(rawOptions, { measure: (o) => this.measureOf(content, o) });
130
188
  const gutter = this.gutter;
131
189
  const contentWidth = this.contentWidth(options.maxWidth);
132
- const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
133
- const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
134
- const followed = this._followed;
135
- const wasShown = followed.start >= this._offset && followed.end <= this._offset + this._extent.rows;
136
- this._extent = extent;
137
- if (!Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown) {
138
- this.ensureVisible(followed.start, followed.end);
139
- }
140
- if (!Number.isNaN(followed.start))
141
- this._followed = { ...followed, rows: extent.rows };
142
- this._offset = this.resolve(extent);
143
- this._moves = [];
144
- const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
145
- // [LAW:no-ambient-temporal-coupling] The thumb is drawn from the offset
146
- // this render just resolved, not from `offset` as it stood before it.
147
- const thumb = thumbRows(extent, this._offset);
190
+ const lines = layout({ ...options, maxWidth: contentWidth });
191
+ const asked = this.asked();
192
+ const { scroll, moves } = follow === undefined ? asked : following(asked, follow);
193
+ // [LAW:no-ambient-temporal-coupling] Content handed no column is held, not
194
+ // drawn, so this render learns its budget but not the content's length. It
195
+ // keeps nothing: the offset, the followed range and the queued moves wait
196
+ // for a frame that draws, and this one draws the scrollbar against the
197
+ // length the last one found.
198
+ const held = drawsNothing({ maxWidth: contentWidth });
199
+ const length = held ? scroll.extent.lines : lines.length;
200
+ const extent = { rows: viewRows(height, this.rows, length), lines: length };
201
+ const offset = held ? clampOffset(scroll.offset, extent) : this.keep(settle(scroll, moves, extent), asked.moves);
202
+ const shown = fitHeight(lines.lines(offset, offset + extent.rows), { rows: extent.rows, exact: true });
203
+ // The thumb is drawn from the offset this render resolved, not from the
204
+ // offset the last frame kept.
205
+ const thumb = thumbRows(extent, offset);
148
206
  // The content's cells and the gutter's drawn cells sum to the offer: a
149
207
  // gutter wider than the whole offer is drawn cropped to it.
150
208
  const drawn = Math.min(gutter, options.maxWidth);
@@ -168,26 +226,54 @@ export class Viewport {
168
226
  yield Segment.line();
169
227
  }
170
228
  }
171
- /**
172
- * [LAW:one-source-of-truth] Where the queued moves leave the offset against
173
- * `extent`: what a render commits, and what `canScrollBy` asks from.
174
- * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
175
- * queued moves: content that shrank since the last render pulls it back.
176
- */
177
- resolve(extent) {
178
- return this._moves.reduce((offset, move) => clampOffset(move(offset, extent), extent), clampOffset(this._offset, extent));
179
- }
180
- measure(rawOptions) {
229
+ measureOf(content, rawOptions) {
181
230
  // Its width is its content's and its gutter's. `withCellWidth` and not
182
231
  // `withBoundedWidth`: the bounded parse asks this very method for the
183
232
  // natural width.
184
233
  const options = withCellWidth(rawOptions);
185
234
  const gutter = this.gutter;
186
235
  const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
187
- const content = Measurement.get(inner, this.content);
188
- return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
236
+ const measured = Measurement.get(inner, content);
237
+ return new Measurement(measured.minimum + gutter, measured.maximum + gutter).withMaximum(options.maxWidth);
189
238
  }
190
239
  }
240
+ /** The move that scrolls the least distance showing `range`. */
241
+ const bringIntoView = ({ start, end }) => (offset, { rows }) => Math.min(start, Math.max(offset, end - rows));
242
+ /**
243
+ * `asked` with `range` followed: one that differs from the range last followed
244
+ * becomes the followed range, and a move brings it into view.
245
+ */
246
+ function following(asked, range) {
247
+ const { followed } = asked.scroll;
248
+ if (range.start === followed.start && range.end === followed.end)
249
+ return asked;
250
+ return {
251
+ scroll: { ...asked.scroll, followed: { start: range.start, end: range.end, rows: NaN } },
252
+ moves: [...asked.moves, bringIntoView(range)],
253
+ };
254
+ }
255
+ /**
256
+ * Where a frame that drew the content against `extent` leaves the scroll: a
257
+ * followed range that was in view is kept in view if the rows changed, and the
258
+ * moves resolve into the offset.
259
+ */
260
+ function settle(scroll, moves, extent) {
261
+ const { followed, offset } = scroll;
262
+ const wasShown = followed.start >= offset && followed.end <= offset + scroll.extent.rows;
263
+ const keepShown = !Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown ? [bringIntoView(followed)] : [];
264
+ const settled = resolve(offset, [...moves, ...keepShown], extent);
265
+ const rows = Number.isNaN(followed.start) ? followed.rows : extent.rows;
266
+ return { scroll: { offset: settled, extent, followed: { ...followed, rows } }, offset: settled };
267
+ }
268
+ /**
269
+ * [LAW:one-source-of-truth] Where `moves` leave `offset` against `extent`: what
270
+ * a frame keeps, and what `canScrollBy` asks from.
271
+ * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
272
+ * queued moves: content that shrank since the last frame pulls it back.
273
+ */
274
+ function resolve(offset, moves, extent) {
275
+ return moves.reduce((at, move) => clampOffset(move(at, extent), extent), clampOffset(offset, extent));
276
+ }
191
277
  /**
192
278
  * One viewport per thing a pane shows, named by a key. The same viewport comes
193
279
  * back while the key holds, so its scroll position does too; a new key gets a
@@ -210,11 +296,9 @@ export class ViewportPerKey {
210
296
  return this.held.viewport;
211
297
  }
212
298
  fresh() {
213
- // Empty until a view hands it content: the content is built where the frame is.
214
- return new Viewport(EMPTY, this.options);
299
+ return new Viewport(this.options);
215
300
  }
216
301
  }
217
- const EMPTY = { *render() { } };
218
302
  const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
219
303
  /** `scrollbar` in its own styles, drawn in ASCII. */
220
304
  function asciiScrollbar({ thumb, track }) {
@@ -4,13 +4,25 @@
4
4
  *
5
5
  * A host that builds its cells in templates — cc-candybar's statusline, say —
6
6
  * evaluates each template once per redraw and owns the clock that says when
7
- * that is. So the time is a capability the registration takes, the way
8
- * `richTextFuncs(drawnAt)` takes the drawn depth: `frameTime()` is read once
9
- * per call, when the template is evaluated, and nothing here reads a clock.
7
+ * that is. So the frame is a capability the registration takes, the way
8
+ * `richTextFuncs(drawnAt)` takes the drawn depth: `frame()` is read once per
9
+ * call, when the template is evaluated, and nothing here reads a clock.
10
10
  * [LAW:no-ambient-temporal-coupling] A host hands in the same `t` and gets
11
11
  * the same fragment back; a host that redraws once a second or sixty times
12
12
  * a second registers the same functions.
13
13
  *
14
+ * Every function draws its animation on a `Timeline` (`../core/timeline.ts`)
15
+ * the host hands in as a getter beside the time — playing from host time 0
16
+ * at rate 1, the host's own clock, when it hands none — so a daemon pauses,
17
+ * reverses, scrubs or speeds one session's animations by changing one value
18
+ * it holds, with nothing recompiled, as `paletteFuncs` follows a theme
19
+ * through its getter. The timeline is a map the drawing runs through, not a
20
+ * number the time is turned into first: a frame's stamp says when it changes
21
+ * on the host's clock, which only `Timeline.run` can carry it back onto —
22
+ * paused, never; backwards, at the hold it is about to drop under.
23
+ * [LAW:single-enforcer] `drawing` is the one place the time, the timeline
24
+ * and the reader's preference for motion are read, for every function.
25
+ *
14
26
  * `frames <seconds> <frame> <seconds> <frame> …` is a `FrameSequence`
15
27
  * (`renderables/frames.ts`) in a template: the frame in progress at the
16
28
  * moment, as a fragment at the width of the widest. Its frames arrive as
@@ -25,13 +37,70 @@
25
37
  * end, so the engine's `alternating` gate types every slot and lifts each
26
38
  * string frame to a fragment; the pairing — a trailing hold with no frame or
27
39
  * colour — is parsed in `pairs`, once. [LAW:types-are-the-program]
40
+ *
41
+ * The rest start at a moment the template names, on the timeline's clock —
42
+ * a field of the host's state, the moment a value changed — and each is the
43
+ * animation `Timeline.play(start)` runs, so a start is a timeline like any
44
+ * other and nothing here offsets a time by hand. `tween <seconds> <ease>
45
+ * <start> <from> <to>` is a number eased from one value to another and held
46
+ * (`core/tween.ts`), its ease spelled by name (`parseEase`); `loop <seconds>
47
+ * <start>` is the way through a period, 0 to 1 and round again — two
48
+ * cells started a step apart are a stagger. Each is a number, so it goes
49
+ * wherever a number goes: a colour tween is `ramp (tween …) "linear" 0 $from
50
+ * 1 $to` and a ramp scrolling is `ramp (loop …) …`, because `ColorRamp` is
51
+ * the one place two colours are blended and a colour tween is a two-stop
52
+ * ramp read over time (`ColorRamp.over`). [LAW:one-source-of-truth] A
53
+ * second blender here would be a second answer to what lies between two
54
+ * colours. `glyphs <name> <start> <fragment>` is the fragment through a
55
+ * glyph effect from the catalogue (`GLYPH_EFFECT_CATALOGUE`), at its
56
+ * defaults, before any render (`reglyphText`). Every argument list is an
57
+ * exact arity of typed slots, so a slot of the wrong kind fails at the gate.
58
+ *
59
+ * Every function says when what it drew changes, so a host paints a
60
+ * template while it moves and not after. `frames` and `glyphs` stamp the
61
+ * fragment they draw (`textFrameAt`), covering its own glyphs. A number or a
62
+ * colour carries no cells to stamp, so `tween`, `loop` and `stepped` tell
63
+ * their hold to the frame instead (`TemplateFrame.onHold`), as a placed
64
+ * animation of no cells does (`RenderOptions.onHold`). Nothing here claims
65
+ * a moment it does not know.
28
66
  */
29
67
  import type { FuncMap } from "@promptctl/go-template-js";
68
+ import type { RenderOptions } from "../core/protocol.js";
69
+ import { Timeline } from "../core/timeline.js";
70
+ /** What a registration is drawn on, beside the frame each evaluation is handed. */
71
+ export interface AnimationFuncsOptions {
72
+ /** The timeline to draw on, read once per call; the frame's own time when omitted. */
73
+ readonly timeline?: () => Timeline;
74
+ }
75
+ /**
76
+ * The frame a template is evaluated for, in the fields a render's options
77
+ * carry under the same names: `time` in seconds, `onHold`, told when what a
78
+ * function drew there changes, and the reader's preferences —
79
+ * `reducedMotion`, and `asciiOnly`, which the glyphs an effect picks for
80
+ * itself go through. They arrive with every frame and nowhere else, so a
81
+ * template drawn in an `App` is drawn as the app's preferences say.
82
+ * [LAW:one-source-of-truth]
83
+ *
84
+ * `time` is on the clock `AnimationFuncsOptions.timeline` maps from, and
85
+ * `onHold` hears on that clock. A host outside rich-js hands its own clock's
86
+ * time. Inside a rich-js frame the time is the frame's, which its host's
87
+ * timeline has already mapped, so a template drawn there registers no
88
+ * `timeline` and is handed `templateFrame(options)`.
89
+ */
90
+ export interface TemplateFrame extends Pick<RenderOptions, "onHold" | "reducedMotion" | "asciiOnly"> {
91
+ readonly time: number;
92
+ }
93
+ /** The frame a renderable is drawn in, as the frame a template it evaluates there is drawn for. */
94
+ export declare function templateFrame(options: RenderOptions): TemplateFrame;
30
95
  /**
31
- * Register `frames` and `stepped`, each drawn for the moment `frameTime`
32
- * reports when the template is evaluated: seconds on the host's own
33
- * timeline, from whatever zero it counts from. The functions exist whatever
34
- * `frameTime` returns, so a template compiles once and is evaluated at every
35
- * moment the host draws.
96
+ * Register `frames`, `stepped`, `tween`, `loop` and `glyphs`, each drawn for
97
+ * the frame `frame` reports when the template is evaluated — its time in
98
+ * seconds, from whatever zero the host counts from — through
99
+ * `options.timeline`, and each telling that frame when what it drew changes.
100
+ * The functions exist whatever `frame` returns, so a template compiles once
101
+ * and is evaluated at every moment the host draws. In a frame that says
102
+ * `reducedMotion` each draws its animation reduced (`Animation.reduced`): a
103
+ * loop at its start, a one-shot cut from where it starts to where it arrives
104
+ * when it starts.
36
105
  */
37
- export declare function animationFuncs(frameTime: () => number): FuncMap;
106
+ export declare function animationFuncs(frame: () => TemplateFrame, options?: AnimationFuncsOptions): FuncMap;
@@ -4,13 +4,25 @@
4
4
  *
5
5
  * A host that builds its cells in templates — cc-candybar's statusline, say —
6
6
  * evaluates each template once per redraw and owns the clock that says when
7
- * that is. So the time is a capability the registration takes, the way
8
- * `richTextFuncs(drawnAt)` takes the drawn depth: `frameTime()` is read once
9
- * per call, when the template is evaluated, and nothing here reads a clock.
7
+ * that is. So the frame is a capability the registration takes, the way
8
+ * `richTextFuncs(drawnAt)` takes the drawn depth: `frame()` is read once per
9
+ * call, when the template is evaluated, and nothing here reads a clock.
10
10
  * [LAW:no-ambient-temporal-coupling] A host hands in the same `t` and gets
11
11
  * the same fragment back; a host that redraws once a second or sixty times
12
12
  * a second registers the same functions.
13
13
  *
14
+ * Every function draws its animation on a `Timeline` (`../core/timeline.ts`)
15
+ * the host hands in as a getter beside the time — playing from host time 0
16
+ * at rate 1, the host's own clock, when it hands none — so a daemon pauses,
17
+ * reverses, scrubs or speeds one session's animations by changing one value
18
+ * it holds, with nothing recompiled, as `paletteFuncs` follows a theme
19
+ * through its getter. The timeline is a map the drawing runs through, not a
20
+ * number the time is turned into first: a frame's stamp says when it changes
21
+ * on the host's clock, which only `Timeline.run` can carry it back onto —
22
+ * paused, never; backwards, at the hold it is about to drop under.
23
+ * [LAW:single-enforcer] `drawing` is the one place the time, the timeline
24
+ * and the reader's preference for motion are read, for every function.
25
+ *
14
26
  * `frames <seconds> <frame> <seconds> <frame> …` is a `FrameSequence`
15
27
  * (`renderables/frames.ts`) in a template: the frame in progress at the
16
28
  * moment, as a fragment at the width of the widest. Its frames arrive as
@@ -25,11 +37,49 @@
25
37
  * end, so the engine's `alternating` gate types every slot and lifts each
26
38
  * string frame to a fragment; the pairing — a trailing hold with no frame or
27
39
  * colour — is parsed in `pairs`, once. [LAW:types-are-the-program]
40
+ *
41
+ * The rest start at a moment the template names, on the timeline's clock —
42
+ * a field of the host's state, the moment a value changed — and each is the
43
+ * animation `Timeline.play(start)` runs, so a start is a timeline like any
44
+ * other and nothing here offsets a time by hand. `tween <seconds> <ease>
45
+ * <start> <from> <to>` is a number eased from one value to another and held
46
+ * (`core/tween.ts`), its ease spelled by name (`parseEase`); `loop <seconds>
47
+ * <start>` is the way through a period, 0 to 1 and round again — two
48
+ * cells started a step apart are a stagger. Each is a number, so it goes
49
+ * wherever a number goes: a colour tween is `ramp (tween …) "linear" 0 $from
50
+ * 1 $to` and a ramp scrolling is `ramp (loop …) …`, because `ColorRamp` is
51
+ * the one place two colours are blended and a colour tween is a two-stop
52
+ * ramp read over time (`ColorRamp.over`). [LAW:one-source-of-truth] A
53
+ * second blender here would be a second answer to what lies between two
54
+ * colours. `glyphs <name> <start> <fragment>` is the fragment through a
55
+ * glyph effect from the catalogue (`GLYPH_EFFECT_CATALOGUE`), at its
56
+ * defaults, before any render (`reglyphText`). Every argument list is an
57
+ * exact arity of typed slots, so a slot of the wrong kind fails at the gate.
58
+ *
59
+ * Every function says when what it drew changes, so a host paints a
60
+ * template while it moves and not after. `frames` and `glyphs` stamp the
61
+ * fragment they draw (`textFrameAt`), covering its own glyphs. A number or a
62
+ * colour carries no cells to stamp, so `tween`, `loop` and `stepped` tell
63
+ * their hold to the frame instead (`TemplateFrame.onHold`), as a placed
64
+ * animation of no cells does (`RenderOptions.onHold`). Nothing here claims
65
+ * a moment it does not know.
28
66
  */
29
- import { beat } from "../core/easing.js";
30
- import { FrameSequence } from "../renderables/frames.js";
67
+ import { Animation, frameTimeOf, holdOf, shownAt } from "../core/animation.js";
68
+ import { Phase, beat, finiteTime, parseEase } from "../core/easing.js";
69
+ import { Timeline } from "../core/timeline.js";
70
+ import { tween } from "../core/tween.js";
71
+ import { cellSeed } from "../renderables/effect.js";
72
+ import { reglyphText } from "../renderables/glyph-effect.js";
73
+ import { GLYPH_EFFECT_CATALOGUE } from "../renderables/glyph-effects.js";
74
+ import { FrameSequence, textFrameAt } from "../renderables/frames.js";
31
75
  import { asColor } from "./color-funcs.js";
32
76
  import { asFragment } from "./helpers.js";
77
+ /** The frame a renderable is drawn in, as the frame a template it evaluates there is drawn for. */
78
+ export function templateFrame(options) {
79
+ return { time: frameTimeOf(options), onHold: options.onHold, reducedMotion: options.reducedMotion, asciiOnly: options.asciiOnly };
80
+ }
81
+ /** The host's own clock as a timeline: host time 0 at animation time 0, at rate 1. */
82
+ const HOST_CLOCK = Timeline.play(0);
33
83
  /**
34
84
  * The `(seconds, value)` pairs a call spells, each value read through
35
85
  * `value`. The gate has typed every even slot as a float and every odd one
@@ -54,17 +104,59 @@ function pairs(what, tail, value) {
54
104
  return paired;
55
105
  }
56
106
  /**
57
- * Register `frames` and `stepped`, each drawn for the moment `frameTime`
58
- * reports when the template is evaluated: seconds on the host's own
59
- * timeline, from whatever zero it counts from. The functions exist whatever
60
- * `frameTime` returns, so a template compiles once and is evaluated at every
61
- * moment the host draws.
107
+ * Every glyph effect in the catalogue by the name a template spells it
108
+ * with, a transition as the effect it runs. [LAW:types-are-the-program]
109
+ * Keyed by both name types, so a name added to the catalogue does not
110
+ * compile until it is spelled here too.
111
+ */
112
+ const GLYPH_EFFECTS = {
113
+ ...GLYPH_EFFECT_CATALOGUE.loops,
114
+ typewriter: (element) => GLYPH_EFFECT_CATALOGUE.transitions.typewriter(element).effect,
115
+ scramble: (element) => GLYPH_EFFECT_CATALOGUE.transitions.scramble(element).effect,
116
+ backspace: (element) => GLYPH_EFFECT_CATALOGUE.transitions.backspace(element).effect,
117
+ scrambleOut: (element) => GLYPH_EFFECT_CATALOGUE.transitions.scrambleOut(element).effect,
118
+ };
119
+ /**
120
+ * The gate a spelled glyph effect crosses: the name in, the effect's maker
121
+ * out, or a refusal naming every legal one. [LAW:parse-dont-validate]
122
+ */
123
+ function glyphEffect(name) {
124
+ if (!Object.hasOwn(GLYPH_EFFECTS, name)) {
125
+ throw new RangeError(`unknown glyph effect ${JSON.stringify(name)}; expected one of ${Object.keys(GLYPH_EFFECTS).map((n) => JSON.stringify(n)).join(", ")}`);
126
+ }
127
+ return GLYPH_EFFECTS[name];
128
+ }
129
+ /**
130
+ * Register `frames`, `stepped`, `tween`, `loop` and `glyphs`, each drawn for
131
+ * the frame `frame` reports when the template is evaluated — its time in
132
+ * seconds, from whatever zero the host counts from — through
133
+ * `options.timeline`, and each telling that frame when what it drew changes.
134
+ * The functions exist whatever `frame` returns, so a template compiles once
135
+ * and is evaluated at every moment the host draws. In a frame that says
136
+ * `reducedMotion` each draws its animation reduced (`Animation.reduced`): a
137
+ * loop at its start, a one-shot cut from where it starts to where it arrives
138
+ * when it starts.
62
139
  */
63
- export function animationFuncs(frameTime) {
140
+ export function animationFuncs(frame, options = {}) {
141
+ const timeline = options.timeline ?? (() => HOST_CLOCK);
142
+ // `animation` on this call's timeline, and the time of `at`, the frame it
143
+ // is drawn in, read once a call so a value and its hold are one frame's.
144
+ const drawing = (what, animation, at) => [Timeline.run(timeline(), animation), finiteTime(what, at.time)];
145
+ // What a value animation shows now, under the reader's preference, its
146
+ // hold told to the frame: a value has no cells to carry it.
147
+ const now = (what, animation) => {
148
+ const at = frame();
149
+ const [drawn, t] = drawing(what, animation, at);
150
+ const shown = shownAt(drawn, t, at);
151
+ at.onHold?.(holdOf(shown, t));
152
+ return shown.value;
153
+ };
154
+ const startedAt = (start, animation) => Timeline.run(Timeline.play(start), animation);
64
155
  const framesFunc = {
65
156
  fn: ((...tail) => {
66
157
  const frames = pairs("frame", tail, asFragment).map(({ seconds, value }) => ({ seconds, text: value }));
67
- return new FrameSequence(frames).text(frameTime());
158
+ const at = frame();
159
+ return textFrameAt(...drawing("frames", new FrameSequence(frames), at), at);
68
160
  }),
69
161
  argTypes: ["float", "liftable"],
70
162
  arity: { kind: "alternating", minimum: 2 },
@@ -73,11 +165,37 @@ export function animationFuncs(frameTime) {
73
165
  const steppedFunc = {
74
166
  fn: ((...tail) => {
75
167
  const steps = pairs("color", tail, (v) => asColor(v, "stepped"));
76
- return steps[beat(steps.map((step) => step.seconds)).at(frameTime())].value.hex;
168
+ return steps[now("stepped", beat(steps.map((step) => step.seconds)))].value.hex;
77
169
  }),
78
170
  argTypes: ["float", "string"],
79
171
  arity: { kind: "alternating", minimum: 2 },
80
172
  returnType: "string",
81
173
  };
82
- return { frames: framesFunc, stepped: steppedFunc };
174
+ const tweenFunc = {
175
+ fn: ((seconds, ease, start, from, to) => now("tween", startedAt(start, tween(from, to, { seconds, ease: parseEase(ease) })))),
176
+ argTypes: ["float", "string", "float", "float", "float"],
177
+ arity: { kind: "exact" },
178
+ returnType: "float",
179
+ };
180
+ const loopFunc = {
181
+ fn: ((seconds, start) => now("loop", startedAt(start, Animation.continuous(Phase.loop(seconds))))),
182
+ argTypes: ["float", "float"],
183
+ arity: { kind: "exact" },
184
+ returnType: "float",
185
+ };
186
+ const glyphsFunc = {
187
+ fn: ((name, start, child) => {
188
+ const make = glyphEffect(name);
189
+ const fragment = asFragment(child);
190
+ // One text, one seed: the element is named by what it says.
191
+ const key = fragment.plain;
192
+ const at = frame();
193
+ const reglyphed = reglyphText(fragment, make({ z: cellSeed(key, 0, 0) }), { key, asciiOnly: at.asciiOnly === true });
194
+ return textFrameAt(...drawing("glyphs", startedAt(start, reglyphed), at), at);
195
+ }),
196
+ argTypes: ["string", "float", "liftable"],
197
+ arity: { kind: "exact" },
198
+ returnType: "T",
199
+ };
200
+ return { frames: framesFunc, stepped: steppedFunc, tween: tweenFunc, loop: loopFunc, glyphs: glyphsFunc };
83
201
  }
@@ -14,10 +14,10 @@
14
14
  * - `paletteFuncs(getPalette)` — `color` and `ramp`, the two functions that
15
15
  * resolve a palette name. This is the *only* registration that knows a
16
16
  * palette exists. [LAW:one-way-deps]
17
- * - `animationFuncs(frameTime)` — `frames` and `stepped`, a fragment and a
18
- * colour drawn for a moment. It is the only registration that knows the
19
- * template is evaluated at a time, and the time is the host's to hand in
20
- * (`./animation-funcs.ts`).
17
+ * - `animationFuncs(frame)` — `frames`, `stepped`, `tween`, `loop` and
18
+ * `glyphs`, drawn for a moment on a timeline. It is the only registration
19
+ * that knows the template is evaluated at a time, and the time and the
20
+ * timeline are the host's to hand in (`./animation-funcs.ts`).
21
21
  *
22
22
  * `createRichTextEngine()` wires up `richTextFuncs()` only — it cannot supply a
23
23
  * palette. Consumers that name theme colours call `paletteFuncs(getPalette)`
@@ -43,7 +43,7 @@ import type { RenderOptions } from "../core/protocol.js";
43
43
  import { type DrawnAt } from "./color-funcs.js";
44
44
  export { paletteFuncs } from "./palette-funcs.js";
45
45
  export { colorFuncs, readableOnFunc, type DrawnAt } from "./color-funcs.js";
46
- export { animationFuncs } from "./animation-funcs.js";
46
+ export { animationFuncs, templateFrame, type AnimationFuncsOptions, type TemplateFrame } from "./animation-funcs.js";
47
47
  /**
48
48
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
49
49
  * colour math, text attributes, and the `link` cell-splitter. Nothing here