@promptctl/rich-js 0.24.1 → 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 (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 +21 -29
  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 +13 -16
  47. package/dist/core/strip.js +119 -110
  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 +132 -54
  73. package/dist/renderables/effect.js +188 -115
  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
@@ -32,11 +32,25 @@
32
32
  * why it is a search and not the ulp walk a sum on one clock takes. A value
33
33
  * moving at the mapped time changes at the host time itself.
34
34
  *
35
+ * A stagger — a Group's children pulsing in sequence, a light walking down
36
+ * a Table's rows — is the same map once per child: `stagger` runs each
37
+ * child's animation on a timeline playing from its index times the delay,
38
+ * and the group changes when the soonest child does, each child's answer
39
+ * carried onto the group's clock by `run`. Before its turn a child is asked
40
+ * for a time before 0, which is its own business: a loop is shifted in
41
+ * phase. Tree, Table, Group and Layout take the children's frames as they
42
+ * take any renderables, through `Animation.map`, so none of them knows a
43
+ * stagger exists. [LAW:composability] A lifecycle is not staggered here: it
44
+ * carries its turn as `entered`, on the host's clock its `leave` and `gone`
45
+ * take, so moving it onto a shifted clock would lose them
46
+ * ([LAW:one-source-of-truth] for when it arrives).
47
+ *
35
48
  * [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. The host
36
49
  * time is an argument everywhere, and a timeline is built from one the host
37
50
  * hands in, so the same host time maps to the same animation time at one
38
51
  * sample a second or sixty.
39
52
  */
53
+ import { Animation } from "./animation.js";
40
54
  import { firstReaching } from "./easing.js";
41
55
  /** Throws, naming `what`, unless `seconds` is a finite number of seconds. */
42
56
  function finiteSeconds(what, seconds) {
@@ -81,7 +95,7 @@ function played(animation, { origin, time, rate }) {
81
95
  const sum = origin + (edge - time) / rate;
82
96
  return Number.isFinite(sum) ? firstReaching(sum, (host) => past(mapped(host), edge)) : sum;
83
97
  };
84
- return {
98
+ return Animation.of({
85
99
  at: (host) => animation.at(mapped(hostTime(host))),
86
100
  nextChange(host) {
87
101
  const h = hostTime(host);
@@ -95,7 +109,8 @@ function played(animation, { origin, time, rate }) {
95
109
  const edge = behind(t);
96
110
  return edge === t ? h : Math.min(h, hostOf(edge));
97
111
  },
98
- };
112
+ reduced: () => played(animation.reduced(), { playing: true, origin, time, rate }),
113
+ });
99
114
  }
100
115
  export const Timeline = {
101
116
  /** Playing from host time `origin`, at which the animation time is 0, at `rate` animation seconds a host second. */
@@ -128,17 +143,38 @@ export const Timeline = {
128
143
  * host time to, changing at the host moment the mapped time reaches the
129
144
  * animation's own answer — backwards, when it drops under where the value
130
145
  * began. Paused, the value never changes and never did. The value is the
131
- * animation's own, on no clock: what a drawn value's cells say about when
132
- * they change is stamped by whoever draws it at a host time
133
- * (`../renderables/frames.ts`, `frameAt`), never carried in from here.
146
+ * animation's own, on no clock: placed, it is stamped at the frame's time
147
+ * (`placedRender`, `./animation.ts`) with this animation's answers, so a
148
+ * value's contents follow the frame's clock and not this map.
149
+ * Reduced, it is the animation reduced on the same timeline: what shows
150
+ * still follows the timeline, and only the motion is gone.
134
151
  */
135
152
  run(timeline, animation) {
136
153
  if (timeline.playing)
137
154
  return played(animation, timeline);
138
- return {
155
+ return Animation.of({
139
156
  at: (host) => animation.at(Timeline.at(timeline, host)),
140
157
  nextChange: (host) => (hostTime(host), Number.POSITIVE_INFINITY),
141
158
  lastChange: (host) => (hostTime(host), Number.NEGATIVE_INFINITY),
142
- };
159
+ reduced: () => Timeline.run(timeline, animation.reduced()),
160
+ });
161
+ },
162
+ /**
163
+ * Every one of `animations` at once, each on a timeline playing from its
164
+ * index times `delay` seconds after the earliest, their values in order, on
165
+ * a clock of the stagger's own from 0. It changes when the soonest of them
166
+ * does, on that clock (`Animation.all` of `run`s). A delay of 0 starts them
167
+ * together and a negative one starts the last at 0 and the first last.
168
+ * Reduced, the offsets are the cascade's choreography, which is motion: every
169
+ * child reduced at once, so from 0 everyone has arrived.
170
+ */
171
+ stagger(animations, delay) {
172
+ finiteSeconds("Timeline.stagger delay", delay);
173
+ const span = finiteSeconds("Timeline.stagger delay times the last index", Math.max(0, animations.length - 1) * delay);
174
+ // [LAW:dataflow-not-control-flow] Whichever way the cascade runs, every
175
+ // origin is shifted by the lowest of them, so the earliest child plays from 0.
176
+ const earliest = Math.min(0, span);
177
+ const cascade = Animation.all(animations.map((animation, i) => Timeline.run(Timeline.play(i * delay - earliest), animation)));
178
+ return Animation.of({ ...cascade, reduced: () => Animation.all(animations.map((animation) => animation.reduced())) });
143
179
  },
144
180
  };
@@ -30,7 +30,7 @@
30
30
  * OKLCH, and a colour tween is a two-stop ramp over its seconds, not a
31
31
  * second blender. [LAW:one-source-of-truth]
32
32
  *
33
- * Tier 2 of `src/core/`: `./easing.ts` and `./animation.ts`, nothing else.
33
+ * Tier 6 of `src/core/`: `./easing.ts` and `./animation.ts`, nothing else.
34
34
  * [LAW:one-way-deps]
35
35
  */
36
36
  import { Animation } from "./animation.js";
@@ -30,7 +30,7 @@
30
30
  * OKLCH, and a colour tween is a two-stop ramp over its seconds, not a
31
31
  * second blender. [LAW:one-source-of-truth]
32
32
  *
33
- * Tier 2 of `src/core/`: `./easing.ts` and `./animation.ts`, nothing else.
33
+ * Tier 6 of `src/core/`: `./easing.ts` and `./animation.ts`, nothing else.
34
34
  * [LAW:one-way-deps]
35
35
  */
36
36
  import { Animation } from "./animation.js";
@@ -55,7 +55,7 @@ export function tween(from, to, timing) {
55
55
  const phase = Phase.once(0, seconds);
56
56
  if (from === to)
57
57
  return Animation.constant(from);
58
- return {
58
+ return Animation.of({
59
59
  at(t) {
60
60
  const p = ease(phase(t));
61
61
  return (1 - p) * from + p * to;
@@ -65,5 +65,7 @@ export function tween(from, to, timing) {
65
65
  // `seconds`, or always was before it began.
66
66
  nextChange: (t) => (t < 0 ? 0 : t < seconds ? t : Number.POSITIVE_INFINITY),
67
67
  lastChange: (t) => (t < 0 ? Number.NEGATIVE_INFINITY : t < seconds ? t : seconds),
68
- };
68
+ // A one-shot, reduced, cuts from where it starts to where it arrives.
69
+ reduced: () => Animation.cut(from, to),
70
+ });
69
71
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * View — what a host draws, and the frame it draws from it at a host time on
3
+ * its timeline: the lines, and what they say about when they change.
4
+ *
5
+ * A host — a `Live`, an `App` — reads its clock once a frame and draws its
6
+ * view for that moment. The view is a renderable, drawn as it is now (a
7
+ * table mutated in place between frames), at the time the host's `Timeline`
8
+ * (`./timeline.ts`) maps its clock to, written on the frame
9
+ * (`RenderOptions.frame`): every animation placed anywhere inside it draws
10
+ * at that time and stamps its cells with when it changes, on that clock
11
+ * (`placedRender`, `./animation.ts`). A program may hand a function for one
12
+ * instead, asked each frame so a view of its state is built from the state
13
+ * as it is then. It is handed no time, and `hostView` makes it the
14
+ * renderable it stands for, so a host holds one shape of view and draws it
15
+ * one way. [LAW:one-type-per-behavior]
16
+ *
17
+ * So a view says when it changes only through what it draws: what moves in
18
+ * it is an animation placed there — `Animation.continuous` for a value
19
+ * computed from the time — and never a time handed to the program.
20
+ * [LAW:one-source-of-truth] Pausing, reversing, scrubbing and speeding up
21
+ * what a host shows are changes to one value the host holds and not to
22
+ * anything it draws. What the drawn frame says about when it changes is on
23
+ * the timeline's clock, and `drawView` carries it back onto the host's by
24
+ * running the frame through the same timeline: the frame is a sample held
25
+ * for the intersection of its cells' holds (`Segment.lastChange`,
26
+ * `Segment.nextChange`) and the holds it was told that no cell carries
27
+ * (`RenderOptions.onHold`), and `Timeline.run` is the one place a hold is
28
+ * mapped onto host time, forwards, backwards or paused.
29
+ *
30
+ * What a host does with that is its own policy, and it reads it off the
31
+ * `DrawnView`. A view may also change without saying so — it reads state
32
+ * the host cannot see — so a host that polls for such a change, a `Live`'s
33
+ * auto-refresh, polls every frame.
34
+ *
35
+ * Tier 7 of `src/core/`: `./animation.ts`'s `Animation`, `./timeline.ts`,
36
+ * `./segment.ts`, `./protocol.ts` and `./style.ts`'s `Hold`, nothing else.
37
+ * [LAW:one-way-deps]
38
+ */
39
+ import { type FrameHost, type Renderable, type RenderOptions } from "./protocol.js";
40
+ import { Segment } from "./segment.js";
41
+ import { Timeline } from "./timeline.js";
42
+ /**
43
+ * What a host draws: a renderable — an animation of renderables among them,
44
+ * or anything holding one — or a function asked for one each frame.
45
+ */
46
+ export type View = Renderable | (() => Renderable);
47
+ /** `view` as the renderable a host holds: a function is asked each time it is drawn. */
48
+ export declare function hostView(view: View): Renderable;
49
+ /**
50
+ * A host's time, its clock read once: `now` on that clock; `epoch`, the
51
+ * moment on it the host began; and `timeline`, the map every view is drawn
52
+ * through. Handed none, a host's timeline starts as `Timeline.play(epoch)`,
53
+ * so a frame is drawn for the seconds since `epoch` until the timeline is
54
+ * paused, sought, re-rated or replaced.
55
+ */
56
+ export interface HostTime {
57
+ readonly now: number;
58
+ readonly epoch: number;
59
+ readonly timeline: Timeline;
60
+ }
61
+ /**
62
+ * The time a frame drawn at `time` is drawn for, on the clock its cells are
63
+ * stamped on: the time the timeline maps `now` to. [LAW:single-enforcer]
64
+ * `drawView` draws at it and hands it to the frame as
65
+ * `RenderOptions.frame`, and a host reads it here for anything heard between
66
+ * frames — a key — so a moment a widget keeps and the frame it is compared
67
+ * against are on one clock.
68
+ */
69
+ export declare function frameTime(time: HostTime): number;
70
+ /** What a frame drawn at host time `t` says about when it changes. */
71
+ export interface FrameSays {
72
+ /** The host time the frame was drawn at. */
73
+ readonly t: number;
74
+ /** The first host moment, never before `t`, at which the frame may stop showing this: `t` while it moves, `Infinity` once it never will. */
75
+ readonly nextChange: number;
76
+ }
77
+ /** A frame drawn from a view: its lines, and what it says. */
78
+ export interface DrawnView extends FrameSays {
79
+ readonly lines: Segment[][];
80
+ }
81
+ /** What a frame not yet drawn at host time `t` says: nothing. */
82
+ export declare const undrawn: (t: number) => FrameSays;
83
+ /**
84
+ * `view` drawn by `drawnBy` at `time` under `options`, its lines shaped by
85
+ * `draw` — the host's own render under those options, with its height and
86
+ * overflow — and when the frame changes on the host's clock: the hold its
87
+ * cells say, carried back through the timeline. [LAW:one-source-of-truth]
88
+ * `options` is the one place the frame reads the reader's preference for
89
+ * motion, handed on to `draw` with the frame: the time it is drawn for
90
+ * (`frameTime`) and `drawnBy`, the host whose time that is.
91
+ */
92
+ export declare function drawView(view: Renderable, time: HostTime, drawnBy: FrameHost, options: RenderOptions, draw: (renderable: Renderable, options: RenderOptions) => Segment[][]): DrawnView;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * View — what a host draws, and the frame it draws from it at a host time on
3
+ * its timeline: the lines, and what they say about when they change.
4
+ *
5
+ * A host — a `Live`, an `App` — reads its clock once a frame and draws its
6
+ * view for that moment. The view is a renderable, drawn as it is now (a
7
+ * table mutated in place between frames), at the time the host's `Timeline`
8
+ * (`./timeline.ts`) maps its clock to, written on the frame
9
+ * (`RenderOptions.frame`): every animation placed anywhere inside it draws
10
+ * at that time and stamps its cells with when it changes, on that clock
11
+ * (`placedRender`, `./animation.ts`). A program may hand a function for one
12
+ * instead, asked each frame so a view of its state is built from the state
13
+ * as it is then. It is handed no time, and `hostView` makes it the
14
+ * renderable it stands for, so a host holds one shape of view and draws it
15
+ * one way. [LAW:one-type-per-behavior]
16
+ *
17
+ * So a view says when it changes only through what it draws: what moves in
18
+ * it is an animation placed there — `Animation.continuous` for a value
19
+ * computed from the time — and never a time handed to the program.
20
+ * [LAW:one-source-of-truth] Pausing, reversing, scrubbing and speeding up
21
+ * what a host shows are changes to one value the host holds and not to
22
+ * anything it draws. What the drawn frame says about when it changes is on
23
+ * the timeline's clock, and `drawView` carries it back onto the host's by
24
+ * running the frame through the same timeline: the frame is a sample held
25
+ * for the intersection of its cells' holds (`Segment.lastChange`,
26
+ * `Segment.nextChange`) and the holds it was told that no cell carries
27
+ * (`RenderOptions.onHold`), and `Timeline.run` is the one place a hold is
28
+ * mapped onto host time, forwards, backwards or paused.
29
+ *
30
+ * What a host does with that is its own policy, and it reads it off the
31
+ * `DrawnView`. A view may also change without saying so — it reads state
32
+ * the host cannot see — so a host that polls for such a change, a `Live`'s
33
+ * auto-refresh, polls every frame.
34
+ *
35
+ * Tier 7 of `src/core/`: `./animation.ts`'s `Animation`, `./timeline.ts`,
36
+ * `./segment.ts`, `./protocol.ts` and `./style.ts`'s `Hold`, nothing else.
37
+ * [LAW:one-way-deps]
38
+ */
39
+ import { render } from "./protocol.js";
40
+ import { Segment } from "./segment.js";
41
+ import { Timeline } from "./timeline.js";
42
+ /** `view` as the renderable a host holds: a function is asked each time it is drawn. */
43
+ export function hostView(view) {
44
+ return typeof view === "function" ? { render: (options) => render(view(), options) } : view;
45
+ }
46
+ /**
47
+ * The time a frame drawn at `time` is drawn for, on the clock its cells are
48
+ * stamped on: the time the timeline maps `now` to. [LAW:single-enforcer]
49
+ * `drawView` draws at it and hands it to the frame as
50
+ * `RenderOptions.frame`, and a host reads it here for anything heard between
51
+ * frames — a key — so a moment a widget keeps and the frame it is compared
52
+ * against are on one clock.
53
+ */
54
+ export function frameTime(time) {
55
+ return Timeline.at(time.timeline, time.now);
56
+ }
57
+ /** What a frame not yet drawn at host time `t` says: nothing. */
58
+ export const undrawn = (t) => ({ t, nextChange: Number.POSITIVE_INFINITY });
59
+ /** The hold of the frame `lines` draw, told `told` besides: the intersection of its cells' holds and it. */
60
+ const frameHold = (lines, told) => lines.reduce((hold, row) => ({ lastChange: Math.max(hold.lastChange, Segment.lastChange(row)), nextChange: Math.min(hold.nextChange, Segment.nextChange(row)) }), told);
61
+ /**
62
+ * `view` drawn by `drawnBy` at `time` under `options`, its lines shaped by
63
+ * `draw` — the host's own render under those options, with its height and
64
+ * overflow — and when the frame changes on the host's clock: the hold its
65
+ * cells say, carried back through the timeline. [LAW:one-source-of-truth]
66
+ * `options` is the one place the frame reads the reader's preference for
67
+ * motion, handed on to `draw` with the frame: the time it is drawn for
68
+ * (`frameTime`) and `drawnBy`, the host whose time that is.
69
+ */
70
+ export function drawView(view, time, drawnBy, options, draw) {
71
+ const { now: host, timeline } = time;
72
+ let told = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
73
+ const onHold = (hold) => {
74
+ told = { lastChange: Math.max(told.lastChange, hold.lastChange), nextChange: Math.min(told.nextChange, hold.nextChange) };
75
+ };
76
+ const lines = draw(view, { ...options, frame: { time: frameTime(time), host: drawnBy }, onHold });
77
+ const hold = frameHold(lines, told);
78
+ // The frame as the sample of the view it is, on the timeline's clock; the
79
+ // timeline says when the host's clock leaves its hold.
80
+ const sample = { at: () => lines, nextChange: () => hold.nextChange, lastChange: () => hold.lastChange, reduced: () => sample };
81
+ const nextChange = Timeline.run(timeline, sample).nextChange(host);
82
+ return { t: host, lines, nextChange };
83
+ }
@@ -34,8 +34,10 @@ import { type Clock, type FrameRate } from "../core/clock.js";
34
34
  import { type ConsoleOptions } from "../core/console.js";
35
35
  import { Segment } from "../core/segment.js";
36
36
  import { type Surface } from "../core/paint.js";
37
- import { type Renderable } from "../core/protocol.js";
37
+ import { type FrameHost } from "../core/protocol.js";
38
38
  import type { Unsubscribe } from "../core/subscription.js";
39
+ import { Timeline } from "../core/timeline.js";
40
+ import { type View } from "../core/view.js";
39
41
  import { type KeyPress, type ScreenMouseEvent } from "./input.js";
40
42
  import type { TerminalHost } from "./terminal-host.js";
41
43
  /**
@@ -44,7 +46,7 @@ import type { TerminalHost } from "./terminal-host.js";
44
46
  * the program; `stopped` has handed it back for good.
45
47
  */
46
48
  export type AppPhase = "idle" | "running" | "suspended" | "stopped";
47
- declare const DRAW_OPTIONS: readonly ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
49
+ declare const DRAW_OPTIONS: readonly ["asciiOnly", "reducedMotion", "theme", "onStyleError", "markup", "highlight", "highlighter"];
48
50
  type DrawOption = (typeof DRAW_OPTIONS)[number];
49
51
  type DrawOptions = Readonly<Pick<ConsoleOptions, DrawOption>>;
50
52
  export interface AppOptions extends DrawOptions {
@@ -52,15 +54,28 @@ export interface AppOptions extends DrawOptions {
52
54
  readonly host: TerminalHost;
53
55
  readonly surface: Surface;
54
56
  /**
55
- * The app's root renderable, asked for once per frame, so a view of the
56
- * app's state is drawn from the state as it is at that frame. `t` is the
57
- * frame's time in seconds on the app's `clock`, which is what an effect is
58
- * sampled at.
57
+ * What the app draws: a renderable, or a function asked for one once per
58
+ * frame — so a view of the app's state is drawn from the state as it is at
59
+ * that frame. What moves in it is an `Animation` placed there, drawn at the
60
+ * frame's time on the app's `timeline`, which says when it changes, so the
61
+ * app knows when to paint again.
59
62
  */
60
- readonly view: (t: number) => Renderable;
63
+ readonly view: View;
61
64
  /** What the app reads the time from and ticks on. The platform's, unless given. */
62
65
  readonly clock?: Clock;
63
- /** How often the app paints while something is animating. 30 frames a second, unless given. */
66
+ /**
67
+ * The map from the clock to the time the view is drawn for (`Timeline`).
68
+ * Playing from 0 when the host is made, at the clock's pace, unless given
69
+ * — so a tween in the view starts when the host is made, not when the clock
70
+ * began; `app.timeline` reads and replaces it.
71
+ */
72
+ readonly timeline?: Timeline;
73
+ /**
74
+ * How often the app paints while what it shows moves every moment — a
75
+ * frame that says it moves. A frame held until a
76
+ * moment is painted at that moment, whatever the rate. 30 frames a second,
77
+ * unless given.
78
+ */
64
79
  readonly rate?: FrameRate;
65
80
  /**
66
81
  * The keys that stop the app, by `keyName`, heard before any `onKey`
@@ -87,14 +102,17 @@ type WheelTurn = Extract<ScreenMouseEvent, {
87
102
  }>;
88
103
  /** A pointer event `onMouse` hears: every one but the vertical wheel, which is `onWheel`'s. */
89
104
  export type AppMouseEvent = Exclude<ScreenMouseEvent, WheelTurn>;
90
- export declare class App {
105
+ export declare class App implements FrameHost {
91
106
  private readonly host;
92
107
  private readonly surface;
93
108
  private readonly view;
94
- private readonly clock;
109
+ private readonly _clock;
110
+ private readonly _epoch;
111
+ private _timeline;
95
112
  private _rate;
96
- private readonly animations;
97
113
  private ticking;
114
+ private said;
115
+ private waking;
98
116
  private readonly console;
99
117
  private _phase;
100
118
  private _frame;
@@ -110,7 +128,18 @@ export declare class App {
110
128
  private settle;
111
129
  constructor(options: AppOptions);
112
130
  get phase(): AppPhase;
113
- /** How often the app paints while something is animating. */
131
+ /** The clock the app reads the time from: where the host time a `Timeline` operation takes is read. */
132
+ get clock(): Clock;
133
+ /** The map from the clock to the time the view is drawn for. */
134
+ get timeline(): Timeline;
135
+ /**
136
+ * Replace the timeline and paint the frame for the time it now maps the
137
+ * clock's now to, so a seek shows where it sought, once. Paused, the frame
138
+ * then holds: nothing drawn for a held time changes, so the app waits on
139
+ * nothing until the timeline plays again, and paints when asked.
140
+ */
141
+ set timeline(timeline: Timeline);
142
+ /** How often the app paints while what it shows moves every moment. */
114
143
  get rate(): FrameRate;
115
144
  /**
116
145
  * Set how often the app paints: an animation under way goes on at the new
@@ -119,6 +148,14 @@ export declare class App {
119
148
  set rate(rate: FrameRate);
120
149
  /** The rows on screen now, as painted: what a pointer event lands on. */
121
150
  get frame(): readonly (readonly Segment[])[];
151
+ /**
152
+ * The time a frame drawn now is drawn for (`RenderOptions.frame`): the
153
+ * clock's now, through the timeline. What a moment heard
154
+ * between frames — a key — is stamped with, so it is on the clock the
155
+ * frame is.
156
+ */
157
+ get frameTime(): number;
158
+ private _time;
122
159
  /**
123
160
  * Take the terminal, paint the first frame, and resolve when the app stops
124
161
  * — or reject with the error a frame threw or `fail` was given, the terminal
@@ -137,15 +174,6 @@ export declare class App {
137
174
  * for as long as it ran.
138
175
  */
139
176
  refresh(): void;
140
- /**
141
- * Paint at the app's `rate` until the returned function is called: what an
142
- * effect sampled at the frame's time asks for while it moves. The app ticks
143
- * while anything is animating and it holds the terminal, and stops when
144
- * nothing is, so a still app paints only when asked. The animation's first
145
- * frame does not wait out an interval: at one frame every two seconds, that
146
- * would hold its start off screen for two.
147
- */
148
- animate(): Unsubscribe;
149
177
  /**
150
178
  * Hand the terminal to whatever launched the program, and take it back and
151
179
  * repaint when that resumes it. What sends the app here is the app's own —
package/dist/host/app.js CHANGED
@@ -35,6 +35,8 @@ import { Console } from "../core/console.js";
35
35
  import { Segment } from "../core/segment.js";
36
36
  import { SurfacePainter } from "../core/paint.js";
37
37
  import { fitHeight, render } from "../core/protocol.js";
38
+ import { Timeline } from "../core/timeline.js";
39
+ import { drawView, frameTime, hostView, undrawn } from "../core/view.js";
38
40
  import { hostEnvironment } from "./host-environment.js";
39
41
  import { InputParser, keyName, parseKeyName } from "./input.js";
40
42
  // [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
@@ -43,7 +45,7 @@ import { InputParser, keyName, parseKeyName } from "./input.js";
43
45
  // console declares them. This list is both the type and what is copied: the
44
46
  // host alone decides the console's size, colours and sink, so no other
45
47
  // property of the options object reaches it, whatever the object carries.
46
- const DRAW_OPTIONS = ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
48
+ const DRAW_OPTIONS = ["asciiOnly", "reducedMotion", "theme", "onStyleError", "markup", "highlight", "highlighter"];
47
49
  function drawOptions(options) {
48
50
  const picked = {};
49
51
  const copy = (key) => {
@@ -71,12 +73,19 @@ export class App {
71
73
  host;
72
74
  surface;
73
75
  view;
74
- clock;
76
+ _clock;
77
+ // The clock moment this app began: what its default timeline plays from,
78
+ // and what a renderable view is drawn for the time since (`HostTime`).
79
+ _epoch;
80
+ _timeline;
75
81
  _rate;
76
- // What is animating now: one token per `animate()` not yet released.
77
- animations = new Set();
78
82
  // The timer painting frames, and the rate it was started at.
79
83
  ticking = null;
84
+ // What the frame on screen says about when it changes, on the clock.
85
+ // Before a frame, nothing.
86
+ said = undrawn(0);
87
+ // The one wait for the frame's `nextChange`, armed after each frame.
88
+ waking = null;
80
89
  // [LAW:one-source-of-truth] The host is the console's whole environment:
81
90
  // its size, its colours, where bytes go. Every frame renders with this
82
91
  // console's options and is encoded for its destination.
@@ -98,8 +107,10 @@ export class App {
98
107
  this.input = new InputParser({ key: (press) => this.key(press), mouse: (event) => this.mouse(event) }, options.host.inputChunks);
99
108
  this.surface = options.surface;
100
109
  this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
101
- this.view = options.view;
102
- this.clock = options.clock ?? systemClock();
110
+ this.view = hostView(options.view);
111
+ this._clock = options.clock ?? systemClock();
112
+ this._epoch = this._clock.now();
113
+ this._timeline = options.timeline ?? Timeline.play(this._epoch);
103
114
  this._rate = options.rate ?? DEFAULT_RATE;
104
115
  this.quitKeys = new Set((options.quitKeys ?? DEFAULT_QUIT_KEYS).map(parseKeyName));
105
116
  this.console = new Console({ ...drawOptions(options), environment: hostEnvironment(options.host) });
@@ -107,7 +118,25 @@ export class App {
107
118
  get phase() {
108
119
  return this._phase;
109
120
  }
110
- /** How often the app paints while something is animating. */
121
+ /** The clock the app reads the time from: where the host time a `Timeline` operation takes is read. */
122
+ get clock() {
123
+ return this._clock;
124
+ }
125
+ /** The map from the clock to the time the view is drawn for. */
126
+ get timeline() {
127
+ return this._timeline;
128
+ }
129
+ /**
130
+ * Replace the timeline and paint the frame for the time it now maps the
131
+ * clock's now to, so a seek shows where it sought, once. Paused, the frame
132
+ * then holds: nothing drawn for a held time changes, so the app waits on
133
+ * nothing until the timeline plays again, and paints when asked.
134
+ */
135
+ set timeline(timeline) {
136
+ this._timeline = timeline;
137
+ this.refresh();
138
+ }
139
+ /** How often the app paints while what it shows moves every moment. */
111
140
  get rate() {
112
141
  return this._rate;
113
142
  }
@@ -123,6 +152,18 @@ export class App {
123
152
  get frame() {
124
153
  return this._frame;
125
154
  }
155
+ /**
156
+ * The time a frame drawn now is drawn for (`RenderOptions.frame`): the
157
+ * clock's now, through the timeline. What a moment heard
158
+ * between frames — a key — is stamped with, so it is on the clock the
159
+ * frame is.
160
+ */
161
+ get frameTime() {
162
+ return frameTime(this._time());
163
+ }
164
+ _time() {
165
+ return { now: this._clock.now(), epoch: this._epoch, timeline: this._timeline };
166
+ }
126
167
  /**
127
168
  * Take the terminal, paint the first frame, and resolve when the app stops
128
169
  * — or reject with the error a frame threw or `fail` was given, the terminal
@@ -187,24 +228,6 @@ export class App {
187
228
  this.paint();
188
229
  });
189
230
  }
190
- /**
191
- * Paint at the app's `rate` until the returned function is called: what an
192
- * effect sampled at the frame's time asks for while it moves. The app ticks
193
- * while anything is animating and it holds the terminal, and stops when
194
- * nothing is, so a still app paints only when asked. The animation's first
195
- * frame does not wait out an interval: at one frame every two seconds, that
196
- * would hold its start off screen for two.
197
- */
198
- animate() {
199
- const animation = {};
200
- this.animations.add(animation);
201
- this.tick();
202
- this.refresh();
203
- return () => {
204
- this.animations.delete(animation);
205
- this.tick();
206
- };
207
- }
208
231
  /**
209
232
  * Hand the terminal to whatever launched the program, and take it back and
210
233
  * repaint when that resumes it. What sends the app here is the app's own —
@@ -350,19 +373,27 @@ export class App {
350
373
  }
351
374
  }
352
375
  // [LAW:single-enforcer] Whether the clock is ticking, and at what rate,
353
- // follows from the phase, what is animating and the rate, and is brought
354
- // in line with them here alone, after every change to any — so a timer
355
- // runs exactly while a running app has something animating, at the rate
356
- // the app has now, and none outlives the app.
376
+ // and whether a wake is armed for the frame's next change, follow from the
377
+ // phase, the rate and what the frame on screen says, and are brought in
378
+ // line with them here alone, after every change to any — so a timer runs
379
+ // exactly while a running app shows a frame that says it moves, at the
380
+ // rate the app has now, a wake is armed exactly while a running app shows
381
+ // a frame held until a moment, and none outlives the app.
382
+ // [LAW:one-source-of-truth] What the frame draws is the only thing that
383
+ // says whether the view moves.
357
384
  tick() {
358
- const wanted = this._phase === "running" && this.animations.size > 0;
385
+ const running = this._phase === "running";
386
+ const { t, nextChange } = this.said;
387
+ const wanted = running && nextChange === t;
359
388
  if (this.ticking !== null && (!wanted || this.ticking.rate.perSecond !== this._rate.perSecond)) {
360
389
  this.ticking.stop();
361
390
  this.ticking = null;
362
391
  }
363
392
  if (wanted && this.ticking === null) {
364
- this.ticking = { rate: this._rate, stop: this.clock.every(this._rate, () => this.paint()) };
393
+ this.ticking = { rate: this._rate, stop: this._clock.every(this._rate, () => this.paint()) };
365
394
  }
395
+ this.waking?.();
396
+ this.waking = running && t < nextChange && nextChange < Number.POSITIVE_INFINITY ? this._clock.at(nextChange, () => this.paint()) : null;
366
397
  }
367
398
  // [LAW:no-silent-failure] A frame that throws ends the app with that
368
399
  // error: the terminal is handed back, and `run` rejects with it.
@@ -380,13 +411,16 @@ export class App {
380
411
  const options = this.console.options;
381
412
  const height = this.painter.height(options.height.rows);
382
413
  // [LAW:no-ambient-temporal-coupling] The frame owner reads the clock,
383
- // once a frame; the view is handed the time as data.
384
- const lines = Segment.splitLines(render(this.view(this.clock.now()), { ...options, height }));
385
- // The app set the budget, so the app shapes what comes back: no deeper
386
- // than the terminal — a taller frame would scroll the rows the painter
387
- // goes back over — and, as a region, exactly that deep.
388
- const frame = this.painter.paint(() => fitHeight(lines.slice(0, height.rows), height), { rows: height.rows, cols: options.maxWidth }, this.console.destination);
414
+ // once a frame; the view is handed the time the timeline maps it to, as
415
+ // data, and what the frame says about when it changes comes back on the
416
+ // clock. The app set the budget, so the app shapes what comes back: no
417
+ // deeper than the terminal — a taller frame would scroll the rows the
418
+ // painter goes back over — and, as a region, exactly that deep.
419
+ const drawn = drawView(this.view, this._time(), this, { ...options, height }, (renderable, under) => fitHeight(Segment.splitLines(render(renderable, under)).slice(0, height.rows), height));
420
+ const frame = this.painter.paint(() => drawn.lines, { rows: height.rows, cols: options.maxWidth }, this.console.destination);
389
421
  this._frame = frame;
422
+ this.said = drawn;
423
+ this.tick();
390
424
  for (const handler of [...this.paintHandlers])
391
425
  handler(frame);
392
426
  }
@@ -146,7 +146,6 @@ const M_LOWER = 0x6d; // m
146
146
  const UTF8_ENCODER = new TextEncoder();
147
147
  const UTF8_DECODER = new TextDecoder("utf-8");
148
148
  const ASCII_DECODER = new TextDecoder("ascii");
149
- const EMPTY_BYTES = new Uint8Array(0);
150
149
  /**
151
150
  * How long a `"split"` host's parser waits for the rest of an unfinished
152
151
  * escape sequence before reading it as the keys it already holds. Textual's
@@ -195,7 +194,7 @@ export class InputParser {
195
194
  // `Buffer` — node's Buffer extends Uint8Array, so Buffer chunks from
196
195
  // process.stdin satisfy this type, while a browser host can emit native
197
196
  // Uint8Array without depending on a Buffer global polyfill.
198
- buffer = EMPTY_BYTES;
197
+ buffer = new Uint8Array(0);
199
198
  // [LAW:types-are-the-program] `setTimeout`/`clearTimeout` are universal
200
199
  // across node and browser; `setImmediate` is node-only.
201
200
  escTimer;
@@ -218,7 +217,7 @@ export class InputParser {
218
217
  this.cancelEscape();
219
218
  // The buffer is settled before the sink hears Escape, so a sink that
220
219
  // resets the parser on it (an app stopping) leaves nothing to drain.
221
- this.buffer = concatBytes(escaped ? EMPTY_BYTES : this.buffer, next);
220
+ this.buffer = concatBytes(escaped ? new Uint8Array(0) : this.buffer, next);
222
221
  try {
223
222
  if (escaped)
224
223
  this.sink.key(ESCAPE);
@@ -292,7 +291,7 @@ export class InputParser {
292
291
  /** Drop what is buffered and any held escape sequence: the parser as constructed. */
293
292
  reset() {
294
293
  this.cancelEscape();
295
- this.buffer = EMPTY_BYTES;
294
+ this.buffer = new Uint8Array(0);
296
295
  }
297
296
  /** A lone ESC, the one hold a next chunk can settle before its bytes are read. */
298
297
  holdingEscape() {