@promptctl/rich-js 0.27.0 → 0.28.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 (110) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +36 -17
  4. package/dist/core/animation.js +61 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +93 -5
  17. package/dist/core/color.d.ts +6 -7
  18. package/dist/core/color.js +11 -13
  19. package/dist/core/console.d.ts +18 -1
  20. package/dist/core/console.js +24 -5
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/env.d.ts +6 -0
  24. package/dist/core/env.js +9 -1
  25. package/dist/core/export-html.js +4 -3
  26. package/dist/core/export-lines.d.ts +2 -0
  27. package/dist/core/export-lines.js +7 -3
  28. package/dist/core/export-svg.js +6 -5
  29. package/dist/core/json.js +9 -2
  30. package/dist/core/markup.js +4 -3
  31. package/dist/core/orient.d.ts +68 -0
  32. package/dist/core/orient.js +224 -0
  33. package/dist/core/osc8.d.ts +12 -5
  34. package/dist/core/osc8.js +14 -7
  35. package/dist/core/pretty.js +22 -18
  36. package/dist/core/protocol.d.ts +27 -6
  37. package/dist/core/protocol.js +14 -6
  38. package/dist/core/render.d.ts +8 -3
  39. package/dist/core/render.js +18 -5
  40. package/dist/core/seal.d.ts +26 -0
  41. package/dist/core/seal.js +75 -0
  42. package/dist/core/segment.d.ts +9 -1
  43. package/dist/core/segment.js +11 -1
  44. package/dist/core/style.d.ts +29 -1
  45. package/dist/core/style.js +41 -5
  46. package/dist/core/text-order.d.ts +51 -0
  47. package/dist/core/text-order.js +50 -0
  48. package/dist/core/text.d.ts +10 -0
  49. package/dist/core/text.js +133 -87
  50. package/dist/core/timeline.js +6 -6
  51. package/dist/core/view.d.ts +1 -1
  52. package/dist/core/view.js +5 -4
  53. package/dist/core/wrap.d.ts +24 -6
  54. package/dist/core/wrap.js +82 -16
  55. package/dist/host/app.d.ts +44 -3
  56. package/dist/host/app.js +111 -6
  57. package/dist/host/index.d.ts +3 -2
  58. package/dist/host/index.js +1 -0
  59. package/dist/host/input.d.ts +35 -2
  60. package/dist/host/input.js +88 -4
  61. package/dist/host/terminal-colors.d.ts +18 -0
  62. package/dist/host/terminal-colors.js +46 -0
  63. package/dist/host/terminal-host.d.ts +2 -1
  64. package/dist/index.d.ts +5 -4
  65. package/dist/index.js +2 -2
  66. package/dist/renderables/change.d.ts +1 -3
  67. package/dist/renderables/change.js +4 -6
  68. package/dist/renderables/effect.d.ts +60 -59
  69. package/dist/renderables/effect.js +91 -81
  70. package/dist/renderables/effects.d.ts +37 -8
  71. package/dist/renderables/effects.js +185 -52
  72. package/dist/renderables/frames.d.ts +1 -10
  73. package/dist/renderables/frames.js +1 -16
  74. package/dist/renderables/glyph-effect.d.ts +12 -13
  75. package/dist/renderables/glyph-effect.js +34 -32
  76. package/dist/renderables/glyph-effects.d.ts +27 -8
  77. package/dist/renderables/glyph-effects.js +75 -7
  78. package/dist/renderables/lifecycle.d.ts +5 -7
  79. package/dist/renderables/lifecycle.js +6 -7
  80. package/dist/renderables/markdown.js +4 -2
  81. package/dist/renderables/panel.js +6 -4
  82. package/dist/renderables/progressBar.d.ts +3 -3
  83. package/dist/renderables/progressBar.js +16 -13
  84. package/dist/renderables/syntax.js +7 -2
  85. package/dist/renderables/table.js +2 -1
  86. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  87. package/dist/template-bindings/animation-funcs.js +115 -32
  88. package/dist/template-bindings/index.d.ts +7 -5
  89. package/dist/template-bindings/index.js +8 -5
  90. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  91. package/dist/template-bindings/palette-funcs.js +1 -1
  92. package/dist/themes/buildPalette.d.ts +3 -2
  93. package/dist/themes/buildPalette.js +2 -2
  94. package/dist/themes/data/index.js +1 -4
  95. package/dist/themes/data/textual-ansi.js +0 -143
  96. package/dist/themes/ramp.d.ts +1 -1
  97. package/dist/themes/ramp.js +1 -1
  98. package/dist/themes/registry.d.ts +6 -8
  99. package/dist/themes/registry.js +2 -4
  100. package/dist/themes/terminalThemes.d.ts +4 -11
  101. package/dist/themes/terminalThemes.js +31 -47
  102. package/dist/widgets/button.d.ts +1 -1
  103. package/dist/widgets/button.js +6 -6
  104. package/dist/widgets/checkbox.js +4 -4
  105. package/dist/widgets/dropdown.d.ts +1 -1
  106. package/dist/widgets/dropdown.js +7 -5
  107. package/dist/widgets/themed-widget.js +3 -3
  108. package/dist/widgets/toggle.d.ts +1 -1
  109. package/dist/widgets/toggle.js +5 -5
  110. package/package.json +5 -4
