@promptctl/rich-js 0.27.0 → 0.29.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 (185) hide show
  1. package/README.md +7 -6
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +65 -46
  4. package/dist/core/animation.js +91 -67
  5. package/dist/core/ansi.d.ts +19 -12
  6. package/dist/core/ansi.js +114 -36
  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 +94 -6
  17. package/dist/core/clock.d.ts +1 -1
  18. package/dist/core/color.d.ts +56 -57
  19. package/dist/core/color.js +79 -81
  20. package/dist/core/console.d.ts +24 -7
  21. package/dist/core/console.js +27 -8
  22. package/dist/core/control-codes.d.ts +61 -0
  23. package/dist/core/control-codes.js +157 -0
  24. package/dist/core/easing.d.ts +40 -40
  25. package/dist/core/easing.js +40 -40
  26. package/dist/core/env.d.ts +6 -0
  27. package/dist/core/env.js +9 -1
  28. package/dist/core/export-html.js +5 -4
  29. package/dist/core/export-lines.d.ts +13 -11
  30. package/dist/core/export-lines.js +19 -15
  31. package/dist/core/export-svg.d.ts +3 -3
  32. package/dist/core/export-svg.js +10 -9
  33. package/dist/core/json.js +9 -2
  34. package/dist/core/markup.js +5 -4
  35. package/dist/core/memo.d.ts +1 -1
  36. package/dist/core/memo.js +1 -1
  37. package/dist/core/oklch.d.ts +14 -14
  38. package/dist/core/oklch.js +10 -10
  39. package/dist/core/orient.d.ts +68 -0
  40. package/dist/core/orient.js +224 -0
  41. package/dist/core/osc8.d.ts +12 -5
  42. package/dist/core/osc8.js +14 -7
  43. package/dist/core/paint.d.ts +9 -9
  44. package/dist/core/paint.js +3 -3
  45. package/dist/core/pretty.d.ts +1 -1
  46. package/dist/core/pretty.js +23 -19
  47. package/dist/core/protocol.d.ts +47 -26
  48. package/dist/core/protocol.js +31 -23
  49. package/dist/core/render.d.ts +19 -14
  50. package/dist/core/render.js +30 -17
  51. package/dist/core/seal.d.ts +26 -0
  52. package/dist/core/seal.js +75 -0
  53. package/dist/core/segment.d.ts +12 -4
  54. package/dist/core/segment.js +16 -6
  55. package/dist/core/strip.d.ts +83 -83
  56. package/dist/core/strip.js +70 -70
  57. package/dist/core/style.d.ts +46 -18
  58. package/dist/core/style.js +92 -56
  59. package/dist/core/text-order.d.ts +51 -0
  60. package/dist/core/text-order.js +50 -0
  61. package/dist/core/text.d.ts +12 -2
  62. package/dist/core/text.js +136 -90
  63. package/dist/core/timeline.d.ts +5 -5
  64. package/dist/core/timeline.js +11 -11
  65. package/dist/core/tween.d.ts +6 -6
  66. package/dist/core/tween.js +10 -10
  67. package/dist/core/view.d.ts +5 -5
  68. package/dist/core/view.js +15 -14
  69. package/dist/core/wrap.d.ts +24 -6
  70. package/dist/core/wrap.js +82 -16
  71. package/dist/host/app.d.ts +50 -9
  72. package/dist/host/app.js +120 -15
  73. package/dist/host/host-environment.d.ts +2 -2
  74. package/dist/host/host-environment.js +2 -2
  75. package/dist/host/index.d.ts +3 -2
  76. package/dist/host/index.js +1 -0
  77. package/dist/host/input.d.ts +42 -9
  78. package/dist/host/input.js +91 -7
  79. package/dist/host/terminal-colors.d.ts +18 -0
  80. package/dist/host/terminal-colors.js +46 -0
  81. package/dist/host/terminal-host.d.ts +4 -3
  82. package/dist/host/terminal-host.js +1 -1
  83. package/dist/index.d.ts +17 -16
  84. package/dist/index.js +11 -11
  85. package/dist/node/prompt.d.ts +1 -1
  86. package/dist/node/prompt.js +1 -1
  87. package/dist/node/terminal-host.d.ts +1 -1
  88. package/dist/node/traceback.js +2 -2
  89. package/dist/renderables/change.d.ts +1 -3
  90. package/dist/renderables/change.js +6 -8
  91. package/dist/renderables/effect.d.ts +88 -87
  92. package/dist/renderables/effect.js +124 -114
  93. package/dist/renderables/effects.d.ts +105 -76
  94. package/dist/renderables/effects.js +309 -176
  95. package/dist/renderables/embed.d.ts +1 -1
  96. package/dist/renderables/embed.js +2 -2
  97. package/dist/renderables/flexStrip.d.ts +7 -7
  98. package/dist/renderables/flexStrip.js +15 -15
  99. package/dist/renderables/frames.d.ts +7 -16
  100. package/dist/renderables/frames.js +12 -27
  101. package/dist/renderables/glyph-effect.d.ts +22 -23
  102. package/dist/renderables/glyph-effect.js +43 -41
  103. package/dist/renderables/glyph-effects.d.ts +33 -14
  104. package/dist/renderables/glyph-effects.js +90 -22
  105. package/dist/renderables/html-entities.d.ts +1 -1
  106. package/dist/renderables/html-entities.js +1 -1
  107. package/dist/renderables/lifecycle.d.ts +8 -10
  108. package/dist/renderables/lifecycle.js +17 -18
  109. package/dist/renderables/live.d.ts +2 -2
  110. package/dist/renderables/live.js +7 -7
  111. package/dist/renderables/markdown-parse.js +3 -3
  112. package/dist/renderables/markdown.js +4 -2
  113. package/dist/renderables/panel.js +7 -5
  114. package/dist/renderables/progress.d.ts +1 -1
  115. package/dist/renderables/progress.js +1 -1
  116. package/dist/renderables/progressBar.d.ts +3 -3
  117. package/dist/renderables/progressBar.js +21 -18
  118. package/dist/renderables/prompt.d.ts +1 -1
  119. package/dist/renderables/scroll.d.ts +1 -1
  120. package/dist/renderables/scroll.js +3 -3
  121. package/dist/renderables/spinner.d.ts +4 -4
  122. package/dist/renderables/spinner.js +10 -10
  123. package/dist/renderables/syntax.d.ts +2 -2
  124. package/dist/renderables/syntax.js +9 -4
  125. package/dist/renderables/table.js +5 -4
  126. package/dist/renderables/traceback.d.ts +1 -1
  127. package/dist/renderables/traceback.js +1 -1
  128. package/dist/renderables/tree.js +3 -3
  129. package/dist/template-bindings/animation-funcs.d.ts +60 -29
  130. package/dist/template-bindings/animation-funcs.js +126 -43
  131. package/dist/template-bindings/color-funcs.d.ts +14 -14
  132. package/dist/template-bindings/color-funcs.js +25 -25
  133. package/dist/template-bindings/index.d.ts +16 -14
  134. package/dist/template-bindings/index.js +17 -14
  135. package/dist/template-bindings/palette-funcs.d.ts +2 -2
  136. package/dist/template-bindings/palette-funcs.js +4 -4
  137. package/dist/template-bindings/style-funcs.d.ts +5 -5
  138. package/dist/template-bindings/style-funcs.js +16 -16
  139. package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
  140. package/dist/themes/{transpose.js → adjust.js} +28 -28
  141. package/dist/themes/buildPalette.d.ts +6 -5
  142. package/dist/themes/buildPalette.js +22 -22
  143. package/dist/themes/colorMath.d.ts +16 -16
  144. package/dist/themes/colorMath.js +32 -32
  145. package/dist/themes/data/ansi-tables.d.ts +2 -2
  146. package/dist/themes/data/ansi-tables.js +2 -2
  147. package/dist/themes/data/index.js +1 -4
  148. package/dist/themes/data/textual-ansi.js +0 -143
  149. package/dist/themes/data/types.d.ts +4 -4
  150. package/dist/themes/data/types.js +2 -2
  151. package/dist/themes/palette.d.ts +12 -12
  152. package/dist/themes/palette.js +16 -16
  153. package/dist/themes/ramp.d.ts +17 -17
  154. package/dist/themes/ramp.js +12 -12
  155. package/dist/themes/registry.d.ts +6 -8
  156. package/dist/themes/registry.js +6 -8
  157. package/dist/themes/terminalThemes.d.ts +4 -11
  158. package/dist/themes/terminalThemes.js +31 -47
  159. package/dist/widgets/button.d.ts +3 -3
  160. package/dist/widgets/button.js +8 -8
  161. package/dist/widgets/checkbox.d.ts +2 -2
  162. package/dist/widgets/checkbox.js +4 -4
  163. package/dist/widgets/dropdown.d.ts +3 -3
  164. package/dist/widgets/dropdown.js +7 -5
  165. package/dist/widgets/event-router.d.ts +6 -6
  166. package/dist/widgets/event-router.js +2 -2
  167. package/dist/widgets/focus-manager.d.ts +3 -3
  168. package/dist/widgets/focus-manager.js +3 -3
  169. package/dist/widgets/index.d.ts +2 -2
  170. package/dist/widgets/index.js +1 -1
  171. package/dist/widgets/ink.d.ts +3 -3
  172. package/dist/widgets/ink.js +4 -4
  173. package/dist/widgets/slider.d.ts +2 -2
  174. package/dist/widgets/static-item.d.ts +2 -2
  175. package/dist/widgets/text-input.d.ts +2 -2
  176. package/dist/widgets/text-input.js +5 -5
  177. package/dist/widgets/themed-widget.js +4 -4
  178. package/dist/widgets/toggle.d.ts +4 -4
  179. package/dist/widgets/toggle.js +6 -6
  180. package/dist/widgets/types.d.ts +6 -6
  181. package/dist/widgets/types.js +2 -2
  182. package/dist/widgets/widget-app.d.ts +10 -10
  183. package/dist/widgets/widget-app.js +8 -8
  184. package/dist/widgets/widget-base.d.ts +2 -2
  185. package/package.json +5 -4
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Effected — a renderable whose finished cells' looks — colours and text
2
+ * Effected — a renderable whose finished cells' looks — colors and text
3
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`
@@ -11,26 +11,27 @@
11
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
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'
14
+ * them, which are no one's to paint: a seam's colors are its neighbours'
15
15
  * edges, so once the effect has painted every other cell, each such cell is
16
16
  * resolved again from the cells beside it as the effect left them
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
- * 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`,
25
+ * A cell's look is one value, its colors and its attributes together
26
+ * (`CellLook`), rather than an attribute stage beside the color one, so
27
+ * everything that takes an `Effect` — `confined`, `sampleAndHold`,
27
28
  * `Animation.all`, nesting over a glyph effect — takes one that moves
