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