@@ -17,9 +17,10 @@
17
17
  * (`JoinedCell`, `../core/strip.ts`), the way the joiner drew it. Shimmer,
18
18
  * pulse, fade, dissolve and a blinking attribute are all this one shape,
19
19
  * differing by what the function does with its inputs
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.
20
+ * [LAW:one-type-per-behavior]; two effects stack as layers of one effect
21
+ * (`layered`, `./effects.ts`), each moving what the last left and recognising
22
+ * a cell by what the element drew (`EffectCell.pair`), or by nesting one
23
+ * wrapper in another, which measures the inner one's output instead.
23
24
  *
24
25
  * A cell's look is one value, its colours and its attributes together
25
26
  * (`CellLook`), rather than an attribute stage beside the colour one, so
@@ -69,60 +70,67 @@
69
70
  * what the child drew, so a sweep crosses the element, not the screen.
70
71
  */
71
72
  import { drawnBy, shiftAnchor } from "../core/anchor.js";
72
- import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
73
+ import { Animation, holdOf, placedRender, shownAt } from "../core/animation.js";
73
74
  import { cellLen, graphemes } from "../core/cells.js";
74
- import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
75
+ import { ColorDepth, ColorSpec, resolveTerminal, SURFACE_BLACK } from "../core/color.js";
75
76
  import { fnv1a } from "../core/fnv1a.js";
76
77
  import { Measurement } from "../core/measure.js";
77
78
  import { render } from "../core/protocol.js";
78
79
  import { Segment } from "../core/segment.js";
79
80
  import { JOIN, joinedCell } from "../core/strip.js";
80
- import { ATTRIBUTE_NAMES, Style } from "../core/style.js";
81
+ import { ATTRIBUTE_NAMES, NEVER_CHANGES, Style } from "../core/style.js";
81
82
  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 };
83
+ /**
84
+ * The hold an effect drawn at `t` tells: its animation's there, or held for
85
+ * good over no cell, since it changes how cells look and never which exist.
86
+ * What drew none says for itself when it will (a lifecycle not yet entered).
87
+ */
88
+ export function holdOver(anyCell, shown, t) {
89
+ return anyCell ? holdOf(shown, t) : NEVER_CHANGES;
90
+ }
84
91
  /**
85
92
  * An `Effected` at `t`, or an element through an effect at a time an
86
93
  * 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]
94
+ * with its cells' looks passed through `effect` at `t`. Its cells say nothing
95
+ * of when that changes, since `t` is not the frame's clock: it tells the hold
96
+ * it drew in (`RenderOptions.onMoved`), and whatever placed the animation
97
+ * carries that onto the frame's and stamps it. [LAW:one-type-per-behavior]
90
98
  */
