@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,30 +1,42 @@
1
1
  /**
2
- * Effected — a renderable whose finished cells' colours pass through an
3
- * effect at a moment `t`.
2
+ * Effected — a renderable whose finished cells' looks — colours and text
3
+ * attributes — pass through an effect at a moment `t`.
4
4
  *
5
- * An effect is one pure function in two stages: a time and what the element
6
- * draws in that frame in, a function of the frame out; and into that, a
7
- * cell's colours and where the cell is, the cell's new colours out. Whatever
8
- * holds for the whole frame is worked out in the first stage, once, and every
9
- * cell of the frame is handed to the second, but for those a wrapper keeps
10
- * back: `confined` hands over only the cells its owners drew, so a second
11
- * stage never counts on seeing them all. Shimmer, pulse, fade and
12
- * dissolve are all this one shape, differing by what the function does with
13
- * its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
5
+ * An effect is, for what an element draws in a frame, an `Animation`
6
+ * (`core/animation.ts`) of what is done to its cells: its value at `t` is a
7
+ * function from a cell's look and where the cell is to the cell's new look
8
+ * (`EffectFrame`), and it says when that next changes as every
9
+ * animation does. Whatever holds for the whole frame is worked out once, in
10
+ * the effect and its `at(t)`, and every cell of the frame is handed to the
11
+ * function `at(t)` returns, but for those a wrapper keeps back: `confined`
12
+ * hands over only the cells its owners drew, so an effect never counts on
13
+ * seeing them all. Shimmer, pulse, fade, dissolve and a blinking attribute
14
+ * are all this one shape, differing by what the function does with its
15
+ * inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
14
16
  * wrapper in another, which is the same as calling one after the other but
15
17
  * for what the outer one measures: the inner one's output.
16
18
  *
17
- * What the element draws (`Drawn`) — the cells the second stage will be
18
- * handed, and the colours of its words — is measured here, in the render, off
19
- * the same reading of each segment its cells are drawn from, and handed to the
20
- * effect's first stage, once a frame. It is what an effect keeping words legible is
21
- * settled against (`onColors`, `./effects.ts`), so no caller derives it a
22
- * second time from a render of its own.
19
+ * A cell's look is one value, its colours and its attributes together
20
+ * (`CellLook`), rather than an attribute stage beside the colour one, so
21
+ * everything that takes an `Effect` — `confined`, `onBeat`,
22
+ * `Animation.all`, nesting over a glyph effect — takes one that moves
23
+ * attributes with no second copy of itself. `reverse` is the one attribute
24
+ * a look leaves out (`CellAttributes`): it decides which of a style's slots
25
+ * a colour on screen is in, and a swap of the two is already a colour move.
23
26
  *
24
- * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
25
- * clock read. Whoever draws frames builds the view for each frame's `t`, so
26
- * the same `t` always draws the same bytes, at 30 frames a second or one
27
- * every two seconds.
27
+ * What the element draws (`Drawn`) — the cells the effect will be handed,
28
+ * and the colours of its words — is measured here, in the render, off the
29
+ * same reading of each segment its cells are drawn from, and handed to the
30
+ * effect once a frame. It is what an effect keeping words legible is settled
31
+ * against (`onColors`, `./effects.ts`), so no caller derives it a second time
32
+ * from a render of its own.
33
+ *
34
+ * An `Effected` is the element under its effect over time: an
35
+ * `Animation<Renderable>`, placed wherever a renderable goes and drawn at
36
+ * the time of the frame it is drawn in. Its value at `t` is the element
37
+ * through the effect at `t` (`Recolored`). [LAW:no-ambient-temporal-coupling]
38
+ * The time is the frame's, never a clock read, so the same time always draws
39
+ * the same bytes, at 30 frames a second or one every two seconds.
28
40
  *
29
41
  * What an effect may not do is the contract this module holds:
30
42
  *
@@ -34,46 +46,44 @@
34
46
  * segment's link and meta and carries its anchor shifted to the run's first
35
47
  * cell (`Style.shiftedBy`), so hit testing still finds the owner.
36
48
  * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
37
- * and a segment whose every cell is unchanged is yielded with its look as
38
- * it was, so the identity effect is byte-identical; what it gains is when
39
- * the frame next changes (`Style.nextChange`), which writes no byte.
49
+ * one whose attributes come back as its style set them keeps them, and a
50
+ * segment whose every cell is unchanged is yielded with its look as
51
+ * it was, so the identity effect is byte-identical; what an `Effected` gains is when
52
+ * the frame next changes (`Style.hold`), which writes no byte.
40
53
  * Neighbouring cells the output depth
41
54
  * writes alike stay one segment: a segment is cut only where the written
42
- * colours change, so two colours that land on one index are one run.
55
+ * colours or the attributes change, so two colours that land on one index
56
+ * are one run.
43
57
  * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
44
58
  * string, so no frame adds an entry to a parse cache.
45
- * - With no colour output (`colorSystem: null`) there is nothing for an
46
- * effect to change, and the child's segments pass through untouched.
59
+ * - With no colour output (`colorSystem: null`) no style is written at all,
60
+ * attributes included, so there is nothing for an effect to change, and
61
+ * the child's segments pass through untouched.
47
62
  *
48
63
  * Positions are the child's own: row and column count from the top-left of
49
64
  * what the child drew, so a sweep crosses the element, not the screen.
50
65
  */