28
29
  * attributes with no second copy of itself. `reverse` is the one attribute
29
30
  * 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.
31
+ * a color on screen is in, and a swap of the two is already a color move.
31
32
  *
32
33
  * What the element draws (`Drawn`) — the cells the effect will be handed,
33
- * and the colours of its words — is measured here, in the render, off the
34
+ * and the colors of its words — is measured here, in the render, off the
34
35
  * same reading of each segment its cells are drawn from, and handed to the
35
36
  * effect once a frame. It is what an effect keeping words legible is settled
36
37
  * against (`onColors`, `./effects.ts`), so no caller derives it a second time
@@ -50,18 +51,18 @@
50
51
  * to the effect and is never cut, and a run cut out of a segment keeps that
51
52
  * segment's link and meta and carries its anchor shifted to the run's first
52
53
  * cell (`Style.shiftedBy`), so hit testing still finds the owner.
53
- * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
54
+ * - A cell whose colors come back unchanged keeps its original `ColorSpec`,
54
55
  * one whose attributes come back as its style set them keeps them, and a
55
56
  * segment whose every cell is unchanged is yielded with its look as
56
57
  * it was, so the identity effect is byte-identical; what an `Effected` gains is when
57
58
  * the frame next changes (`Style.hold`), which writes no byte.