91
99
  export class Recolored {
92
100
  renderable;
93
101
  effect;
94
- t;
102
+ _t;
95
103
  key;
96
- theme;
97
104
  constructor(renderable, effect, options) {
98
105
  this.renderable = renderable;
99
106
  this.effect = effect;
100
- this.t = finiteTime("Effected", options.t);
107
+ this._t = finiteTime("Effected", options.t);
101
108
  this.key = options.key;
102
- this.theme = options.theme;
103
109
  }
104
110
  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) {
109
111
  const segments = render(this.renderable, options);
110
112
  // With no colour output nothing the effect does is written, so the child
111
113
  // is what is shown, and it changes when the child says it does.
112
- if (options.colorSystem === null)
113
- return { segments, hold: UNMOVED };
114
+ if (options.colorSystem === null) {
115
+ options.onMoved?.(this._t, this.key, NEVER_CHANGES);
116
+ return segments;
117
+ }
114
118
  // No depth named is no downgrade, as `drawnColors` reads it.
115
119
  const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
120
+ // [LAW:one-source-of-truth] Every colour below resolves on the frame's
121
+ // terminal, so two effects in a frame cannot disagree.
122
+ const terminal = resolveTerminal(options.terminal, depth);
116
123
  // [LAW:one-source-of-truth] The frame is measured before any cell is
117
124
  // handed to the effect, off the same looks its cells are drawn from, and
118
125
  // the one measure is what the effect's animation is made from.
119
- const looked = looks(segments, depth, this.theme);
126
+ const looked = looks(segments, depth, terminal);
120
127
  const walked = walk(looked);
121
- const animation = this.effect(measured(looked, () => walked, depth, this.theme));
122
- const shown = shownAt(animation, this.t, options);
123
- return { segments: this._recolored(walked, shown.value, depth), hold: holdOf(shown, this.t) };
128
+ const drawn = measured(looked, () => walked, depth, terminal);
129
+ const shown = shownAt(this.effect(drawn), this._t, options);
130
+ options.onMoved?.(this._t, this.key, holdOver(walked.some(({ glyphs }) => glyphs.some(({ cell }) => cell !== undefined)), shown, this._t));
131
+ return this._recolored(walked, shown.value, drawn);
124
132
  }
125
- *_recolored(walked, frame, depth) {
133
+ *_recolored(walked, frame, drawn) {
126
134
  // [LAW:one-source-of-truth] Every cell but a join cell that follows its
127
135
  // neighbours is painted first, so that cell is resolved from the cells
128
136
  // beside it as the effect left them, wherever in the frame they fall.
@@ -133,8 +141,7 @@ export class Recolored {
133
141
  if (joined?.follows === true)
134
142
  return { joined };
135
143
  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 })))) };
144
+ return { painted: glyphs.map(({ cell }) => (cell === undefined ? from : returned(frame(from, { ...cell, seed: cellSeed(this.key, cell.row, cell.col), pair: look.pair })))) };
138
145
  });
139
146
  const beside = edgesBeside(walked, drawings);
140
147
  for (const [i, { segment, look, glyphs }] of walked.entries()) {
@@ -142,13 +149,13 @@ export class Recolored {
142
149
  if (look === undefined || drawing === undefined)
143
150
  yield segment;
144
151
  else if ("joined" in drawing)
145
- yield this._rejoined(segment, look, drawing.joined, glyphs, beside, frame, depth);
152
+ yield this._rejoined(segment, look, drawing.joined, glyphs, beside, frame, drawn);
146
153
  else
147
- yield* this._repainted(segment, look, glyphs, drawing.painted, depth);
154
+ yield* this._repainted(segment, look, glyphs, drawing.painted, drawn);
148
155
  }
149
156
  }
150
157
  /** `segment`'s cells drawn in the looks the effect painted them. */