51
66
  import { drawnBy, shiftAnchor } from "../core/anchor.js";
67
+ import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
52
68
  import { cellLen, graphemes } from "../core/cells.js";
53
69
  import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
54
70
  import { fnv1a } from "../core/fnv1a.js";
55
71
  import { Measurement } from "../core/measure.js";
72
+ import { render } from "../core/protocol.js";
56
73
  import { Segment } from "../core/segment.js";
57
74
  import { JOIN, colorsOwnedBy } from "../core/strip.js";
58
- import { Style } from "../core/style.js";
75
+ import { ATTRIBUTE_NAMES, Style } from "../core/style.js";
76
+ import { finiteTime } from "../core/easing.js";
77
+ /** What drew nothing the effect can change: held forever, as far as the effect says. */
78
+ const UNMOVED = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
59
79
  /**
60
- * When `effect`'s frame at `t` of `drawn` next changes: what it says, or `t`
61
- * for an effect that says nothing. [LAW:single-enforcer] The one reading of
62
- * an absent `nextChange`, so no caller holds its own default.
80
+ * An `Effected` at `t`, or an element through an effect at a time an
81
+ * animation's `at` was handed — a lifecycle's time since it entered: `renderable`
82
+ * with its cells' looks passed through `effect` at `t`, saying nothing of
83
+ * when that changes, since whatever placed the animation stamps its hold.
84
+ * [LAW:one-type-per-behavior]
63
85
  */