58
59
  * Neighbouring cells the output depth
59
60
  * writes alike stay one segment: a segment is cut only where the written
60
- * colours or the attributes change, so two colours that land on one index
61
+ * colors or the attributes change, so two colors that land on one index
61
62
  * are one run.
62
- * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
63
+ * - New colors are built with `ColorSpec.fromRgba`, never parsed from a
63
64
  * string, so no frame adds an entry to a parse cache.
64
- * - With no colour output (`colorSystem: null`) no style is written at all,
65
+ * - With no color output (`colorSystem: null`) no style is written at all,
65
66
  * attributes included, so there is nothing for an effect to change, and
66
67
  * the child's segments pass through untouched.
67
68
  *
@@ -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, validityOf, 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 validityOver(anyCell, shown, t) {
89
+ return anyCell ? validityOf(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.onReads`), 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
- // With no colour output nothing the effect does is written, so the child
112
+ // With no color 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.onReads?.(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 color 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.onReads?.(this._t, this.key, validityOver(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,16 +149,16 @@ 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
- // cell is drawn at the output depth first and merged on the colours that
161
+ // cell is drawn at the output depth first and merged on the colors that
155
162
  // writes and on its attributes, so two truecolors landing on one index
156
163
  // stay one run. A cell the effect gave its neighbour's look is drawn as
157
164
  // that neighbour was.
@@ -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,30 +198,30 @@ 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
204
211
  // 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.
212
+ // color names none, and the terminal the effect draws on says what it is.
206
213
  // 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.
214
+ // RGB a blend or another color lands on is not the terminal's own.
208
215
  const terminals = new Set();
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];
@@ -246,9 +253,9 @@ export class Recolored {
246
253
  // [LAW:one-source-of-truth] Each slot is written as the depth writes what
247
254
  // it resolved to, by the writer the lift measured it with
248
255
  // (`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
256
+ // as it did when measured. A slot resolved to the color it was before
250
257
  // 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
258
+ // The terminal's own colors, which stand in a moved edge as the spec of
252
259
  // the RGB the effect's terminal gives them and reach a slot unchanged or
253
260
  // not at all, are written by the writer that wrote them on the item
254
261
  // beside, on that terminal, which writes its own ground below truecolor as
@@ -256,12 +263,12 @@ 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;
263
270
  // [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.
271
+ // a slot that resolved to a color when drawn resolves to one after.
265
272
  if (now === undefined)
266
273
  throw new Error(`a seam cell resolved to ${before?.name ?? "nothing"} when drawn and to nothing after an effect moved its neighbours`);
267
274
  return terminals.has(now) ? round : drew;
@@ -276,46 +283,43 @@ export class Recolored {
276
283
  return Measurement.get(options, this.renderable);
277
284
  }
278
285
  /**
279
- * The specs that draw a cell's slot colours `to` where the segment drew
286
+ * The specs that draw a cell's slot colors `to` where the segment drew
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 (`Reads`), 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
+ validUntil(t, moves) {
319
+ return moves?.(t, this.key)?.validUntil ?? t;
316
320
  }
317
- lastChange(t) {
318
- return t;
321
+ validFrom(t, moves) {
322
+ return moves?.(t, this.key)?.validFrom ?? t;
319
323
  }
320
324
  /** The element under the effect with its motion taken out. */
321
325
  reduced() {
@@ -324,45 +328,44 @@ 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
- * 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
336
- * moving a pair's colours leaves its words reading in.
337
+ * The colors the screen shows for `pair` once it is drawn moved to `to`,
338
+ * both as the screen shows them, at `depth` on `terminal`: what an effect
339
+ * moving a pair's colors 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
  /**
347
350
  * 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
351
+ * into [0, 1). [LAW:one-source-of-truth] One rule for a color effect's seeds
349
352
  * and a glyph effect's, so one element under both twinkles from one seed.
350
353
  */
351
354
  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
  /**
@@ -371,7 +374,7 @@ function looks(segments, depth, theme) {
371
374
  * column, and its anchor shifted to that column as `Style.shiftedBy` shifts
372
375
  * a run's. A control segment draws nothing and has none.
373
376
  * [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`)
377
+ * render hands a color effect, and what a glyph effect (`./glyph-effect.ts`)
375
378
  * is handed are all read off it.
376
379
  */
377
380
  export function walkGlyphs(segments) {
@@ -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;
@@ -394,7 +398,7 @@ export function walkGlyphs(segments) {
394
398
  }
395
399
  /**
396
400
  * Whether `text` draws ink: anything but blanks. [LAW:one-source-of-truth]
397
- * The one test, so the words a colour effect lights are the words a glyph
401
+ * The one test, so the words a color effect lights are the words a glyph
398
402
  * effect (`./glyph-effects.ts`) scrambles and its particles pass behind.
399
403
  */
400
404
  export function inked(text) {
@@ -446,17 +450,17 @@ function edgesBeside(walked, drawings) {
446
450
  }
447
451
  /** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
448
452
  function drawsLines(style) {
449
- return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
453
+ return [style.underline, style.doubleUnderline, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
450
454
  }
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 }) => {
@@ -471,7 +475,7 @@ function measured(looked, walked, depth, theme) {
471
475
  let shown;
472
476
  const words = looked.flatMap(({ segment, look }) => (look !== undefined && inked(segment.text) && !look.seam ? [look.pair] : []));
473
477
  const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
474
- // [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
478
+ // [LAW:one-source-of-truth] A slot the terminal fills is no color of the element's, whatever its hex.
475
479
  const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
476
480
  return {
477
481
  all: () => (all ??= listed().map(({ cell }) => cell)),
@@ -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,43 +507,49 @@ 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
- * A style's colour slots as the screen shows them, or the reverse: `reverse`
530
+ * A style's color slots as the screen shows them, or the reverse: `reverse`
521
531
  * swaps glyph and ground, and swapping twice is where you started.
522
532
  */
523
533
  function onScreen(slots, reverse) {
524
534
  return reverse ? { fg: slots.bg, bg: slots.fg } : slots;
525
535
  }
526
536
  /**
527
- * The specs a cell's slots are written in once their colours move from
537
+ * The specs a cell's slots are written in once their colors move from
528
538
  * `from`, what the written specs draw, to `to`: each laid on what is beneath
529
539
  * it — the ground on `SURFACE_BLACK`, the glyph on that ground, as the writer
530
- * lays any translucent colour (`Style.drawnColors`) — then matched at `depth`
540
+ * lays any translucent color (`Style.drawnColors`) — then matched at `depth`
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) {
535
- // [LAW:one-source-of-truth] The writer's surface, so one translucent colour draws one way in a frame.
544
+ function movedSpecs(from, to, depth, terminal) {
545
+ // [LAW:one-source-of-truth] The writer's surface, so one translucent color 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
- * Whether two cells' colours are written alike. A slot with no colour is not
542
- * the default colour: it writes nothing, so a style a container lays beneath
551
+ * Whether two cells' colors are written alike. A slot with no color is not
552
+ * the default color: it writes nothing, so a style a container lays beneath
543
553
  * (`Segment.applyStyle`) still shows through it, where `39`/`49` would not.
544
554
  */
545
555
  function writesAlike(a, b) {
@@ -577,7 +587,7 @@ export function cellAttributes(of) {
577
587
  function sameAttributes(a, b) {
578
588
  return a === b || MOVED.every((name) => a[name] === b[name]);
579
589
  }
580
- /** Whether two specs are one colour as written: the same kind, naming the same colour. */
590
+ /** Whether two specs are one color as written: the same kind, naming the same color. */
581
591
  function sameSpec(a, b) {
582
592
  return a === b || (a !== undefined && b !== undefined && a.type === b.type && a.name === b.name);
583
593
  }