151
- *_repainted(segment, look, glyphs, moved, depth) {
158
+ *_repainted(segment, look, glyphs, moved, drawn) {
152
159
  const { style: base, wire, slots, pair, attributes } = look;
153
160
  // [LAW:one-source-of-truth] A run is cells the wire writes alike: each
154
161
  // cell is drawn at the output depth first and merged on the colours that
@@ -161,7 +168,7 @@ export class Recolored {
161
168
  // Back from the screen to the style's slots: the same swap undoes itself.
162
169
  const colors = onScreen(now, pair.reverse);
163
170
  const last = runs[runs.length - 1];
164
- const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
171
+ const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, drawn);
165
172
  if (last !== undefined && writesAlike(last.drawn, writes) && sameAttributes(last.attributes, now.attributes)) {
166
173
  last.text += glyph;
167
174
  last.cells += cellLen(glyph);
@@ -191,13 +198,13 @@ export class Recolored {
191
198
  * nothing of keeps the edge the cell was drawn from, so with neither moved
192
199
  * the cell is as it was drawn.
193
200
  */
194
- _rejoined(segment, look, joined, glyphs, beside, frame, depth) {
201
+ _rejoined(segment, look, joined, glyphs, beside, frame, measure) {
202
+ const { depth, terminal } = measure;
195
203
  const cells = glyphs.flatMap(({ cell }) => (cell === undefined ? [] : [cell]));
196
204
  // A join cell that draws no cell shows nothing an effect could move.
197
205
  const [at] = cells;
198
206
  if (at === undefined)
199
207
  return segment;
200
- const { theme } = this;
201
208
  // [LAW:one-source-of-truth] A moved edge is the spec the depth writes its
202
209
  // slot in, the spec the joiner reads of an item drawn so: a palette slot
203
210
  // stays that slot, which the lift tells apart by number, never an RGB read
@@ -209,12 +216,12 @@ export class Recolored {
209
216
  const edgeOf = (spec, foreground) => {
210
217
  if (!spec?.isDefault)
211
218
  return spec;
212
- const own = ColorSpec.fromRgba(spec.getTruecolor(theme, foreground));
219
+ const own = ColorSpec.fromRgba(spec.getTruecolor(terminal, foreground));
213
220
  terminals.add(own);
214
221
  return own;
215
222
  };
216
223
  const moved = (from, to) => {
217
- const specs = movedSpecs(from, to, depth, theme);
224
+ const specs = movedSpecs(from, to, depth, terminal);
218
225
  return specs.fg === undefined && specs.bg === undefined ? undefined : { color: edgeOf(specs.fg, true), bgcolor: edgeOf(specs.bg, false) };
219
226
  };
220
227
  // [LAW:one-source-of-truth] A neighbour the frame shows no cell of beside
@@ -228,9 +235,9 @@ export class Recolored {
228
235
  // comes here [LAW:no-silent-failure].
229
236
  const unseen = (edge) => {
230
237
  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 })));
238
+ const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(terminal, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(terminal, false) };
239
+ const own = { fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true };
240
+ return moved(slots, returned(frame({ ...slots, attributes: cellAttributes(edge) }, { ...at, seed: cellSeed(this.key, at.row, at.col), pair: { ...slots, terminal: own, reverse: false } })));
234
241
  };
235
242
  const moveOf = (side) => {
236
243
  const read = joined.reads[side];
@@ -256,7 +263,7 @@ export class Recolored {
256
263
  const was = joined.style;
257
264
  const drawn = colors.drawnColors(depth);
258
265
  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);
266
+ const rounded = this.respec(wire, slots, { fg: colors.color?.getTruecolor(terminal, true) ?? slots.fg, bg: colors.bgcolor?.getTruecolor(terminal, false) ?? slots.bg }, measure);
260
267
  const written = (now, before, wrote, drew, round) => {
261
268
  if (sameSpec(now, before))
262
269
  return wrote;
@@ -280,42 +287,39 @@ export class Recolored {
280
287
  * `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
281
288
  * in, the rest as `wire` wrote them.
282
289
  */
283
- respec(wire, from, to, depth) {
284
- const moved = movedSpecs(from, to, depth, this.theme);
290
+ respec(wire, from, to, { depth, terminal }) {
291
+ const moved = movedSpecs(from, to, depth, terminal);
285
292
  return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
286
293
  }
287
294
  }
288
295
  /**
289
296
  * `renderable` under `effect` over time: an `Animation<Renderable>` whose
290
297
  * 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
+ * renderable goes. An `Effect` is made from what the element draws, so with
299
+ * nothing drawn its answer to when it changes is `t`, at every time: early,
300
+ * never false. Placed, on its own or as a part of any combinator, it says
301
+ * exactly: the frame's drawing told the effect's hold at that time and the
302
+ * output's depth (`Moves`), so at a depth that shows no motion a loop says it
303
+ * never changes there and the host sleeps.
298
304
  */
299
305
  export class Effected {
300
306
  renderable;
301
307
  effect;
302
308
  key;
303
- theme;
304
309
  constructor(renderable, effect, options) {
305
310
  this.renderable = renderable;
306
311
  this.effect = effect;
307
312
  this.key = options.key;
308
- this.theme = options.theme;
309
313
  }
310
314
  /** The element through the effect at `t`, unstamped. */
311
315
  at(t) {
312
- return new Recolored(this.renderable, this.effect, { t, key: this.key, theme: this.theme });
316
+ return new Recolored(this.renderable, this.effect, { t, key: this.key });
313
317
  }
314
- nextChange(t) {
315
- return t;
318
+ nextChange(t, moves) {
319
+ return moves?.(t, this.key)?.nextChange ?? t;
316
320
  }
317
- lastChange(t) {
318
- return t;
321
+ lastChange(t, moves) {
322
+ return moves?.(t, this.key)?.lastChange ?? t;
319
323
  }
320
324
  /** The element under the effect with its motion taken out. */
321
325
  reduced() {
@@ -324,23 +328,22 @@ export class Effected {
324
328
  }
325
329
  /** Drawn at the frame's time, its cells stamped with the effect's exact hold there. */
326
330
  render(options) {
327
- const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
328
- return stampFrame(segments, hold);
331
+ return placedRender(this, options);
329
332
  }
330
333
  measure(options) {
331
334
  return Measurement.get(options, this.renderable);
332
335
  }
333
336
  /**
334
337
  * The colours the screen shows for `pair` once it is drawn moved to `to`,
335
- * both as the screen shows them, at `depth` on `theme`: what an effect
338
+ * both as the screen shows them, at `depth` on `terminal`: what an effect
336
339
  * moving a pair's colours leaves its words reading in.
337
340
  * [LAW:one-source-of-truth] Read through `movedSpecs`, the one writer a
338
341
  * moved slot is written with.
339
342
  */
340
- static shows(pair, to, depth, theme) {
343
+ static shows(pair, to, depth, terminal) {
341
344
  const from = onScreen(pair, pair.reverse);
342
- const moved = movedSpecs(from, onScreen(to, pair.reverse), depth, theme);
343
- return onScreen({ fg: moved.fg?.getTruecolor(theme, true) ?? from.fg, bg: moved.bg?.getTruecolor(theme, false) ?? from.bg }, pair.reverse);
345
+ const moved = movedSpecs(from, onScreen(to, pair.reverse), depth, terminal);
346
+ return onScreen({ fg: moved.fg?.getTruecolor(terminal, true) ?? from.fg, bg: moved.bg?.getTruecolor(terminal, false) ?? from.bg }, pair.reverse);
344
347
  }
345
348
  }
346
349
  /**
@@ -352,17 +355,17 @@ export function cellSeed(key, row, col) {
352
355
  return Number.parseInt(fnv1a(`${key}:${row}:${col}`), 16) / 0x1_0000_0000;
353
356
  }
354
357
  const DEFAULT = ColorSpec.default();
355
- /** Each of `segments` with how it is drawn at `depth` on `theme`; a control segment draws nothing. */
356
- function looks(segments, depth, theme) {
358
+ /** Each of `segments` with how it is drawn at `depth` on `terminal`; a control segment draws nothing. */
359
+ function looks(segments, depth, terminal) {
357
360
  return [...segments].map((segment) => {
358
361
  if (segment.isControl)
359
362
  return { segment, look: undefined };
360
363
  const style = segment.style ?? Style.null();
361
364
  const wire = style.drawnColors(depth);
362
- const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
365
+ const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(terminal, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(terminal, false) };
363
366
  const reverse = style.reverse === true;
364
- const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
365
- return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse }, attributes: cellAttributes(style), seam: drawnBy(style.anchor, JOIN) } };
367
+ const own = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
368
+ return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal: own, reverse }, attributes: cellAttributes(style), seam: drawnBy(style.anchor, JOIN) } };
366
369
  });
367
370
  }
368
371
  /**
@@ -381,10 +384,11 @@ export function walkGlyphs(segments) {
381
384
  if (segment.isControl)
382
385
  return [];
383
386
  const anchor = segment.style?.anchor;
387
+ const boxFrame = segment.style?.boxFrame;
384
388
  // Cells into the segment, as `base.shiftedBy` counts them in the render.
385
389
  let into = 0;
386
390
  return [...graphemes(segment.text)].map((glyph) => {
387
- const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
391
+ const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into), boxFrame };
388
392
  const width = cellLen(glyph);
389
393
  [row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + width];
390
394
  into += width;
@@ -451,12 +455,12 @@ function drawsLines(style) {
451
455
  /**
452
456
  * What `looked` draws: `Drawn`'s measure, taken once per frame. Its `all`
453
457
  * and `cells` are listed off `walked` the first time either is asked for,
454
- * since only an effect spread over the element or turning its runs asks, and
458
+ * since an effect on attributes alone never asks, and
455
459
  * a glyph walk is what measuring the words alone, a segment at a time, is
456
460
  * spared (`drawnOf`).
457
461
  */
458
- function measured(looked, walked, depth, theme) {
459
- const ground = theme.backgroundColor.hex;
462
+ function measured(looked, walked, depth, terminal) {
463
+ const ground = terminal.backgroundColor.hex;
460
464
  // [LAW:one-source-of-truth] One listing, each cell with whether it shows; `all` and `cells` are read off it.
461
465
  let everything;
462
466
  const list = () => walked().flatMap(({ look, glyphs }) => {
@@ -480,7 +484,7 @@ function measured(looked, walked, depth, theme) {
480
484
  colors: new Set([...own("fg"), ...own("bg")]),
481
485
  fills: new Set(own("bg")),
482
486
  depth,
483
- theme,
487
+ terminal,
484
488
  };
485
489
  }
486
490
  /**
@@ -503,18 +507,24 @@ export function confined(effect, ...owners) {
503
507
  return false;
504
508
  };
505
509
  // The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
506
- const narrowed = (drawn) => ({ ...drawn, all: () => drawn.all().filter(owned), cells: () => drawn.cells().filter(owned) });
510
+ const narrowed = (drawn) => {
511
+ let all;
512
+ let cells;
513
+ return { ...drawn, all: () => (all ??= drawn.all().filter(owned)), cells: () => (cells ??= drawn.cells().filter(owned)) };
514
+ };
507
515
  // The owners' cells change when the effect's frame does.
508
516
  return (drawn) => Animation.map(effect(narrowed(drawn)), (frame) => (look, cell) => (owned(cell) ? frame(look, cell) : look));
509
517
  }
510
518
  /**
511
- * What `segments`, drawn at `depth` on `theme`, draw their words in, measured
512
- * as `Effected` measures its child for every frame. Measure an element's
513
- * output under an effect to read the contrast its words are left at.
519
+ * What `segments`, drawn at `depth` on `terminal`, draw their words in, measured
520
+ * as `Effected` measures its child for every frame, so `options.terminal` means
521
+ * here what it means on a frame. Measure an element's output under an effect to
522
+ * read the contrast its words are left at.
514
523
  */
515
- export function drawnOf(segments, depth, theme) {
516
- const looked = looks(segments, depth, theme);
517
- return measured(looked, () => walk(looked), depth, theme);
524
+ export function drawnOf(segments, depth, terminal) {
525
+ const on = resolveTerminal(terminal, depth);
526
+ const looked = looks(segments, depth, on);
527
+ return measured(looked, () => walk(looked), depth, on);
518
528
  }
519
529
  /**
520
530
  * A style's colour slots as the screen shows them, or the reverse: `reverse`
@@ -531,11 +541,11 @@ function onScreen(slots, reverse) {
531
541
  * (`ColorSpec.matchMove`). A slot the depth shows no move in has none, and
532
542
  * keeps the spec it was written in.
533
543
  */
534
- function movedSpecs(from, to, depth, theme) {
544
+ function movedSpecs(from, to, depth, terminal) {
535
545
  // [LAW:one-source-of-truth] The writer's surface, so one translucent colour draws one way in a frame.
536
546
  const ground = to.bg.compositeOver(SURFACE_BLACK);
537
547
  const glyph = to.fg.compositeOver(ground);
538
- return { fg: ColorSpec.matchMove(from.fg, glyph, depth, theme, true), bg: ColorSpec.matchMove(from.bg, ground, depth, theme, false) };
548
+ return { fg: ColorSpec.matchMove(from.fg, glyph, depth, terminal, true), bg: ColorSpec.matchMove(from.bg, ground, depth, terminal, false) };
539
549
  }
540
550
  /**
541
551
  * Whether two cells' colours are written alike. A slot with no colour is not
@@ -73,7 +73,7 @@ export interface Loop {
73
73
  * How much of `touch` each of `colors` (hex) can take: the largest share in
74
74
  * [0, 1] at which every pair `drawn` shows it in, touched at the same
75
75
  * strength, still reads at its resting contrast or `LEGIBLE`, whichever is
76
- * lower, in the colours `Effected` writes it in at `drawn`'s depth and theme.
76
+ * lower, in the colours `Effected` writes it in at `drawn`'s depth and terminal.
77
77
  * A colour in a pair but not in `colors` takes none, and so does one
78
78
  * the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
79
79
  * no pair writes has nothing to be measured against, and takes it all.
@@ -85,7 +85,7 @@ export interface Loop {
85
85
  * the fill room to glow — and a darker colour left as it is always holds,
86
86
  * because its lighter partner was settled against exactly that.
87
87
  */
88
- export declare function shares({ pairs, depth, theme }: Drawn, colors: ReadonlySet<string>, touch: Touch): Map<string, number>;
88
+ export declare function shares({ pairs, depth, terminal }: Drawn, colors: ReadonlySet<string>, touch: Touch): Map<string, number>;
89
89
  /**
90
90
  * A loop as an `Effect` on the element's own colours, by hex, wherever a cell
91
91
  * shows them, each touched at its share of the cell's strength: the share
@@ -93,10 +93,10 @@ export declare function shares({ pairs, depth, theme }: Drawn, colors: ReadonlyS
93
93
  * (`Drawn`), touched as `Effected` writes them at the frame's depth — at 256
94
94
  * colours a touched colour lands on a colour the slot can write, and the
95
95
  * contrast it spends is that colour's; on a sixteen-colour terminal nothing
96
- * moves (`stillOnSlots`). `colors` are the ones lit, by default every colour the element
96
+ * moves (`stillWhereNothingMoves`). `colors` are the ones lit, by default every colour the element
97
97
  * sets itself. A powerline strip's fill is the ground of its cells and the
98
98
  * ink of the seam glyphs between them, so the arrow moves with the cell it
99
- * points out of. A colour the terminal supplies (`EffectCell.terminal`) is
99
+ * points out of. A colour the terminal supplies (`Pair.terminal`) is
100
100
  * not the element's, whatever its hex, and takes no share.
101
101
  *
102
102
  * Settling shares is costly, so it is done once for each set of colours an
@@ -122,7 +122,7 @@ export declare function onColors(loop: Loop, colors?: ReadonlySet<string>): Effe
122
122
  * body. The element is where its frame lists
123
123
  * cells, as the fireflies read it, so a padded or confined element has the
124
124
  * ramp across what it shows. On a sixteen-colour terminal nothing moves
125
- * (`stillOnSlots`): a gradient there is jumps between slots, and the cells
125
+ * (`stillWhereNothingMoves`): a gradient there is jumps between slots, and the cells
126
126
  * hold as drawn.
127
127
  */
128
128
  export declare function gradient(ramp: Animation<ColorRamp>, colors?: readonly string[]): Effect;
@@ -264,7 +264,7 @@ export declare function sparkle(curve: Curve, glow: ColorRgba, z: number): Loop;
264
264
  export type Visibility = (t: number, drawn: Drawn) => (cell: EffectCell) => number;
265
265
  /**
266
266
  * A cell mixed toward the ground of the terminal the frame is drawn on
267
- * (`Drawn.theme`) by how hidden it is: its fill toward the ground, and its ink
267
+ * (`Drawn.terminal`) by how hidden it is: its fill toward the ground, and its ink
268
268
  * toward that fill, so an element with fills of its own — a powerline strip —
269
269
  * arrives and leaves whole, fill and words together, never as bare coloured
270
270
  * blocks. Invisible is ink and fill equal to the ground. `swing` scales the
@@ -360,9 +360,38 @@ export declare function dissolveOut(curve: Curve, z: number): Transition;
360
360
  * is drawn in at its floor (`floorOf`) — its resting contrast or WCAG AA,
361
361
  * whichever is lower — as `shares` keeps the light's (`turnedBounds`). The
362
362
  * turn only happens to the element's own colours (`Drawn.colors`), never the
363
- * terminal's. On a sixteen-colour terminal nothing turns (`stillOnSlots`).
363
+ * terminal's. On a sixteen-colour terminal nothing turns (`stillWhereNothingMoves`).
364
364
  */
365
365
  export declare function wheel(curve: Curve, z: number): Effect;
366
+ /**
367
+ * Several effects on one element, as one effect: each layer is handed the look
368
+ * the one before it left and the same cell, so later layers are on top, and each
369
+ * recognises a cell by what the element drew (`EffectCell.pair`), never by the
370
+ * colour it is handed. A hue a layer turned onto another of the element's
371
+ * colours is not mistaken for it, and turns once. It changes when the soonest
372
+ * layer does, and with no layers it leaves every cell as drawn.
373
+ *
374
+ * Every word reads at its floor (`floorOf`), whatever the layers did and in
375
+ * whatever order. Each loop proves that for colours as the element drew them,
376
+ * and a colour another layer moved voids the proof: light lands at another
377
+ * luminance on a turned hue. So once the layers have moved a cell, its ground
378
+ * lands where its inks can still read (`groundBounds`), a function of the colour
379
+ * alone so one fill stays one fill, and an ink that does not read on it slides
380
+ * to the luminance that does, on the side of the ground it stands on while that
381
+ * side has room. A layer that takes an ink across its ground crosses it here too:
382
+ * nothing reads at the crossing, so the word jumps it. [LAW:single-enforcer]
383
+ * Both are measured as `Effected` writes them, and both hand back what they were
384
+ * given where it already holds, so a layer that proved its own floor is drawn as
385
+ * it would be alone. A frame that takes a ground past its bounds is the one
386
+ * exception, whatever its ink reads: a swap of ink and ground lands at the floor,
387
+ * since a bound cannot ask what one cell's ink does without splitting a fill.
388
+ * The floor outranks a colour being the terminal's: a terminal
389
+ * ink is moved here where a ground under it left it unreadable. The rule is the
390
+ * same for one layer as for five: a veil (`veiled`, or a transition's `effect`)
391
+ * stops at the floor here, so a transition goes in an `Effected` around this
392
+ * one, as a lifecycle places it.
393
+ */
394
+ export declare function layered(...effects: readonly Effect[]): Effect;
366
395
  /** One step of a signal: the ink it draws, and how long it is held, in seconds. */
367
396
  export interface ColorStep {
368
397
  readonly color: ColorRgba;
@@ -381,7 +410,7 @@ export interface ColorStep {
381
410
  * (`ensureContrast`, the one slide a template's `readableOn` makes too);
382
411
  * below truecolor, the nearest of the colours the depth has an index for, so on a
383
412
  * sixteen-colour terminal it steps from slot to slot, the one effect here
384
- * that does (`stillOnSlots`). A cell whose ground no offered colour reads
413
+ * that does (`stillWhereNothingMoves`). A cell whose ground no offered colour reads
385
414
  * on keeps the ink it rested in.
386
415
  *
387
416
  * The search is settled once for each ground and step — below truecolor,