@promptctl/rich-js 0.23.0 → 0.24.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 (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -5
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { Measurement } from "../core/measure.js";
12
12
  import { normalizePadding } from "./padding.js";
13
- import { withBoundedWidth, withCellWidth } from "../core/protocol.js";
13
+ import { render, withBoundedWidth, withCellWidth } from "../core/protocol.js";
14
14
  import { cellCount } from "../core/cells.js";
15
15
  import { EmbeddedText, embed } from "./embed.js";
16
16
  import { Constrain } from "./constrain.js";
@@ -68,7 +68,12 @@ export class Columns {
68
68
  // single column is laid out. An unbounded width reaches the same place by
69
69
  // the same route, which is why the parse here is the bounded one.
70
70
  const options = withBoundedWidth(rawOptions, this);
71
- yield* this._grid(options).render(options);
71
+ yield* render(this._grid(options), options);
72
+ }
73
+ // The items, in the order they were given. Laid out column-first, the grid
74
+ // draws them in an order its width decides, and handed no column it has none.
75
+ contents() {
76
+ return this.renderables;
72
77
  }
73
78
  measure(rawOptions) {
74
79
  // The maximum is the grid's, which reports its width before `expand`: a
@@ -2,12 +2,13 @@
2
2
  * Constrain — wraps a renderable and constrains its maximum width.
3
3
  */
4
4
  import { Segment } from "../core/segment.js";
5
- import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
6
- export declare class Constrain implements Renderable, Measurable {
5
+ import type { Composite, Renderable, Measurable, RenderOptions } from "../core/protocol.js";
6
+ export declare class Constrain implements Renderable, Measurable, Composite {
7
7
  readonly renderable: Renderable;
8
8
  readonly width: number | undefined;
9
9
  constructor(renderable: Renderable, width?: number);
10
10
  render(options: RenderOptions): Iterable<Segment>;
11
+ contents(): Iterable<Renderable>;
11
12
  measure(options: RenderOptions): {
12
13
  minimum: number;
13
14
  maximum: number;
@@ -2,6 +2,7 @@
2
2
  * Constrain — wraps a renderable and constrains its maximum width.
3
3
  */
4
4
  import { Measurement } from "../core/measure.js";
5
+ import { render } from "../core/protocol.js";
5
6
  export class Constrain {
6
7
  renderable;
7
8
  width;
@@ -18,7 +19,10 @@ export class Constrain {
18
19
  ...options,
19
20
  maxWidth: constrainedWidth,
20
21
  };
21
- yield* this.renderable.render(innerOptions);
22
+ yield* render(this.renderable, innerOptions);
23
+ }
24
+ *contents() {
25
+ yield this.renderable;
22
26
  }
23
27
  measure(options) {
24
28
  // Measured at the width it will be drawn at, as Rich's `update_width` does;
@@ -2,23 +2,26 @@
2
2
  * Effected — a renderable whose finished cells' colours pass through an
3
3
  * effect at a moment `t`.
4
4
  *
5
- * An effect is one pure function in two stages: a time and what the element
6
- * draws in that frame in, a function of the frame out; and into that, a
7
- * cell's colours and where the cell is, the cell's new colours out. Whatever
8
- * holds for the whole frame is worked out in the first stage, once, and every
9
- * cell of the frame is handed to the second, but for those a wrapper keeps
10
- * back: `confined` hands over only the cells its owners drew, so a second
11
- * stage never counts on seeing them all. Shimmer, pulse, fade and
12
- * dissolve are all this one shape, differing by what the function does with
13
- * its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
14
- * wrapper in another, which is the same as calling one after the other but
15
- * for what the outer one measures: the inner one's output.
5
+ * An effect is, for what an element draws in a frame, an `Animation`
6
+ * (`core/animation.ts`) of what is done to its cells: its value at `t` is a
7
+ * function from a cell's colours and where the cell is to the cell's new
8
+ * colours (`EffectFrame`), and it says when that next changes as every
9
+ * animation does. Whatever holds for the whole frame is worked out once, in
10
+ * the effect and its `at(t)`, and every cell of the frame is handed to the
11
+ * function `at(t)` returns, but for those a wrapper keeps back: `confined`
12
+ * hands over only the cells its owners drew, so an effect never counts on
13
+ * seeing them all. Shimmer, pulse, fade and dissolve are all this one shape,
14
+ * 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.
16
18
  *
17
- * What the element draws (`Drawn`) is measured here, in the render, off the
19
+ * What the element draws (`Drawn`) — the cells the effect will be handed,
20
+ * and the colours of its words — is measured here, in the render, off the
18
21
  * same reading of each segment its cells are drawn from, and handed to the
19
- * effect's first stage, once a frame. It is what an effect keeping words legible is
20
- * settled against (`onColors`, `./effects.ts`), so no caller derives it a
21
- * second time from a render of its own.
22
+ * effect once a frame. It is what an effect keeping words legible is settled
23
+ * against (`onColors`, `./effects.ts`), so no caller derives it a second time
24
+ * from a render of its own.
22
25
  *
23
26
  * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
24
27
  * clock read. Whoever draws frames builds the view for each frame's `t`, so
@@ -33,8 +36,10 @@
33
36
  * segment's link and meta and carries its anchor shifted to the run's first
34
37
  * cell (`Style.shiftedBy`), so hit testing still finds the owner.
35
38
  * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
36
- * and a segment whose every cell is unchanged is yielded as it was, so the
37
- * identity effect is byte-identical. Neighbouring cells the output depth
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.
42
+ * Neighbouring cells the output depth
38
43
  * writes alike stay one segment: a segment is cut only where the written
39
44
  * colours change, so two colours that land on one index are one run.
40
45
  * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
@@ -46,8 +51,10 @@
46
51
  * what the child drew, so a sweep crosses the element, not the screen.
47
52
  */
48
53
  import { type Anchor } from "../core/anchor.js";
54
+ import { Animation } from "../core/animation.js";
49
55
  import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
50
- import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
56
+ import type { Beat } from "../core/easing.js";
57
+ import type { Composite, Measurable, Renderable, RenderOptions } from "../core/protocol.js";
51
58
  import { Segment } from "../core/segment.js";
52
59
  /**
53
60
  * A cell's foreground and background as they appear on screen — the glyph's
@@ -78,6 +85,8 @@ export interface EffectCell {
78
85
  readonly fg: boolean;
79
86
  readonly bg: boolean;
80
87
  };
88
+ /** Whether the cell's style swaps its two slots, as `Pair.reverse` says: with `colors` and `terminal`, the cell's `Pair`. */
89
+ readonly reverse: boolean;
81
90
  /**
82
91
  * Who drew the cell, and where it sits in their output, as
83
92
  * `Segment.anchorAt` reads it off a frame: what tells one owner's cells
@@ -85,6 +94,8 @@ export interface EffectCell {
85
94
  */
86
95
  readonly anchor: Anchor | undefined;
87
96
  }
97
+ /** Where a cell sits and who drew it: an `EffectCell` before it has a seed or colours. */
98
+ export type Placed = Pick<EffectCell, "row" | "col" | "anchor">;
88
99
  /**
89
100
  * A cell's ink and ground as the screen shows them, which of the two are the
90
101
  * terminal's, and whether its style swaps them (`reverse`): what an `Effect`
@@ -101,13 +112,24 @@ export interface Pair extends CellColors {
101
112
  readonly reverse: boolean;
102
113
  }
103
114
  /**
104
- * The colours the element an effect wraps draws its words in, in one frame,
105
- * as the screen shows them: what an effect that must keep the words legible
106
- * is measured against. A word cell is one whose glyph is not blank and that a
107
- * strip's joiner did not draw (`JOIN`): a seam's ink is the fill beside it,
108
- * not words.
115
+ * What the element an effect wraps draws in one frame: the cells the effect
116
+ * is handed, and the colours the element draws its words in, as the screen
117
+ * shows them — what an effect that must keep the words legible is measured
118
+ * against. A word cell is one whose glyph is not blank and that a strip's
119
+ * joiner did not draw (`JOIN`): a seam's ink is the fill beside it, not words.
109
120
  */
110
121
  export interface Drawn {
122
+ /**
123
+ * Every cell the element shows something in this frame — a glyph, a
124
+ * ground that is not the terminal's, or a line its style draws — where it
125
+ * sits and who drew it, listed before the effect is handed any: what
126
+ * an effect spread over the element — a transition, which has its last cell
127
+ * settle as it ends — is spread over. A blank cell on the terminal's ground
128
+ * that draws no line is handed but not listed, since nothing in it shows.
129
+ * `confined` narrows it to the cells its owners drew. A call, so a frame
130
+ * whose effect does not ask lists none.
131
+ */
132
+ cells(): readonly Placed[];
111
133
  /** Every distinct ink and ground of a word cell. */
112
134
  readonly pairs: readonly Pair[];
113
135
  /** The colours the element sets itself in its word cells, by hex: never the terminal's, whatever its hex. */
@@ -119,14 +141,23 @@ export interface Drawn {
119
141
  /** The terminal the frame is drawn on. */
120
142
  readonly theme: TerminalTheme;
121
143
  }
144
+ /** What an effect does to the cells of one frame: each cell's colours and where it is in, its colours out. Return `colors` to leave a cell be. */
145
+ export type EffectFrame = (colors: CellColors, cell: EffectCell) => CellColors;
122
146
  /**
123
- * The frame at time `t`, in seconds, whose element draws `drawn`: each cell's
124
- * colours in that frame. Return `colors` to leave a cell be.
125
- * [LAW:types-are-the-program] Per-frame work is the outer stage, so it runs
126
- * once a frame by the shape of the call, never by a cache asking whether this
127
- * cell's frame is the last one's.
147
+ * For an element drawing `drawn`, the animation of what is done to its cells.
148
+ * [LAW:types-are-the-program] Per-frame work is `at(t)`, so it runs once a
149
+ * frame by the shape of the call, never by a cache asking whether this cell's
150
+ * frame is the last one's.
151
+ *
152
+ * Its `nextChange` is when the frame next changes: a stepped effect its next
153
+ * step's start, a transition `t` until it is done and `Infinity` after, a
154
+ * loop that holds every cell still at the frame's depth `Infinity` there, and
155
+ * a loop that moves `t` (`Animation.continuous`). [LAW:types-are-the-program]
156
+ * The answer is a fact about the frame, so the animation is made from
157
+ * everything the frame is drawn from; one made from `t` alone could not say
158
+ * that a frame is still because of its depth.
128
159
  */
129
- export type Effect = (t: number, drawn: Drawn) => (colors: CellColors, cell: EffectCell) => CellColors;
160
+ export type Effect = (drawn: Drawn) => Animation<EffectFrame>;
130
161
  export interface EffectedOptions {
131
162
  /** The moment to draw, in seconds. */
132
163
  readonly t: number;
@@ -142,7 +173,7 @@ export interface EffectedOptions {
142
173
  */
143
174
  readonly theme: TerminalTheme;
144
175
  }
145
- export declare class Effected implements Renderable, Measurable {
176
+ export declare class Effected implements Renderable, Measurable, Composite {
146
177
  readonly renderable: Renderable;
147
178
  readonly effect: Effect;
148
179
  readonly t: number;
@@ -150,6 +181,9 @@ export declare class Effected implements Renderable, Measurable {
150
181
  readonly theme: TerminalTheme;
151
182
  constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
152
183
  render(options: RenderOptions): Iterable<Segment>;
184
+ contents(): Iterable<Renderable>;
185
+ private _moved;
186
+ private _recolored;
153
187
  measure(options: RenderOptions): {
154
188
  minimum: number;
155
189
  maximum: number;
@@ -171,6 +205,25 @@ export declare class Effected implements Renderable, Measurable {
171
205
  /** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
172
206
  private seed;
173
207
  }
208
+ /**
209
+ * `effect` sampled and held on `beat`: drawn at the moment the hold in
210
+ * progress began (`Beat.lastChange`), so within a hold every frame is the frame
211
+ * at its start, and at each hold's start it moves on. A continuous effect
212
+ * given a short curve becomes one that steps — a `wheel` turning a sixth of
213
+ * a turn once a second — and at one redraw a second a hold shorter than a
214
+ * second is skipped, as frames are. [LAW:no-ambient-temporal-coupling] The
215
+ * held time is a function of `t`, so the same `t` draws the same bytes at
216
+ * any rate. The frame next changes at a hold's start, and only one the
217
+ * sampled effect has moved by: the first hold start at or after what it
218
+ * answers for the frame held now (`Beat.nextChange`). An effect that moves
219
+ * every frame makes the beat's own answer, the end of this hold; one that is
220
+ * done and says never leaves the beat saying never too, since the hold after
221
+ * this one would draw the same frame. The answer is read off what the effect
222
+ * says, never off a comparison of frames: an effect that steps and is back
223
+ * on the same colour by the next hold still turns the beat there, an early
224
+ * answer the contract allows.
225
+ */
226
+ export declare function onBeat(effect: Effect, beat: Beat): Effect;
174
227
  /**
175
228
  * `effect` on the colours any of `owners` drew, itself or through an owner
176
229
  * nested in its output, and every other colour left as it is: one item of a
@@ -2,23 +2,26 @@
2
2
  * Effected — a renderable whose finished cells' colours pass through an
3
3
  * effect at a moment `t`.
4
4
  *
5
- * An effect is one pure function in two stages: a time and what the element
6
- * draws in that frame in, a function of the frame out; and into that, a
7
- * cell's colours and where the cell is, the cell's new colours out. Whatever
8
- * holds for the whole frame is worked out in the first stage, once, and every
9
- * cell of the frame is handed to the second, but for those a wrapper keeps
10
- * back: `confined` hands over only the cells its owners drew, so a second
11
- * stage never counts on seeing them all. Shimmer, pulse, fade and
12
- * dissolve are all this one shape, differing by what the function does with
13
- * its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
14
- * wrapper in another, which is the same as calling one after the other but
15
- * for what the outer one measures: the inner one's output.
5
+ * An effect is, for what an element draws in a frame, an `Animation`
6
+ * (`core/animation.ts`) of what is done to its cells: its value at `t` is a
7
+ * function from a cell's colours and where the cell is to the cell's new
8
+ * colours (`EffectFrame`), and it says when that next changes as every
9
+ * animation does. Whatever holds for the whole frame is worked out once, in
10
+ * the effect and its `at(t)`, and every cell of the frame is handed to the
11
+ * function `at(t)` returns, but for those a wrapper keeps back: `confined`
12
+ * hands over only the cells its owners drew, so an effect never counts on
13
+ * seeing them all. Shimmer, pulse, fade and dissolve are all this one shape,
14
+ * 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.
16
18
  *
17
- * What the element draws (`Drawn`) is measured here, in the render, off the
19
+ * What the element draws (`Drawn`) — the cells the effect will be handed,
20
+ * and the colours of its words — is measured here, in the render, off the
18
21
  * same reading of each segment its cells are drawn from, and handed to the
19
- * effect's first stage, once a frame. It is what an effect keeping words legible is
20
- * settled against (`onColors`, `./effects.ts`), so no caller derives it a
21
- * second time from a render of its own.
22
+ * effect once a frame. It is what an effect keeping words legible is settled
23
+ * against (`onColors`, `./effects.ts`), so no caller derives it a second time
24
+ * from a render of its own.
22
25
  *
23
26
  * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
24
27
  * clock read. Whoever draws frames builds the view for each frame's `t`, so
@@ -33,8 +36,10 @@
33
36
  * segment's link and meta and carries its anchor shifted to the run's first
34
37
  * cell (`Style.shiftedBy`), so hit testing still finds the owner.
35
38
  * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
36
- * and a segment whose every cell is unchanged is yielded as it was, so the
37
- * identity effect is byte-identical. Neighbouring cells the output depth
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.
42
+ * Neighbouring cells the output depth
38
43
  * writes alike stay one segment: a segment is cut only where the written
39
44
  * colours change, so two colours that land on one index are one run.
40
45
  * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
@@ -46,10 +51,12 @@
46
51
  * what the child drew, so a sweep crosses the element, not the screen.
47
52
  */
48
53
  import { drawnBy, shiftAnchor } from "../core/anchor.js";
54
+ import { Animation } from "../core/animation.js";
49
55
  import { cellLen, graphemes } from "../core/cells.js";
50
56
  import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
51
57
  import { fnv1a } from "../core/fnv1a.js";
52
58
  import { Measurement } from "../core/measure.js";
59
+ import { render } from "../core/protocol.js";
53
60
  import { Segment } from "../core/segment.js";
54
61
  import { JOIN, colorsOwnedBy } from "../core/strip.js";
55
62
  import { Style } from "../core/style.js";
@@ -69,21 +76,32 @@ export class Effected {
69
76
  this.key = options.key;
70
77
  this.theme = options.theme;
71
78
  }
72
- *render(options) {
73
- const segments = this.renderable.render(options);
74
- if (options.colorSystem === null) {
75
- yield* segments;
76
- return;
77
- }
79
+ render(options) {
80
+ const segments = render(this.renderable, options);
81
+ // With no colour output nothing the effect does is written, so the child
82
+ // is what is shown, and it changes when the child says it does.
83
+ 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) {
78
91
  // No depth named is no downgrade, as `drawnColors` reads it.
79
92
  const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
80
93
  // [LAW:one-source-of-truth] The frame is measured before any cell is
81
- // handed to the effect, off the same looks its cells are drawn from.
94
+ // handed to the effect, off the same looks its cells are drawn from, and
95
+ // the one measure is what the effect's animation is made from.
82
96
  const looked = looks(segments, depth, this.theme);
83
- const frame = this.effect(this.t, measured(looked, depth, this.theme));
84
- let row = 0;
85
- let col = 0;
86
- for (const { segment, look } of looked) {
97
+ const walked = walk(looked);
98
+ 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) }));
102
+ }
103
+ *_recolored(walked, frame, depth) {
104
+ for (const { segment, look, glyphs } of walked) {
87
105
  if (look === undefined) {
88
106
  yield segment;
89
107
  continue;
@@ -96,21 +114,9 @@ export class Effected {
96
114
  // writes, so two truecolors landing on one index stay one run. A cell
97
115
  // the effect gave its neighbour's colours is drawn as that neighbour was.
98
116
  const runs = [];
99
- // Cells into the segment, as `base.shiftedBy` counts them below.
100
- let into = 0;
101
- for (const glyph of graphemes(segment.text)) {
102
- let colors = slots;
103
- if (glyph === "\n") {
104
- row++;
105
- col = 0;
106
- }
107
- else {
108
- const anchor = base.anchor && shiftAnchor(base.anchor, into);
109
- // Back from the screen to the style's slots: the same swap undoes itself.
110
- colors = onScreen(frame(from, { row, col, seed: this.seed(row, col), terminal, anchor }), pair.reverse);
111
- col += cellLen(glyph);
112
- }
113
- into += cellLen(glyph);
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);
114
120
  const last = runs[runs.length - 1];
115
121
  const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
116
122
  if (last !== undefined && writesAlike(last.drawn, writes)) {
@@ -178,13 +184,101 @@ function looks(segments, depth, theme) {
178
184
  return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse } } };
179
185
  });
180
186
  }
181
- /** What `looked` draws its words in: `Drawn`'s measure, taken once per frame. */
182
- function measured(looked, depth, theme) {
187
+ /**
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.
193
+ */
194
+ function walk(looked) {
195
+ let row = 0;
196
+ let col = 0;
197
+ return looked.map(({ segment, look }) => {
198
+ if (look === undefined)
199
+ return { segment, look, glyphs: [] };
200
+ const anchor = look.style.anchor;
201
+ // Cells into the segment, as `base.shiftedBy` counts them in the render.
202
+ let into = 0;
203
+ const glyphs = [...graphemes(segment.text)].map((glyph) => {
204
+ const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
205
+ [row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + cellLen(glyph)];
206
+ into += cellLen(glyph);
207
+ return { glyph, cell };
208
+ });
209
+ return { segment, look, glyphs };
210
+ });
211
+ }
212
+ /** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
213
+ function drawsLines(style) {
214
+ return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
215
+ }
216
+ /**
217
+ * What `looked` draws: `Drawn`'s measure, taken once per frame. Its `cells`
218
+ * are listed off `walked` the first time they are asked for, since only an
219
+ * effect spread over the element asks, and a glyph walk is what measuring the
220
+ * words alone, a segment at a time, is spared (`drawnOf`).
221
+ */
222
+ function measured(looked, walked, depth, theme) {
223
+ const ground = theme.backgroundColor.hex;
224
+ let listed;
225
+ const list = () => walked().flatMap(({ look, glyphs }) => {
226
+ if (look === undefined)
227
+ return [];
228
+ // A blank glyph shows its ground, and any line its style draws in its ink.
229
+ const blankShows = look.pair.bg.hex !== ground || drawsLines(look.style);
230
+ return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (glyph.trim() !== "" || blankShows) ? [cell] : []));
231
+ });
183
232
  const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text.trim() !== "" && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
184
233
  const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
185
234
  // [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
186
235
  const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
187
- return { pairs, colors: new Set([...own("fg"), ...own("bg")]), fills: new Set(own("bg")), depth, theme };
236
+ return {
237
+ cells: () => (listed ??= list()),
238
+ pairs,
239
+ colors: new Set([...own("fg"), ...own("bg")]),
240
+ fills: new Set(own("bg")),
241
+ depth,
242
+ theme,
243
+ };
244
+ }
245
+ /**
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
+ };
188
282
  }
189
283
  /**
190
284
  * `effect` on the colours any of `owners` drew, itself or through an owner
@@ -199,16 +293,19 @@ function measured(looked, depth, theme) {
199
293
  */
200
294
  export function confined(effect, ...owners) {
201
295
  const confinedTo = new Set(owners);
202
- return (t, drawn) => {
203
- const frame = effect(t, drawn);
204
- return (colors, cell) => {
205
- const owned = colorsOwnedBy(cell.anchor, confinedTo);
206
- if (!owned.fg && !owned.bg)
207
- return colors;
208
- const moved = frame(colors, cell);
209
- return { fg: owned.fg ? moved.fg : colors.fg, bg: owned.bg ? moved.bg : colors.bg };
210
- };
211
- };
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;
299
+ // 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))) });
301
+ // 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
+ });
212
309
  }
213
310
  /**
214
311
  * What `segments`, drawn at `depth` on `theme`, draw their words in, measured
@@ -216,7 +313,8 @@ export function confined(effect, ...owners) {
216
313
  * output under an effect to read the contrast its words are left at.
217
314
  */
218
315
  export function drawnOf(segments, depth, theme) {
219
- return measured(looks(segments, depth, theme), depth, theme);
316
+ const looked = looks(segments, depth, theme);
317
+ return measured(looked, () => walk(looked), depth, theme);
220
318
  }
221
319
  /**
222
320
  * A style's colour slots as the screen shows them, or the reverse: `reverse`