64
- export function nextChangeOf(effect, t, drawn) {
65
- return effect.nextChange?.(t, drawn) ?? t;
66
- }
67
- /**
68
- * An effect drawing as `effect` does, with `nextChange` as its answer to when
69
- * its frame next changes, in place of whatever `effect` answered; `effect`
70
- * itself is left as it was. A composer whose answer depends on the inner
71
- * effect's reads it with `nextChangeOf`, as `onBeat` and `confined` do.
72
- */
73
- export function withNextChange(effect, nextChange) {
74
- return Object.assign((t, drawn) => effect(t, drawn), { nextChange });
75
- }
76
- export class Effected {
86
+ export class Recolored {
77
87
  renderable;
78
88
  effect;
79
89
  t;
@@ -82,35 +92,30 @@ export class Effected {
82
92
  constructor(renderable, effect, options) {
83
93
  this.renderable = renderable;
84
94
  this.effect = effect;
85
- if (!Number.isFinite(options.t)) {
86
- throw new RangeError(`Effected needs a finite t, got ${options.t}`);
87
- }
88
- this.t = options.t;
95
+ this.t = finiteTime("Effected", options.t);
89
96
  this.key = options.key;
90
97
  this.theme = options.theme;
91
98
  }
92
99
  render(options) {
93
- const segments = this.renderable.render(options);
100
+ return this.moved(options).segments;
101
+ }
102
+ /** The frame the effect draws at `t`, and the hold it says that is in. */
103
+ moved(options) {
104
+ const segments = render(this.renderable, options);
94
105
  // With no colour output nothing the effect does is written, so the child
95
106
  // is what is shown, and it changes when the child says it does.
96
107
  if (options.colorSystem === null)
97
- return segments;
98
- return this._moved(segments, options);
99
- }
100
- *_moved(segments, options) {
108
+ return { segments, hold: UNMOVED };
101
109
  // No depth named is no downgrade, as `drawnColors` reads it.
102
110
  const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
103
111
  // [LAW:one-source-of-truth] The frame is measured before any cell is
104
112
  // handed to the effect, off the same looks its cells are drawn from, and
105
- // the one measure is what both stages and the effect's next change are
106
- // asked with.
113
+ // the one measure is what the effect's animation is made from.
107
114
  const looked = looks(segments, depth, this.theme);
108
115
  const walked = walk(looked);
109
- const drawn = measured(looked, () => walked, depth, this.theme);
110
- const frame = this.effect(this.t, drawn);
111
- // What the effect draws at `t` is drawn until its next change, and the
112
- // cells say so, under whatever the child already said (the sooner wins).
113
- yield* Segment.applyStyle(this._recolored(walked, frame, depth), new Style({ nextChange: nextChangeOf(this.effect, this.t, drawn) }));
116
+ const animation = this.effect(measured(looked, () => walked, depth, this.theme));
117
+ const shown = shownAt(animation, this.t, options);
118
+ return { segments: this._recolored(walked, shown.value, depth), hold: holdOf(shown, this.t) };
114
119
  }
115
120
  *_recolored(walked, frame, depth) {
116
121
  for (const { segment, look, glyphs } of walked) {
@@ -118,35 +123,38 @@ export class Effected {
118
123
  yield segment;
119
124
  continue;
120
125
  }
121
- const { style: base, wire, slots, pair } = look;
126
+ const { style: base, wire, slots, pair, attributes } = look;
122
127
  const { terminal } = pair;
123
- const from = { fg: pair.fg, bg: pair.bg };
128
+ const from = { fg: pair.fg, bg: pair.bg, attributes };
124
129
  // [LAW:one-source-of-truth] A run is cells the wire writes alike: each
125
130
  // cell is drawn at the output depth first and merged on the colours that
126
- // writes, so two truecolors landing on one index stay one run. A cell
127
- // the effect gave its neighbour's colours is drawn as that neighbour was.
131
+ // writes and on its attributes, so two truecolors landing on one index
132
+ // stay one run. A cell the effect gave its neighbour's look is drawn as
133
+ // that neighbour was.
128
134
  const runs = [];
129
135
  for (const { glyph, cell } of glyphs) {
136
+ const moved = cell === undefined ? from : returned(frame(from, { ...cell, seed: cellSeed(this.key, cell.row, cell.col), terminal, reverse: pair.reverse }));
130
137
  // Back from the screen to the style's slots: the same swap undoes itself.
131
- const colors = cell === undefined ? slots : onScreen(frame(from, { ...cell, seed: this.seed(cell.row, cell.col), terminal, reverse: pair.reverse }), pair.reverse);
138
+ const colors = onScreen(moved, pair.reverse);
132
139
  const last = runs[runs.length - 1];
133
140
  const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
134
- if (last !== undefined && writesAlike(last.drawn, writes)) {
141
+ if (last !== undefined && writesAlike(last.drawn, writes) && sameAttributes(last.attributes, moved.attributes)) {
135
142
  last.text += glyph;
136
143
  last.cells += cellLen(glyph);
137
144
  last.colors = colors;
138
145
  }
139
146
  else {
140
- runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn: writes });
147
+ runs.push({ text: glyph, cells: cellLen(glyph), colors, attributes: moved.attributes, drawn: writes });
141
148
  }
142
149
  }
143
- if (runs.every((run) => writesAlike(run.drawn, wire))) {
150
+ if (runs.every((run) => writesAlike(run.drawn, wire) && sameAttributes(run.attributes, attributes))) {
144
151
  yield segment;
145
152
  continue;
146
153
  }
147
154
  let offset = 0;
148
155
  for (const run of runs) {
149
- const shifted = base.shiftedBy(offset);
156
+ const placed = base.shiftedBy(offset);
157
+ const shifted = sameAttributes(run.attributes, attributes) ? placed : placed.withAttributes(cellAttributes(run.attributes));
150
158
  const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
151
159
  yield new Segment(run.text, style);
152
160
  offset += run.cells;
@@ -156,6 +164,61 @@ export class Effected {
156
164
  measure(options) {
157
165
  return Measurement.get(options, this.renderable);
158
166
  }
167
+ /**
168
+ * The specs that draw a cell's slot colours `to` where the segment drew
169
+ * `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
170
+ * in, the rest as `wire` wrote them.
171
+ */
172
+ respec(wire, from, to, depth) {
173
+ const moved = movedSpecs(from, to, depth, this.theme);
174
+ return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
175
+ }
176
+ }
177
+ /**
178
+ * `renderable` under `effect` over time: an `Animation<Renderable>` whose
179
+ * value at `t` is the element through the effect at `t`, placed wherever a
180
+ * renderable goes. Its own answer to when it changes is `t`, at every time:
181
+ * an `Effect` is made from what the element draws, so with nothing drawn
182
+ * nothing can say more — early, never false. Placed, it says exactly: its
183
+ * cells carry the effect's own hold at the frame's time and the output's
184
+ * depth (`stampFrame`), under whatever the element already said, so at a
185
+ * depth that shows no motion a loop says it never changes there and the
186
+ * host sleeps.
187
+ */
188
+ export class Effected {
189
+ renderable;
190
+ effect;
191
+ key;
192
+ theme;
193
+ constructor(renderable, effect, options) {
194
+ this.renderable = renderable;
195
+ this.effect = effect;
196
+ this.key = options.key;
197
+ this.theme = options.theme;
198
+ }
199
+ /** The element through the effect at `t`, unstamped. */
200
+ at(t) {
201
+ return new Recolored(this.renderable, this.effect, { t, key: this.key, theme: this.theme });
202
+ }
203
+ nextChange(t) {
204
+ return t;
205
+ }
206
+ lastChange(t) {
207
+ return t;
208
+ }
209
+ /** The element under the effect with its motion taken out. */
210
+ reduced() {
211
+ const { effect } = this;
212
+ return new Effected(this.renderable, (drawn) => effect(drawn).reduced(), this);
213
+ }
214
+ /** Drawn at the frame's time, its cells stamped with the effect's exact hold there. */
215
+ render(options) {
216
+ const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
217
+ return stampFrame(segments, hold);
218
+ }
219
+ measure(options) {
220
+ return Measurement.get(options, this.renderable);
221
+ }
159
222
  /**
160
223
  * The colours the screen shows for `pair` once it is drawn moved to `to`,
161
224
  * both as the screen shows them, at `depth` on `theme`: what an effect
@@ -168,19 +231,14 @@ export class Effected {
168
231
  const moved = movedSpecs(from, onScreen(to, pair.reverse), depth, theme);
169
232
  return onScreen({ fg: moved.fg?.getTruecolor(theme, true) ?? from.fg, bg: moved.bg?.getTruecolor(theme, false) ?? from.bg }, pair.reverse);
170
233
  }
171
- /**
172
- * The specs that draw a cell's slot colours `to` where the segment drew
173
- * `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
174
- * in, the rest as `wire` wrote them.
175
- */
176
- respec(wire, from, to, depth) {
177
- const moved = movedSpecs(from, to, depth, this.theme);
178
- return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
179
- }
180
- /** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
181
- seed(row, col) {
182
- return Number.parseInt(fnv1a(`${this.key}:${row}:${col}`), 16) / 0x1_0000_0000;
183
- }
234
+ }
235
+ /**
236
+ * A cell's seed: FNV-1a of the element's key and the cell's position, scaled
237
+ * into [0, 1). [LAW:one-source-of-truth] One rule for a colour effect's seeds
238
+ * and a glyph effect's, so one element under both twinkles from one seed.
239
+ */
240
+ export function cellSeed(key, row, col) {
241
+ return Number.parseInt(fnv1a(`${key}:${row}:${col}`), 16) / 0x1_0000_0000;
184
242
  }
185
243
  const DEFAULT = ColorSpec.default();
186
244
  /** Each of `segments` with how it is drawn at `depth` on `theme`; a control segment draws nothing. */
@@ -193,34 +251,48 @@ function looks(segments, depth, theme) {
193
251
  const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
194
252
  const reverse = style.reverse === true;
195
253
  const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
196
- return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse } } };
254
+ return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse }, attributes: cellAttributes(style) } };
197
255
  });
