@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,20 +1,33 @@
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
5
  * An effect is, for what an element draws in a frame, an `Animation`
6
6
  * (`core/animation.ts`) of what is done to its cells: its value at `t` is a
7
- * function from a cell's colours and where the cell is to the cell's new
8
- * colours (`EffectFrame`), and it says when that next changes as every
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
9
  * animation does. Whatever holds for the whole frame is worked out once, in
10
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`
11
+ * function `at(t)` returns, but for those a wrapper keeps back — `confined`
12
12
  * hands over only the cells its owners drew, so an effect never counts on
13
- * seeing them all. Shimmer, pulse, fade and dissolve are all this one shape,
13
+ * seeing them all — and for a strip's join cells painted from the items beside
14
+ * them, which are no one's to paint: a seam's colours are its neighbours'
15
+ * edges, so once the effect has painted every other cell, each such cell is
16
+ * resolved again from the cells beside it as the effect left them
17
+ * (`JoinedCell`, `../core/strip.ts`), the way the joiner drew it. Shimmer,
18
+ * pulse, fade, dissolve and a blinking attribute are all this one shape,
14
19
  * differing by what the function does with its inputs
15
- * [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
16
- * another, which is the same as calling one after the other but for what the
17
- * outer one measures: the inner one's output.
20
+ * [LAW:one-type-per-behavior]; two effects stack by nesting one
21
+ * wrapper in another, which is the same as calling one after the other but
22
+ * for what the outer one measures: the inner one's output.
23
+ *
24
+ * A cell's look is one value, its colours and its attributes together
25
+ * (`CellLook`), rather than an attribute stage beside the colour one, so
26
+ * everything that takes an `Effect` — `confined`, `onBeat`,
27
+ * `Animation.all`, nesting over a glyph effect — takes one that moves
28
+ * attributes with no second copy of itself. `reverse` is the one attribute
29
+ * a look leaves out (`CellAttributes`): it decides which of a style's slots
30
+ * a colour on screen is in, and a swap of the two is already a colour move.
18
31
  *
19
32
  * What the element draws (`Drawn`) — the cells the effect will be handed,
20
33
  * and the colours of its words — is measured here, in the render, off the
@@ -23,10 +36,12 @@
23
36
  * against (`onColors`, `./effects.ts`), so no caller derives it a second time
24
37
  * from a render of its own.
25
38
  *
26
- * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
27
- * clock read. Whoever draws frames builds the view for each frame's `t`, so
28
- * the same `t` always draws the same bytes, at 30 frames a second or one
29
- * every two seconds.
39
+ * An `Effected` is the element under its effect over time: an
40
+ * `Animation<Renderable>`, placed wherever a renderable goes and drawn at
41
+ * the time of the frame it is drawn in. Its value at `t` is the element
42
+ * through the effect at `t` (`Recolored`). [LAW:no-ambient-temporal-coupling]
43
+ * The time is the frame's, never a clock read, so the same time always draws
44
+ * the same bytes, at 30 frames a second or one every two seconds.
30
45
  *
31
46
  * What an effect may not do is the contract this module holds:
32
47
  *
@@ -36,31 +51,44 @@
36
51
  * segment's link and meta and carries its anchor shifted to the run's first
37
52
  * cell (`Style.shiftedBy`), so hit testing still finds the owner.
38
53
  * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
39
- * and a segment whose every cell is unchanged is yielded with its look as
40
- * it was, so the identity effect is byte-identical; what it gains is when
41
- * the frame next changes (`Style.nextChange`), which writes no byte.
54
+ * one whose attributes come back as its style set them keeps them, and a
55
+ * segment whose every cell is unchanged is yielded with its look as
56
+ * it was, so the identity effect is byte-identical; what an `Effected` gains is when
57
+ * the frame next changes (`Style.hold`), which writes no byte.
42
58
  * Neighbouring cells the output depth
43
59
  * writes alike stay one segment: a segment is cut only where the written
44
- * colours change, so two colours that land on one index are one run.
60
+ * colours or the attributes change, so two colours that land on one index
61
+ * are one run.
45
62
  * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
46
63
  * string, so no frame adds an entry to a parse cache.
47
- * - With no colour output (`colorSystem: null`) there is nothing for an
48
- * effect to change, and the child's segments pass through untouched.
64
+ * - With no colour output (`colorSystem: null`) no style is written at all,
65
+ * attributes included, so there is nothing for an effect to change, and
66
+ * the child's segments pass through untouched.
49
67
  *
50
68
  * Positions are the child's own: row and column count from the top-left of
51
69
  * what the child drew, so a sweep crosses the element, not the screen.
52
70
  */
53
71
  import { drawnBy, shiftAnchor } from "../core/anchor.js";
54
- import { Animation } from "../core/animation.js";
72
+ import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
55
73
  import { cellLen, graphemes } from "../core/cells.js";
56
74
  import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
57
75
  import { fnv1a } from "../core/fnv1a.js";
58
76
  import { Measurement } from "../core/measure.js";
59
77
  import { render } from "../core/protocol.js";
60
78
  import { Segment } from "../core/segment.js";
61
- import { JOIN, colorsOwnedBy } from "../core/strip.js";
62
- import { Style } from "../core/style.js";
63
- export class Effected {
79
+ import { JOIN, joinedCell } from "../core/strip.js";
80
+ import { ATTRIBUTE_NAMES, Style } from "../core/style.js";
81
+ import { finiteTime } from "../core/easing.js";
82
+ /** What drew nothing the effect can change: held forever, as far as the effect says. */
83
+ const UNMOVED = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
84
+ /**
85
+ * An `Effected` at `t`, or an element through an effect at a time an
86
+ * animation's `at` was handed — a lifecycle's time since it entered: `renderable`
87
+ * with its cells' looks passed through `effect` at `t`, saying nothing of
88
+ * when that changes, since whatever placed the animation stamps its hold.
89
+ * [LAW:one-type-per-behavior]
90
+ */
91
+ export class Recolored {
64
92
  renderable;
65
93
  effect;
66
94
  t;
@@ -69,25 +97,20 @@ export class Effected {
69
97
  constructor(renderable, effect, options) {
70
98
  this.renderable = renderable;
71
99
  this.effect = effect;
72
- if (!Number.isFinite(options.t)) {
73
- throw new RangeError(`Effected needs a finite t, got ${options.t}`);
74
- }
75
- this.t = options.t;
100
+ this.t = finiteTime("Effected", options.t);
76
101
  this.key = options.key;
77
102
  this.theme = options.theme;
78
103
  }
79
104
  render(options) {
105
+ return this.moved(options).segments;
106
+ }
107
+ /** The frame the effect draws at `t`, and the hold it says that is in. */
108
+ moved(options) {
80
109
  const segments = render(this.renderable, options);
81
110
  // With no colour output nothing the effect does is written, so the child
82
111
  // is what is shown, and it changes when the child says it does.
83
112
  if (options.colorSystem === null)
84
- return segments;
85
- return this._moved(segments, options);
86
- }
87
- *contents() {
88
- yield this.renderable;
89
- }
90
- *_moved(segments, options) {
113
+ return { segments, hold: UNMOVED };
91
114
  // No depth named is no downgrade, as `drawnColors` reads it.
92
115
  const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
93
116
  // [LAW:one-source-of-truth] The frame is measured before any cell is
@@ -96,50 +119,213 @@ export class Effected {
96
119
  const looked = looks(segments, depth, this.theme);
97
120
  const walked = walk(looked);
98
121
  const animation = this.effect(measured(looked, () => walked, depth, this.theme));
99
- // What the effect draws at `t` is drawn until its next change, and the
100
- // cells say so, under whatever the child already said (the sooner wins).
101
- yield* Segment.applyStyle(this._recolored(walked, animation.at(this.t), depth), new Style({ nextChange: animation.nextChange(this.t) }));
122
+ const shown = shownAt(animation, this.t, options);
123
+ return { segments: this._recolored(walked, shown.value, depth), hold: holdOf(shown, this.t) };
102
124
  }
103
125
  *_recolored(walked, frame, depth) {
104
- for (const { segment, look, glyphs } of walked) {
105
- if (look === undefined) {
106
- yield segment;
107
- continue;
108
- }
109
- const { style: base, wire, slots, pair } = look;
110
- const { terminal } = pair;
111
- const from = { fg: pair.fg, bg: pair.bg };
112
- // [LAW:one-source-of-truth] A run is cells the wire writes alike: each
113
- // cell is drawn at the output depth first and merged on the colours that
114
- // writes, so two truecolors landing on one index stay one run. A cell
115
- // the effect gave its neighbour's colours is drawn as that neighbour was.
116
- const runs = [];
117
- for (const { glyph, cell } of glyphs) {
118
- // Back from the screen to the style's slots: the same swap undoes itself.
119
- const colors = cell === undefined ? slots : onScreen(frame(from, { ...cell, seed: this.seed(cell.row, cell.col), terminal, reverse: pair.reverse }), pair.reverse);
120
- const last = runs[runs.length - 1];
121
- const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
122
- if (last !== undefined && writesAlike(last.drawn, writes)) {
123
- last.text += glyph;
124
- last.cells += cellLen(glyph);
125
- last.colors = colors;
126
- }
127
- else {
128
- runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn: writes });
129
- }
130
- }
131
- if (runs.every((run) => writesAlike(run.drawn, wire))) {
126
+ // [LAW:one-source-of-truth] Every cell but a join cell that follows its
127
+ // neighbours is painted first, so that cell is resolved from the cells
128
+ // beside it as the effect left them, wherever in the frame they fall.
129
+ const drawings = walked.map(({ look, glyphs }) => {
130
+ if (look === undefined)
131
+ return undefined;
132
+ const joined = joinedCell(look.style.anchor);
133
+ if (joined?.follows === true)
134
+ return { joined };
135
+ const from = { fg: look.pair.fg, bg: look.pair.bg, attributes: look.attributes };
136
+ const { terminal, reverse } = look.pair;
137
+ return { painted: glyphs.map(({ cell }) => (cell === undefined ? from : returned(frame(from, { ...cell, seed: cellSeed(this.key, cell.row, cell.col), terminal, reverse })))) };
138
+ });
139
+ const beside = edgesBeside(walked, drawings);
140
+ for (const [i, { segment, look, glyphs }] of walked.entries()) {
141
+ const drawing = drawings[i];
142
+ if (look === undefined || drawing === undefined)
132
143
  yield segment;
133
- continue;
144
+ else if ("joined" in drawing)
145
+ yield this._rejoined(segment, look, drawing.joined, glyphs, beside, frame, depth);
146
+ else
147
+ yield* this._repainted(segment, look, glyphs, drawing.painted, depth);
148
+ }
149
+ }
150
+ /** `segment`'s cells drawn in the looks the effect painted them. */
151
+ *_repainted(segment, look, glyphs, moved, depth) {
152
+ const { style: base, wire, slots, pair, attributes } = look;
153
+ // [LAW:one-source-of-truth] A run is cells the wire writes alike: each
154
+ // cell is drawn at the output depth first and merged on the colours that
155
+ // writes and on its attributes, so two truecolors landing on one index
156
+ // stay one run. A cell the effect gave its neighbour's look is drawn as
157
+ // that neighbour was.
158
+ const runs = [];
159
+ for (const [k, { glyph }] of glyphs.entries()) {
160
+ const now = moved[k];
161
+ // Back from the screen to the style's slots: the same swap undoes itself.
162
+ const colors = onScreen(now, pair.reverse);
163
+ const last = runs[runs.length - 1];
164
+ const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
165
+ if (last !== undefined && writesAlike(last.drawn, writes) && sameAttributes(last.attributes, now.attributes)) {
166
+ last.text += glyph;
167
+ last.cells += cellLen(glyph);
168
+ last.colors = colors;
134
169
  }
135
- let offset = 0;
136
- for (const run of runs) {
137
- const shifted = base.shiftedBy(offset);
138
- const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
139
- yield new Segment(run.text, style);
140
- offset += run.cells;
170
+ else {
171
+ runs.push({ text: glyph, cells: cellLen(glyph), colors, attributes: now.attributes, drawn: writes });
141
172
  }
142
173
  }
174
+ if (runs.every((run) => writesAlike(run.drawn, wire) && sameAttributes(run.attributes, attributes))) {
175
+ yield segment;
176
+ return;
177
+ }
178
+ let offset = 0;
179
+ for (const run of runs) {
180
+ const placed = base.shiftedBy(offset);
181
+ const shifted = sameAttributes(run.attributes, attributes) ? placed : placed.withAttributes(cellAttributes(run.attributes));
182
+ const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
183
+ yield new Segment(run.text, style);
184
+ offset += run.cells;
185
+ }
186
+ }
187
+ /**
188
+ * A join cell's `segment` resolved again from the edges beside it, as the
189
+ * effect left them: each neighbour's nearest cell to the join on its row,
190
+ * its slots as the depth writes them. A side whose neighbour the effect moved
191
+ * nothing of keeps the edge the cell was drawn from, so with neither moved
192
+ * the cell is as it was drawn.
193
+ */
194
+ _rejoined(segment, look, joined, glyphs, beside, frame, depth) {
195
+ const cells = glyphs.flatMap(({ cell }) => (cell === undefined ? [] : [cell]));
196
+ // A join cell that draws no cell shows nothing an effect could move.
197
+ const [at] = cells;
198
+ if (at === undefined)
199
+ return segment;
200
+ const { theme } = this;
201
+ // [LAW:one-source-of-truth] A moved edge is the spec the depth writes its
202
+ // slot in, the spec the joiner reads of an item drawn so: a palette slot
203
+ // stays that slot, which the lift tells apart by number, never an RGB read
204
+ // back through a palette the writer did not use. Only the terminal's own
205
+ // colour names none, and the terminal the effect draws on says what it is.
206
+ // Each such spec is kept, so the writer below knows it by identity: an
207
+ // RGB a blend or another colour lands on is not the terminal's own.
208
+ const terminals = new Set();
209
+ const edgeOf = (spec, foreground) => {
210
+ if (!spec?.isDefault)
211
+ return spec;
212
+ const own = ColorSpec.fromRgba(spec.getTruecolor(theme, foreground));
213
+ terminals.add(own);
214
+ return own;
215
+ };
216
+ const moved = (from, to) => {
217
+ const specs = movedSpecs(from, to, depth, theme);
218
+ return specs.fg === undefined && specs.bg === undefined ? undefined : { color: edgeOf(specs.fg, true), bgcolor: edgeOf(specs.bg, false) };
219
+ };
220
+ // [LAW:one-source-of-truth] A neighbour the frame shows no cell of beside
221
+ // the join (cut off by a narrower container, scrolled out of a `Viewport`,
222
+ // covered, or an item that draws nothing) is painted by the effect where
223
+ // the seam shows it: its edge, as the seam drew it, handed to the frame at
224
+ // the seam's own cell. An effect over the whole element moves it as it
225
+ // moves everything there; one confined to some owners leaves it, since no
226
+ // owner drew the seam. Where the frame is whole and every item draws, each
227
+ // neighbour a seam reads has a cell on its row, so only a partial frame
228
+ // comes here [LAW:no-silent-failure].
229
+ const unseen = (edge) => {
230
+ const wire = edge.drawnColors(depth);
231
+ const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
232
+ const terminal = { fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true };
233
+ return moved(slots, returned(frame({ ...slots, attributes: cellAttributes(edge) }, { ...at, seed: cellSeed(this.key, at.row, at.col), terminal, reverse: false })));
234
+ };
235
+ const moveOf = (side) => {
236
+ const read = joined.reads[side];
237
+ if (read === null)
238
+ return undefined;
239
+ const cell = beside(cells, side, read.owner);
240
+ return cell === undefined ? unseen(read.edge) : moved(cell.look.slots, onScreen(cell.moved, cell.look.pair.reverse));
241
+ };
242
+ const rederived = joined.rederived({ left: moveOf("left"), right: moveOf("right") }, look.style, depth);
243
+ if (rederived === undefined)
244
+ return segment;
245
+ const { colors, style } = rederived;
246
+ // [LAW:one-source-of-truth] Each slot is written as the depth writes what
247
+ // it resolved to, by the writer the lift measured it with
248
+ // (`Style.drawnColors`), so a lifted ink stands off its ground in the bytes
249
+ // as it did when measured. A slot resolved to the colour it was before
250
+ // keeps the spec it was written in, as a painted cell's unmoved slot does.
251
+ // The terminal's own colours, which stand in a moved edge as the spec of
252
+ // the RGB the effect's terminal gives them and reach a slot unchanged or
253
+ // not at all, are written by the writer that wrote them on the item
254
+ // beside, on that terminal, which writes its own ground below truecolor as
255
+ // `default`.
256
+ const was = joined.style;
257
+ const drawn = colors.drawnColors(depth);
258
+ const { wire, slots } = look;
259
+ const rounded = this.respec(wire, slots, { fg: colors.color?.getTruecolor(theme, true) ?? slots.fg, bg: colors.bgcolor?.getTruecolor(theme, false) ?? slots.bg }, depth);
260
+ const written = (now, before, wrote, drew, round) => {
261
+ if (sameSpec(now, before))
262
+ return wrote;
263
+ // [LAW:no-silent-failure] Unreachable: a move only fills an edge in, so
264
+ // a slot that resolved to a colour when drawn resolves to one after.
265
+ if (now === undefined)
266
+ throw new Error(`a seam cell resolved to ${before?.name ?? "nothing"} when drawn and to nothing after an effect moved its neighbours`);
267
+ return terminals.has(now) ? round : drew;
268
+ };
269
+ const writes = {
270
+ color: written(colors.color, was.color, wire.color, drawn.color, rounded.color),
271
+ bgcolor: written(colors.bgcolor, was.bgcolor, wire.bgcolor, drawn.bgcolor, rounded.bgcolor),
272
+ };
273
+ return new Segment(segment.text, writesAlike(writes, wire) ? style : style.add(Style.fromColor(writes.color, writes.bgcolor)));
274
+ }
275
+ measure(options) {
276
+ return Measurement.get(options, this.renderable);
277
+ }
278
+ /**
279
+ * The specs that draw a cell's slot colours `to` where the segment drew
280
+ * `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
281
+ * in, the rest as `wire` wrote them.
282
+ */
283
+ respec(wire, from, to, depth) {
284
+ const moved = movedSpecs(from, to, depth, this.theme);
285
+ return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
286
+ }
287
+ }
288
+ /**
289
+ * `renderable` under `effect` over time: an `Animation<Renderable>` whose
290
+ * value at `t` is the element through the effect at `t`, placed wherever a
291
+ * renderable goes. Its own answer to when it changes is `t`, at every time:
292
+ * an `Effect` is made from what the element draws, so with nothing drawn
293
+ * nothing can say more — early, never false. Placed, it says exactly: its
294
+ * cells carry the effect's own hold at the frame's time and the output's
295
+ * depth (`stampFrame`), under whatever the element already said, so at a
296
+ * depth that shows no motion a loop says it never changes there and the
297
+ * host sleeps.
298
+ */
299
+ export class Effected {
300
+ renderable;
301
+ effect;
302
+ key;
303
+ theme;
304
+ constructor(renderable, effect, options) {
305
+ this.renderable = renderable;
306
+ this.effect = effect;
307
+ this.key = options.key;
308
+ this.theme = options.theme;
309
+ }
310
+ /** The element through the effect at `t`, unstamped. */
311
+ at(t) {
312
+ return new Recolored(this.renderable, this.effect, { t, key: this.key, theme: this.theme });
313
+ }
314
+ nextChange(t) {
315
+ return t;
316
+ }
317
+ lastChange(t) {
318
+ return t;
319
+ }
320
+ /** The element under the effect with its motion taken out. */
321
+ reduced() {
322
+ const { effect } = this;
323
+ return new Effected(this.renderable, (drawn) => effect(drawn).reduced(), this);
324
+ }
325
+ /** Drawn at the frame's time, its cells stamped with the effect's exact hold there. */
326
+ render(options) {
327
+ const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
328
+ return stampFrame(segments, hold);
143
329
  }
144
330
  measure(options) {
145
331
  return Measurement.get(options, this.renderable);
@@ -156,19 +342,14 @@ export class Effected {
156
342
  const moved = movedSpecs(from, onScreen(to, pair.reverse), depth, theme);
157
343
  return onScreen({ fg: moved.fg?.getTruecolor(theme, true) ?? from.fg, bg: moved.bg?.getTruecolor(theme, false) ?? from.bg }, pair.reverse);
158
344
  }
159
- /**
160
- * The specs that draw a cell's slot colours `to` where the segment drew
161
- * `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
162
- * in, the rest as `wire` wrote them.
163
- */
164
- respec(wire, from, to, depth) {
165
- const moved = movedSpecs(from, to, depth, this.theme);
166
- return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
167
- }
168
- /** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
169
- seed(row, col) {
170
- return Number.parseInt(fnv1a(`${this.key}:${row}:${col}`), 16) / 0x1_0000_0000;
171
- }
345
+ }
346
+ /**
347
+ * A cell's seed: FNV-1a of the element's key and the cell's position, scaled
348
+ * into [0, 1). [LAW:one-source-of-truth] One rule for a colour effect's seeds
349
+ * and a glyph effect's, so one element under both twinkles from one seed.
350
+ */
351
+ export function cellSeed(key, row, col) {
352
+ return Number.parseInt(fnv1a(`${key}:${row}:${col}`), 16) / 0x1_0000_0000;
172
353
  }
173
354
  const DEFAULT = ColorSpec.default();
174
355
  /** Each of `segments` with how it is drawn at `depth` on `theme`; a control segment draws nothing. */
@@ -181,34 +362,87 @@ function looks(segments, depth, theme) {
181
362
  const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
182
363
  const reverse = style.reverse === true;
183
364
  const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
184
- return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse } } };
365
+ return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse }, attributes: cellAttributes(style) } };
185
366
  });
