@promptctl/rich-js 0.24.1 → 0.25.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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +24 -33
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +98 -29
  47. package/dist/core/strip.js +211 -150
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +158 -63
  73. package/dist/renderables/effect.js +378 -155
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -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
@@ -24,6 +28,13 @@
24
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
@@ -33,7 +44,25 @@
33
44
  */
34
45
  import { Segment } from "../core/segment.js";
35
46
  import type { Style } from "../core/style.js";
36
- import type { Composite, Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
47
+ import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
48
+ /** Lines `start` up to but not including `end` of the content. */
49
+ interface Range {
50
+ readonly start: number;
51
+ readonly end: number;
52
+ }
53
+ /**
54
+ * Content a viewport shows by the line: laid out at a width, it says how many
55
+ * lines it is and draws only the ones asked for. What a view too long to draw
56
+ * whole every frame — a textarea's rows — is shown as (`showingLines`).
57
+ */
58
+ export interface Lines extends Measurable {
59
+ layout(options: RenderOptions): LaidLines;
60
+ }
61
+ /** Content laid out at one width: its length, and its lines `start` up to but not including `end`. */
62
+ export interface LaidLines {
63
+ readonly length: number;
64
+ lines(start: number, end: number): Segment[][];
65
+ }
37
66
  /** How one part of a scrollbar draws each row it covers. */
38
67
  export interface ScrollbarPart {
39
68
  readonly glyph: string;
@@ -60,22 +89,15 @@ export interface ViewportOptions {
60
89
  /** A scrollbar down the right edge. Absent, the content has the full width. */
61
90
  scrollbar?: Scrollbar;
62
91
  }
63
- export declare class Viewport implements Renderable, Measurable, Scrollable, Composite {
64
- /**
65
- * What the viewport shows. Replace it to show new content from the same
66
- * scroll position — a view rebuilt every frame keeps one `Viewport`.
67
- */
68
- content: Renderable;
92
+ export declare class Viewport implements Scrollable {
69
93
  readonly rows: number | undefined;
70
94
  readonly scrollbar: Scrollbar;
71
- private _offset;
72
- private _extent;
73
- private _moves;
74
- private _followed;
75
- constructor(content: Renderable, options?: ViewportOptions);
95
+ private readonly _scroll;
96
+ private readonly _moves;
97
+ constructor(options?: ViewportOptions);
76
98
  /**
77
- * The first content line the last render showed. A move requested since is
78
- * not reflected until the next render resolves it.
99
+ * The first content line the last frame showed. A move requested since is
100
+ * not reflected until the next frame resolves it.
79
101
  */
80
102
  get offset(): number;
81
103
  /** Scroll so `line` is the first line shown. */
@@ -84,7 +106,7 @@ export declare class Viewport implements Renderable, Measurable, Scrollable, Com
84
106
  scrollBy(lines: number): void;
85
107
  /**
86
108
  * Whether `scrollBy(lines)` would move the viewport from where the moves
87
- * queued so far leave it, against the rows and length the last render
109
+ * queued so far leave it, against the rows and length the last frame
88
110
  * found.
89
111
  */
90
112
  canScrollBy(lines: number): boolean;
@@ -96,16 +118,30 @@ export declare class Viewport implements Renderable, Measurable, Scrollable, Com
96
118
  */
97
119
  ensureVisible(start: number, end: number): void;
98
120
  /**
99
- * `ensureVisible(start, end)`, when the range differs from the one the last
100
- * `follow` named. A view rebuilt every frame calls this in every frame with
101
- * its selection's lines, and the selection is brought into view when it
102
- * moves — not on every frame, which would undo on its very render each
103
- * scroll the mouse wheel queues. A followed range the last render showed
104
- * whole stays shown when the row count changes: the same lines can fall off
105
- * the bottom of a viewport that got shorter. One the wheel had scrolled
106
- * away from stays away.
121
+ * `content` shown through this viewport from its scroll position. What it
122
+ * draws is anchored to this viewport, so the wheel finds it.
123
+ *
124
+ * `follow` is the lines of `content` to keep in view — a selection's — and
125
+ * is brought into view with `ensureVisible` when it differs from the range
126
+ * the last frame followed: when the selection moves, not on every frame,
127
+ * which would undo on its very render each scroll the mouse wheel queues. A
128
+ * followed range the last frame showed whole stays shown when the row count
129
+ * changes: the same lines can fall off the bottom of a viewport that got
130
+ * shorter. One the wheel had scrolled away from stays away. Absent, the
131
+ * range last followed stands.
132
+ *
133
+ * [LAW:effects-at-boundaries] What a frame drawing it resolves becomes the
134
+ * viewport's once that frame has been drawn whole, so it may be built and
135
+ * drawn inside a render — `ListView` draws its items to learn their lines
136
+ * and shows them this way.
137
+ */
138
+ showing(content: Renderable, follow?: Range): Renderable & Measurable;
139
+ /**
140
+ * `content` shown through this viewport as `showing` shows a renderable,
141
+ * drawing only the lines the frame shows: a frame costs the rows in view,
142
+ * not the content's whole length.
107
143
  */
108
- follow(start: number, end: number): void;
144
+ showingLines(content: Lines, follow?: Range): Renderable & Measurable;
109
145
  /**
110
146
  * The width its content renders at, out of `width`: what the scrollbar's
111
147
  * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
@@ -113,26 +149,20 @@ export declare class Viewport implements Renderable, Measurable, Scrollable, Com
113
149
  contentWidth(width: number): number;
114
150
  /** The cells the scrollbar's gutter takes beside the content. */
115
151
  get gutter(): number;
116
- contents(): Iterable<Renderable>;
117
- render(rawOptions: RenderOptions): Iterable<Segment>;
118
- /**
119
- * What a render that drew the content found, made the viewport's: `extent`
120
- * is the rows and length the next move is clamped against, a followed range
121
- * that was in view is kept in view if the rows changed, and the queued moves
122
- * resolve into the offset this returns.
123
- */
124
- private commit;
152
+ /** The scroll, and the moves queued against it, as they stand. */
153
+ private asked;
125
154
  /**
126
- * [LAW:one-source-of-truth] Where the queued moves leave the offset against
127
- * `extent`: what a render commits, and what `canScrollBy` asks from.
128
- * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
129
- * queued moves: content that shrank since the last render pulls it back.
155
+ * The offset `settled` resolves, with the scroll it leaves kept once the frame
156
+ * is drawn and the `resolved` moves gone from the queue.
157
+ * [LAW:effects-at-boundaries] Kept once the frame is drawn, never while it is.
158
+ * The resolved moves leave by identity, not by count: another frame drawing
159
+ * this viewport in the same pass may already have taken them, and a move
160
+ * queued since stays.
130
161
  */
131
- private resolve;
132
- measure(rawOptions: RenderOptions): {
133
- minimum: number;
134
- maximum: number;
135
- };
162
+ private keep;
163
+ /** `content`, laid out by `layout`, drawn from the scroll and the moves queued against it, with `follow` followed. */
164
+ private draw;
165
+ private measureOf;
136
166
  }
137
167
  /**
138
168
  * One viewport per thing a pane shows, named by a key. The same viewport comes
@@ -150,3 +180,4 @@ export declare class ViewportPerKey {
150
180
  of(key: string): Viewport;
151
181
  private fresh;
152
182
  }
183
+ export {};
@@ -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
@@ -24,6 +28,13 @@
24
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
@@ -36,7 +47,15 @@ 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
49
  import { drawable, drawsNothing, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
39
- import { render } 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
+ }
40
59
  /** A heavy line for the thumb on a light line for the track. */
41
60
  export const SCROLLBAR = {
42
61
  thumb: { glyph: "┃", style: "scrollbar.thumb" },
@@ -48,30 +67,26 @@ const NO_SCROLLBAR = {
48
67
  track: { glyph: "", style: NULL_STYLE },
49
68
  };
50
69
  export class Viewport {
51
- /**
52
- * What the viewport shows. Replace it to show new content from the same
53
- * scroll position — a view rebuilt every frame keeps one `Viewport`.
54
- */
55
- content;
56
70
  rows;
57
71
  scrollbar;
58
- _offset = 0;
59
- // Nothing rendered, nothing shown: no move can take until the first render.
60
- _extent = { rows: 0, lines: 0 };
61
- _moves = [];
62
- // No range followed yet: NaN equals nothing, so the first `follow` moves.
63
- _followed = { start: NaN, end: NaN, rows: NaN };
64
- constructor(content, options = {}) {
65
- 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 = {}) {
66
81
  this.rows = options.rows;
67
82
  this.scrollbar = options.scrollbar ?? NO_SCROLLBAR;
68
83
  }
69
84
  /**
70
- * The first content line the last render showed. A move requested since is
71
- * 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.
72
87
  */
73
88
  get offset() {
74
- return this._offset;
89
+ return this._scroll.get().offset;
75
90
  }
76
91
  /** Scroll so `line` is the first line shown. */
77
92
  scrollTo(line) {
@@ -83,12 +98,13 @@ export class Viewport {
83
98
  }
84
99
  /**
85
100
  * Whether `scrollBy(lines)` would move the viewport from where the moves
86
- * 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
87
102
  * found.
88
103
  */
89
104
  canScrollBy(lines) {
90
- const offset = this.resolve(this._extent);
91
- 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;
92
108
  }
93
109
  /**
94
110
  * Scroll the least distance that shows lines `start` up to but not
@@ -97,23 +113,42 @@ export class Viewport {
97
113
  * the viewport shows its first line at the top.
98
114
  */
99
115
  ensureVisible(start, end) {
100
- this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
116
+ this._moves.push(bringIntoView({ start, end }));
101
117
  }
102
118
  /**
103
- * `ensureVisible(start, end)`, when the range differs from the one the last
104
- * `follow` named. A view rebuilt every frame calls this in every frame with
105
- * its selection's lines, and the selection is brought into view when it
106
- * moves — not on every frame, which would undo on its very render each
107
- * scroll the mouse wheel queues. A followed range the last render showed
108
- * whole stays shown when the row count changes: the same lines can fall off
109
- * the bottom of a viewport that got shorter. One the wheel had scrolled
110
- * away from stays away.
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.
111
135
  */
112
- follow(start, end) {
113
- if (start === this._followed.start && end === this._followed.end)
114
- return;
115
- this._followed = { start, end, rows: NaN };
116
- this.ensureVisible(start, end);
136
+ showing(content, follow) {
137
+ return {
138
+ render: (options) => this.draw(content, drawnWhole(content), follow, options),
139
+ measure: (options) => this.measureOf(content, options),
140
+ };
141
+ }
142
+ /**
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.
146
+ */
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
+ };
117
152
  }
118
153
  /**
119
154
  * The width its content renders at, out of `width`: what the scrollbar's
@@ -126,26 +161,47 @@ export class Viewport {
126
161
  get gutter() {
127
162
  return gutterWidth(this.scrollbar);
128
163
  }
129
- *contents() {
130
- yield this.content;
164
+ /** The scroll, and the moves queued against it, as they stand. */
165
+ asked() {
166
+ return { scroll: this._scroll.get(), moves: this._moves.get() };
131
167
  }
132
- *render(rawOptions) {
133
- const { height, ...options } = withBoundedWidth(rawOptions, this);
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) });
134
188
  const gutter = this.gutter;
135
189
  const contentWidth = this.contentWidth(options.maxWidth);
136
- const lines = Segment.splitLines(render(this.content, { ...options, maxWidth: contentWidth }));
190
+ const lines = layout({ ...options, maxWidth: contentWidth });
191
+ const asked = this.asked();
192
+ const { scroll, moves } = follow === undefined ? asked : following(asked, follow);
137
193
  // [LAW:no-ambient-temporal-coupling] Content handed no column is held, not
138
194
  // drawn, so this render learns its budget but not the content's length. It
139
- // commits nothing: the offset, the followed range and the queued moves wait
140
- // for a render that draws, and this one draws the scrollbar against the
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
141
197
  // length the last one found.
142
198
  const held = drawsNothing({ maxWidth: contentWidth });
143
- const length = held ? this._extent.lines : lines.length;
199
+ const length = held ? scroll.extent.lines : lines.length;
144
200
  const extent = { rows: viewRows(height, this.rows, length), lines: length };
145
- const offset = held ? clampOffset(this._offset, extent) : this.commit(extent);
146
- const shown = fitHeight(lines.slice(offset, offset + extent.rows), { rows: extent.rows, exact: true });
147
- // The thumb is drawn from the offset this render resolved, not from
148
- // `offset` as it stood before it.
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.
149
205
  const thumb = thumbRows(extent, offset);
150
206
  // The content's cells and the gutter's drawn cells sum to the offer: a
151
207
  // gutter wider than the whole offer is drawn cropped to it.
@@ -170,45 +226,54 @@ export class Viewport {
170
226
  yield Segment.line();
171
227
  }
172
228
  }
173
- /**
174
- * What a render that drew the content found, made the viewport's: `extent`
175
- * is the rows and length the next move is clamped against, a followed range
176
- * that was in view is kept in view if the rows changed, and the queued moves
177
- * resolve into the offset this returns.
178
- */
179
- commit(extent) {
180
- const followed = this._followed;
181
- const wasShown = followed.start >= this._offset && followed.end <= this._offset + this._extent.rows;
182
- this._extent = extent;
183
- if (!Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown) {
184
- this.ensureVisible(followed.start, followed.end);
185
- }
186
- if (!Number.isNaN(followed.start))
187
- this._followed = { ...followed, rows: extent.rows };
188
- this._offset = this.resolve(extent);
189
- this._moves = [];
190
- return this._offset;
191
- }
192
- /**
193
- * [LAW:one-source-of-truth] Where the queued moves leave the offset against
194
- * `extent`: what a render commits, and what `canScrollBy` asks from.
195
- * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
196
- * queued moves: content that shrank since the last render pulls it back.
197
- */
198
- resolve(extent) {
199
- return this._moves.reduce((offset, move) => clampOffset(move(offset, extent), extent), clampOffset(this._offset, extent));
200
- }
201
- measure(rawOptions) {
229
+ measureOf(content, rawOptions) {
202
230
  // Its width is its content's and its gutter's. `withCellWidth` and not
203
231
  // `withBoundedWidth`: the bounded parse asks this very method for the
204
232
  // natural width.
205
233
  const options = withCellWidth(rawOptions);
206
234
  const gutter = this.gutter;
207
235
  const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
208
- const content = Measurement.get(inner, this.content);
209
- 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);
210
238
  }
211
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
+ }
212
277
  /**
213
278
  * One viewport per thing a pane shows, named by a key. The same viewport comes
214
279
  * back while the key holds, so its scroll position does too; a new key gets a
@@ -231,11 +296,9 @@ export class ViewportPerKey {
231
296
  return this.held.viewport;
232
297
  }
233
298
  fresh() {
234
- // Empty until a view hands it content: the content is built where the frame is.
235
- return new Viewport(EMPTY, this.options);
299
+ return new Viewport(this.options);
236
300
  }
237
301
  }
238
- const EMPTY = { *render() { } };
239
302
  const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
240
303
  /** `scrollbar` in its own styles, drawn in ASCII. */
241
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;