198
256
  }
199
257
  /**
200
- * Each glyph of `looked`, where its cell sits: rows and columns from the
201
- * top-left of the output, a wide glyph one cell at its first column, and its
202
- * anchor shifted to that column as `Style.shiftedBy` shifts a run's.
203
- * [LAW:one-source-of-truth] The one walk: what `Drawn.cells` lists and what
204
- * the render hands the effect are read off it.
258
+ * Each glyph of each of `segments`, where its cell sits: rows and columns
259
+ * from the top-left of the output, a wide glyph one cell at its first
260
+ * column, and its anchor shifted to that column as `Style.shiftedBy` shifts
261
+ * a run's. A control segment draws nothing and has none.
262
+ * [LAW:one-source-of-truth] The one walk: what `Drawn.cells` lists, what the
263
+ * render hands a colour effect, and what a glyph effect (`./glyph-effect.ts`)
264
+ * is handed are all read off it.
205
265
  */
206
- function walk(looked) {
266
+ export function walkGlyphs(segments) {
207
267
  let row = 0;
208
268
  let col = 0;
209
- return looked.map(({ segment, look }) => {
210
- if (look === undefined)
211
- return { segment, look, glyphs: [] };
212
- const anchor = look.style.anchor;
269
+ return segments.map((segment) => {
270
+ if (segment.isControl)
271
+ return [];
272
+ const anchor = segment.style?.anchor;
213
273
  // Cells into the segment, as `base.shiftedBy` counts them in the render.
214
274
  let into = 0;
215
- const glyphs = [...graphemes(segment.text)].map((glyph) => {
275
+ return [...graphemes(segment.text)].map((glyph) => {
216
276
  const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
217
277
  [row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + cellLen(glyph)];
218
278
  into += cellLen(glyph);
219
279
  return { glyph, cell };
220
280
  });
221
- return { segment, look, glyphs };
222
281
  });
223
282
  }
283
+ /**
284
+ * Whether `text` draws ink: anything but blanks. [LAW:one-source-of-truth]
285
+ * The one test, so the words a colour effect lights are the words a glyph
286
+ * effect (`./glyph-effects.ts`) scrambles and its particles pass behind.
287
+ */
288
+ export function inked(text) {
289
+ return text.trim() !== "";
290
+ }
291
+ /** `looked` with each segment's glyphs placed, by the one walk. */
292
+ function walk(looked) {
293
+ const placed = walkGlyphs(looked.map(({ segment }) => segment));
294
+ return looked.map((item, i) => ({ ...item, glyphs: placed[i] }));
295
+ }
224
296
  /** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
225
297
  function drawsLines(style) {
226
298
  return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
@@ -239,9 +311,9 @@ function measured(looked, walked, depth, theme) {
239
311
  return [];
240
312
  // A blank glyph shows its ground, and any line its style draws in its ink.
241
313
  const blankShows = look.pair.bg.hex !== ground || drawsLines(look.style);
242
- return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (glyph.trim() !== "" || blankShows) ? [cell] : []));
314
+ return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (inked(glyph) || blankShows) ? [cell] : []));
243
315
  });
244
- const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text.trim() !== "" && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
316
+ const words = looked.flatMap(({ segment, look }) => (look !== undefined && inked(segment.text) && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
245
317
  const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
246
318
  // [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
247
319
  const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
@@ -254,35 +326,6 @@ function measured(looked, walked, depth, theme) {
254
326
  theme,
255
327
  };
256
328
  }
257
- /**
258
- * `effect` sampled and held on `beat`: drawn at the moment the hold in
259
- * progress began (`Beat.start`), so within a hold every frame is the frame
260
- * at its start, and at each hold's start it moves on. A continuous effect
261
- * given a short curve becomes one that steps — a `wheel` turning a sixth of
262
- * a turn once a second — and at one redraw a second a hold shorter than a
263
- * second is skipped, as frames are. [LAW:no-ambient-temporal-coupling] The
264
- * held time is a function of `t`, so the same `t` draws the same bytes at
265
- * any rate. The frame next changes at a hold's start, and only one the
266
- * sampled effect has moved by: the first hold start at or after what it
267
- * answers for the frame held now (`Beat.end`). An effect that moves every
268
- * frame makes the beat's own answer, the end of this hold; one that is done
269
- * and says never leaves the beat saying never too, since the hold after this
270
- * one would draw the same frame. The answer is read off what the effect
271
- * says, never off a comparison of frames: an effect that steps and is back
272
- * on the same colour by the next hold still turns the beat there, an early
273
- * answer the contract allows.
274
- */
275
- export function onBeat(effect, beat) {
276
- return withNextChange((t, drawn) => effect(beat.start(t), drawn), (t, drawn) => {
277
- const moved = nextChangeOf(effect, beat.start(t), drawn);
278
- if (moved === Number.POSITIVE_INFINITY)
279
- return moved;
280
- const end = beat.end(t);
281
- // The hold boundaries after `t` are `end`, then each hold's end in turn;
282
- // the frame turns at the first one the effect has moved by.
283
- return moved <= end ? end : beat.start(moved) === moved ? moved : beat.end(moved);
284
- });
285
- }
286
329
  /**
287
330
  * `effect` on the colours any of `owners` drew, itself or through an owner
288
331
  * nested in its output, and every other colour left as it is: one item of a
@@ -302,16 +345,14 @@ export function confined(effect, ...owners) {
302
345
  // The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
303
346
  const owned = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter((cell) => any(ownedOf(cell))) });
304
347
  // The owners' cells change when the effect's frame does.
305
- return withNextChange((t, drawn) => {
306
- const frame = effect(t, owned(drawn));
307
- return (colors, cell) => {
308
- const owns = ownedOf(cell);
309
- if (!any(owns))
310
- return colors;
311
- const moved = frame(colors, cell);
312
- return { fg: owns.fg ? moved.fg : colors.fg, bg: owns.bg ? moved.bg : colors.bg };
313
- };
314
- }, (t, drawn) => nextChangeOf(effect, t, owned(drawn)));
348
+ return (drawn) => Animation.map(effect(owned(drawn)), (frame) => (look, cell) => {
349
+ const owns = ownedOf(cell);
350
+ if (!any(owns))
351
+ return look;
352
+ const moved = frame(look, cell);
353
+ // An attribute is drawn on the glyph, in its ink, so it moves with whoever owns the ink.
354
+ return { fg: owns.fg ? moved.fg : look.fg, bg: owns.bg ? moved.bg : look.bg, attributes: owns.fg ? moved.attributes : look.attributes };
355
+ });
315
356
  }
316
357
  /**
317
358
  * What `segments`, drawn at `depth` on `theme`, draw their words in, measured
@@ -357,6 +398,32 @@ function slotWritesAlike(a, b, foreground) {
357
398
  function written(spec, foreground) {
358
399
  return spec?.getAnsiCodes(foreground).join(";") ?? "";
359
400
  }
401
+ /**
402
+ * The look a frame returned, refused at once when it carries no attributes.
403
+ * [LAW:no-silent-failure] A frame written before an effect moved attributes
404
+ * returns `{ fg, bg }`, and the effects playground runs a visitor's frame with
405
+ * its types stripped, not checked; unrefused, it fails as a TypeError in run
406
+ * merging that names neither the frame nor the contract.
407
+ */
408
+ function returned(look) {
409
+ if (look.attributes === undefined) {
410
+ throw new TypeError("an effect frame returns the look it was handed with its changes, { ...look, fg, bg }: this one returned no attributes");
411
+ }
412
+ return look;
413
+ }
414
+ /** The attributes a look carries: `ATTRIBUTE_NAMES` but `reverse`. */
415
+ const MOVED = ATTRIBUTE_NAMES.filter((name) => name !== "reverse");
416
+ /**
417
+ * The attributes a look carries, read off anything that sets them — a
418
+ * `Style`, or a look a frame returned with more on it than `CellAttributes`
419
+ * names. [LAW:one-source-of-truth] The one place `reverse` is left out.
420
+ */
421
+ export function cellAttributes(of) {
422
+ return Object.fromEntries(MOVED.map((name) => [name, of[name]]));
423
+ }
424
+ function sameAttributes(a, b) {
425
+ return a === b || MOVED.every((name) => a[name] === b[name]);
426
+ }
360
427
  function sameColors(a, b) {
361
428
  return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
362
429
  }