186
367
  }
187
368
  /**
188
- * Each glyph of `looked`, where its cell sits: rows and columns from the
189
- * top-left of the output, a wide glyph one cell at its first column, and its
190
- * anchor shifted to that column as `Style.shiftedBy` shifts a run's.
191
- * [LAW:one-source-of-truth] The one walk: what `Drawn.cells` lists and what
192
- * the render hands the effect are read off it.
369
+ * Each glyph of each of `segments`, where its cell sits: rows and columns
370
+ * from the top-left of the output, a wide glyph one cell at its first
371
+ * column, and its anchor shifted to that column as `Style.shiftedBy` shifts
372
+ * a run's. A control segment draws nothing and has none.
373
+ * [LAW:one-source-of-truth] The one walk: what `Drawn.cells` lists, what the
374
+ * render hands a colour effect, and what a glyph effect (`./glyph-effect.ts`)
375
+ * is handed are all read off it.
193
376
  */
194
- function walk(looked) {
377
+ export function walkGlyphs(segments) {
195
378
  let row = 0;
196
379
  let col = 0;
197
- return looked.map(({ segment, look }) => {
198
- if (look === undefined)
199
- return { segment, look, glyphs: [] };
200
- const anchor = look.style.anchor;
380
+ return segments.map((segment) => {
381
+ if (segment.isControl)
382
+ return [];
383
+ const anchor = segment.style?.anchor;
201
384
  // Cells into the segment, as `base.shiftedBy` counts them in the render.
202
385
  let into = 0;
203
- const glyphs = [...graphemes(segment.text)].map((glyph) => {
386
+ return [...graphemes(segment.text)].map((glyph) => {
204
387
  const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
205
388
  [row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + cellLen(glyph)];
206
389
  into += cellLen(glyph);
207
390
  return { glyph, cell };
208
391
  });
209
- return { segment, look, glyphs };
210
392
  });
211
393
  }
394
+ /**
395
+ * Whether `text` draws ink: anything but blanks. [LAW:one-source-of-truth]
396
+ * The one test, so the words a colour effect lights are the words a glyph
397
+ * effect (`./glyph-effects.ts`) scrambles and its particles pass behind.
398
+ */
399
+ export function inked(text) {
400
+ return text.trim() !== "";
401
+ }
402
+ /** `looked` with each segment's glyphs placed, by the one walk. */
403
+ function walk(looked) {
404
+ const placed = walkGlyphs(looked.map(({ segment }) => segment));
405
+ return looked.map((item, i) => ({ ...item, glyphs: placed[i] }));
406
+ }
407
+ /**
408
+ * Where to find the cells beside a join in `walked`, painted as `painted` says.
409
+ * Read by owner, never by column alone: a `FlexStrip` can set a gap or a
410
+ * justified fill between a seam and the items it joins.
411
+ */
412
+ function edgesBeside(walked, drawings) {
413
+ let rows;
414
+ const index = () => {
415
+ const byRow = new Map();
416
+ walked.forEach(({ look, glyphs }, i) => {
417
+ const drawing = drawings[i];
418
+ if (look === undefined || drawing === undefined || !("painted" in drawing))
419
+ return;
420
+ const moved = drawing.painted;
421
+ glyphs.forEach(({ cell }, k) => {
422
+ if (cell === undefined)
423
+ return;
424
+ const row = byRow.get(cell.row) ?? [];
425
+ row.push({ col: cell.col, anchor: cell.anchor, look, moved: moved[k] });
426
+ byRow.set(cell.row, row);
427
+ });
428
+ });
429
+ return byRow;
430
+ };
431
+ return (cells, side, owner) => {
432
+ // Cells on a row are listed left to right, as the walk places them.
433
+ const row = (rows ??= index()).get(cells[0].row) ?? [];
434
+ const first = cells[0].col;
435
+ const last = cells[cells.length - 1].col;
436
+ if (side === "right")
437
+ return row.find((cell) => cell.col > last && drawnBy(cell.anchor, owner));
438
+ for (let k = row.length - 1; k >= 0; k--) {
439
+ const cell = row[k];
440
+ if (cell.col < first && drawnBy(cell.anchor, owner))
441
+ return cell;
442
+ }
443
+ return undefined;
444
+ };
445
+ }
212
446
  /** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
213
447
  function drawsLines(style) {
214
448
  return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
@@ -227,9 +461,9 @@ function measured(looked, walked, depth, theme) {
227
461
  return [];
228
462
  // A blank glyph shows its ground, and any line its style draws in its ink.
229
463
  const blankShows = look.pair.bg.hex !== ground || drawsLines(look.style);
230
- return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (glyph.trim() !== "" || blankShows) ? [cell] : []));
464
+ return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (inked(glyph) || blankShows) ? [cell] : []));
231
465
  });
232
- const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text.trim() !== "" && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
466
+ const words = looked.flatMap(({ segment, look }) => (look !== undefined && inked(segment.text) && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
233
467
  const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
234
468
  // [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
235
469
  const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
@@ -243,69 +477,28 @@ function measured(looked, walked, depth, theme) {
243
477
  };
244
478
  }
245
479
  /**
246
- * `effect` sampled and held on `beat`: drawn at the moment the hold in
247
- * progress began (`Beat.lastChange`), so within a hold every frame is the frame
248
- * at its start, and at each hold's start it moves on. A continuous effect
249
- * given a short curve becomes one that steps — a `wheel` turning a sixth of
250
- * a turn once a second — and at one redraw a second a hold shorter than a
251
- * second is skipped, as frames are. [LAW:no-ambient-temporal-coupling] The
252
- * held time is a function of `t`, so the same `t` draws the same bytes at
253
- * any rate. The frame next changes at a hold's start, and only one the
254
- * sampled effect has moved by: the first hold start at or after what it
255
- * answers for the frame held now (`Beat.nextChange`). An effect that moves
256
- * every frame makes the beat's own answer, the end of this hold; one that is
257
- * done and says never leaves the beat saying never too, since the hold after
258
- * this one would draw the same frame. The answer is read off what the effect
259
- * says, never off a comparison of frames: an effect that steps and is back
260
- * on the same colour by the next hold still turns the beat there, an early
261
- * answer the contract allows.
262
- */
263
- export function onBeat(effect, beat) {
264
- return (drawn) => {
265
- const sampled = effect(drawn);
266
- return {
267
- at: (t) => sampled.at(beat.lastChange(t)),
268
- nextChange(t) {
269
- const moved = sampled.nextChange(beat.lastChange(t));
270
- if (moved === Number.POSITIVE_INFINITY)
271
- return moved;
272
- const end = beat.nextChange(t);
273
- // The hold boundaries after `t` are `end`, then each hold's end in turn;
274
- // the frame turns at the first one the effect has moved by.
275
- return moved <= end ? end : beat.lastChange(moved) === moved ? moved : beat.nextChange(moved);
276
- },
277
- // The frame held now began at this hold's start at the latest: an early
278
- // answer where the effect had not moved by it, which the contract allows.
279
- lastChange: (t) => beat.lastChange(t),
280
- };
281
- };
282
- }
283
- /**
284
- * `effect` on the colours any of `owners` drew, itself or through an owner
285
- * nested in its output, and every other colour left as it is: one item of a
480
+ * `effect` on the cells any of `owners` drew, itself or through an owner
481
+ * nested in its output, and every other cell left as it is: one item of a
286
482
  * strip whose items share a fill, the items of a row one other item stands
287
- * apart from, one widget of a screen. A seam cell shows its two neighbours
288
- * side by side, a powerline arrow in the fill it points out of over the fill
289
- * it points into, so each side moves with its own item (`colorsOwnedBy`,
290
- * `../core/strip.ts`). A cell none of them drew is never handed to the
483
+ * apart from, one widget of a screen. A strip's join cell is no item's, so it
484
+ * is never handed; one painted from the items beside it follows them, the
485
+ * moved ones included (`Recolored`). A cell none of them drew is never handed to the
291
486
  * effect, so confining one effect to many owners costs one call a cell, not
292
487
  * one for each owner.
293
488
  */
294
489
  export function confined(effect, ...owners) {
295
490
  const confinedTo = new Set(owners);
296
- // [LAW:single-enforcer] One reading of which slots are owned, for the cells listed and the cells handed alike.
297
- const ownedOf = (cell) => colorsOwnedBy(cell.anchor, confinedTo);
298
- const any = ({ fg, bg }) => fg || bg;
491
+ // [LAW:single-enforcer] One reading of whose a cell is, for the cells listed and the cells handed alike.
492
+ const owned = (cell) => {
493
+ for (let level = cell.anchor; level; level = level.inner)
494
+ if (confinedTo.has(level.owner))
495
+ return true;
496
+ return false;
497
+ };
299
498
  // The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
300
- const owned = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter((cell) => any(ownedOf(cell))) });
499
+ const narrowed = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter(owned) });
301
500
  // The owners' cells change when the effect's frame does.
302
- return (drawn) => Animation.map(effect(owned(drawn)), (frame) => (colors, cell) => {
303
- const owns = ownedOf(cell);
304
- if (!any(owns))
305
- return colors;
306
- const moved = frame(colors, cell);
307
- return { fg: owns.fg ? moved.fg : colors.fg, bg: owns.bg ? moved.bg : colors.bg };
308
- });
501
+ return (drawn) => Animation.map(effect(narrowed(drawn)), (frame) => (look, cell) => (owned(cell) ? frame(look, cell) : look));
309
502
  }
310
503
  /**
311
504
  * What `segments`, drawn at `depth` on `theme`, draw their words in, measured
@@ -351,6 +544,36 @@ function slotWritesAlike(a, b, foreground) {
351
544
  function written(spec, foreground) {
352
545
  return spec?.getAnsiCodes(foreground).join(";") ?? "";
353
546
  }
547
+ /**
548
+ * The look a frame returned, refused at once when it carries no attributes.
549
+ * [LAW:no-silent-failure] A frame written before an effect moved attributes
550
+ * returns `{ fg, bg }`, and the effects playground runs a visitor's frame with
551
+ * its types stripped, not checked; unrefused, it fails as a TypeError in run
552
+ * merging that names neither the frame nor the contract.
553
+ */
554
+ function returned(look) {
555
+ if (look.attributes === undefined) {
556
+ throw new TypeError("an effect frame returns the look it was handed with its changes, { ...look, fg, bg }: this one returned no attributes");
557
+ }
558
+ return look;
559
+ }
560
+ /** The attributes a look carries: `ATTRIBUTE_NAMES` but `reverse`. */
561
+ const MOVED = ATTRIBUTE_NAMES.filter((name) => name !== "reverse");
562
+ /**
563
+ * The attributes a look carries, read off anything that sets them — a
564
+ * `Style`, or a look a frame returned with more on it than `CellAttributes`
565
+ * names. [LAW:one-source-of-truth] The one place `reverse` is left out.
566
+ */
567
+ export function cellAttributes(of) {
568
+ return Object.fromEntries(MOVED.map((name) => [name, of[name]]));
569
+ }
570
+ function sameAttributes(a, b) {
571
+ return a === b || MOVED.every((name) => a[name] === b[name]);
572
+ }
573
+ /** Whether two specs are one colour as written: the same kind, naming the same colour. */
574
+ function sameSpec(a, b) {
575
+ return a === b || (a !== undefined && b !== undefined && a.type === b.type && a.name === b.name);
576
+ }
354
577
  function sameColors(a, b) {
355
578
  return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
356
579
  }