@promptctl/rich-js 0.24.1 → 0.25.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.
- 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 +21 -29
- 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 +13 -16
- package/dist/core/strip.js +119 -110
- 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 +132 -54
- package/dist/renderables/effect.js +188 -115
- 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
package/dist/core/protocol.js
CHANGED
|
@@ -113,6 +113,108 @@ export function withCellWidth(options) {
|
|
|
113
113
|
export function drawsNothing(options) {
|
|
114
114
|
return cellCount(options.maxWidth) < 1;
|
|
115
115
|
}
|
|
116
|
+
// [LAW:no-shared-mutable-globals] The render phase, owned here and nowhere
|
|
117
|
+
// else: only `render` and `measure` below open or enter a pass, and the rest of
|
|
118
|
+
// the library reads it through `activePass` and `afterPass`. Set and restored
|
|
119
|
+
// like a stack, so "not rendering" is `undefined` and needs no counter.
|
|
120
|
+
let active;
|
|
121
|
+
function runIn(phase, run) {
|
|
122
|
+
const outer = active;
|
|
123
|
+
active = phase;
|
|
124
|
+
try {
|
|
125
|
+
return run();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
active = outer;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `handler` run as if between passes: a callback a render makes to user code to
|
|
133
|
+
* report something it met, such as `onStyleError`, is an event handler, and may
|
|
134
|
+
* write what one may — print to a console, set a `State` — as React lets an
|
|
135
|
+
* event handler set state.
|
|
136
|
+
*/
|
|
137
|
+
export function betweenPasses(handler) {
|
|
138
|
+
return runIn(undefined, handler);
|
|
139
|
+
}
|
|
140
|
+
/** The pass a render or measure in progress belongs to, or `undefined` between passes. */
|
|
141
|
+
export function activePass() {
|
|
142
|
+
return active?.pass;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `effect` run once the render pass in progress has drawn its last segment,
|
|
146
|
+
* between passes, where it may write what an event handler may: React's commit
|
|
147
|
+
* phase. A render that learns something only drawing can tell it — the lines
|
|
148
|
+
* content shown through a `Viewport` runs to at the width it was handed —
|
|
149
|
+
* draws from what it learned and hands the commit here, rather than writing it
|
|
150
|
+
* as it draws.
|
|
151
|
+
*
|
|
152
|
+
* Effects run in the order they were handed over, which is document order. A
|
|
153
|
+
* measurement commits nothing, and neither does a pass that throws or whose
|
|
154
|
+
* caller stops iterating it early: nobody saw what it drew. Outside a pass — a
|
|
155
|
+
* renderable's `render` called directly — there is nothing to wait for, and
|
|
156
|
+
* `effect` runs now.
|
|
157
|
+
*/
|
|
158
|
+
export function afterPass(effect) {
|
|
159
|
+
if (active === undefined)
|
|
160
|
+
effect();
|
|
161
|
+
else
|
|
162
|
+
active.effects.push(effect);
|
|
163
|
+
}
|
|
164
|
+
/** The pass in progress, or a new one when this call is the outermost. */
|
|
165
|
+
const phaseHere = () => active ?? { pass: {}, effects: [] };
|
|
166
|
+
/**
|
|
167
|
+
* Every effect `phase`'s renders handed it, run now that it has ended — between
|
|
168
|
+
* passes even when the iteration that ended it was stepped inside another.
|
|
169
|
+
*/
|
|
170
|
+
function commit(phase) {
|
|
171
|
+
runIn(undefined, () => {
|
|
172
|
+
for (const effect of phase.effects.splice(0))
|
|
173
|
+
effect();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `drawn` with every step of its iteration run in `phase`, and its effects
|
|
178
|
+
* committed when the last step is done. A render is often a generator, whose
|
|
179
|
+
* body runs as its caller iterates — after `render` has returned — so the pass
|
|
180
|
+
* is re-entered on each `next()`, however many `yield`s a write sits behind,
|
|
181
|
+
* and ends only when the iteration does. An array has nothing left to run, so
|
|
182
|
+
* its pass has already ended.
|
|
183
|
+
*
|
|
184
|
+
* Only the call that opens the pass is wrapped. A nested render's steps run
|
|
185
|
+
* inside its parent's, which are already in the pass; a nested iterable could
|
|
186
|
+
* only be stepped outside it by being kept somewhere past the render, which
|
|
187
|
+
* takes a writable location `test/seam/render-purity.test.ts` refuses. Wrapping
|
|
188
|
+
* every level cost a third of the render time, once per segment per level.
|
|
189
|
+
*/
|
|
190
|
+
function withinPass(phase, drawn) {
|
|
191
|
+
if (Array.isArray(drawn)) {
|
|
192
|
+
commit(phase);
|
|
193
|
+
return drawn;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
[Symbol.iterator]: () => {
|
|
197
|
+
const steps = runIn(phase, () => drawn[Symbol.iterator]());
|
|
198
|
+
// On `%IteratorPrototype%`, as a generator's iterator is: iterable
|
|
199
|
+
// itself, and carrying the iterator helpers where the engine has them.
|
|
200
|
+
return Object.assign(Object.create(ITERATOR_PROTOTYPE), {
|
|
201
|
+
next: () => {
|
|
202
|
+
const step = runIn(phase, () => steps.next());
|
|
203
|
+
if (step.done === true)
|
|
204
|
+
commit(phase);
|
|
205
|
+
return step;
|
|
206
|
+
},
|
|
207
|
+
return: (value) => runIn(phase, () => steps.return?.(value) ?? { done: true, value: undefined }),
|
|
208
|
+
throw: (error) => runIn(phase, () => {
|
|
209
|
+
if (steps.throw)
|
|
210
|
+
return steps.throw(error);
|
|
211
|
+
throw error;
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const ITERATOR_PROTOTYPE = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
|
|
116
218
|
/**
|
|
117
219
|
* `renderable` drawn under `options`: Rich's `Console.render`, which every
|
|
118
220
|
* renderable there is drawn through, so under one column nothing is.
|
|
@@ -124,34 +226,81 @@ export function drawsNothing(options) {
|
|
|
124
226
|
* this, each container asked `drawsNothing` itself, and the ones that forgot
|
|
125
227
|
* drew a child handed no column anyway.
|
|
126
228
|
*
|
|
229
|
+
* It is where a render pass is entered (`Pass`), so it is the one place a
|
|
230
|
+
* write during render is refused at: a `State` set in a pass that did not make
|
|
231
|
+
* it throws (`./state.ts`).
|
|
232
|
+
*
|
|
127
233
|
* It is also the one reporter of what the view holds (`onPresent`). Under one
|
|
128
|
-
* column a renderable is held,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* instead, and dropping what it drew, did commit that state.
|
|
234
|
+
* column a renderable is held: in the view, with nothing of it on the frame.
|
|
235
|
+
* It is drawn all the same and what it drew dropped (`holding`), so what a held
|
|
236
|
+
* container is reported holding is what it draws, under the options it hands
|
|
237
|
+
* each child — one description of a container's traversal, not a second
|
|
238
|
+
* kept beside it.
|
|
134
239
|
*/
|
|
135
240
|
export function render(renderable, options) {
|
|
241
|
+
const opens = active === undefined;
|
|
136
242
|
const presence = drawsNothing(options) ? "held" : "drawn";
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
243
|
+
const { phase, drawing } = presence === "drawn" ? { phase: phaseHere(), drawing: options } : holding(options);
|
|
244
|
+
const drawn = runIn(phase, () => {
|
|
245
|
+
options.onPresent?.(renderable, presence);
|
|
246
|
+
return renderable.render(drawing);
|
|
247
|
+
});
|
|
248
|
+
if (presence === "held") {
|
|
249
|
+
runIn(phase, () => {
|
|
250
|
+
for (const _ of drawn)
|
|
251
|
+
;
|
|
252
|
+
});
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return opens ? withinPass(phase, drawn) : drawn;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Where a held renderable is drawn, and the options it is drawn under: what
|
|
259
|
+
* it draws is dropped, and everything it draws is reported held, whatever
|
|
260
|
+
* width it hands it. What it tells of its hold (`onHold`) still reaches the
|
|
261
|
+
* frame: an element not yet entered is held at no width, and must say when it
|
|
262
|
+
* enters and takes some. A motion held at no width therefore wakes its host
|
|
263
|
+
* for frames that look the same: when it next takes room is not a moment its
|
|
264
|
+
* hold can name, and an early answer costs a frame where a late one hides the
|
|
265
|
+
* element. It commits nothing, as a measurement commits nothing —
|
|
266
|
+
* its effects (`afterPass`) are learned at a width nobody sees, so a
|
|
267
|
+
* `Viewport` it shows content through keeps the scroll the last frame shown
|
|
268
|
+
* left it.
|
|
269
|
+
*
|
|
270
|
+
* [LAW:parse-dont-validate] It is drawn at no column, 0, whatever width under
|
|
271
|
+
* one its container worked out: `drawsNothing` reads a negative or NaN width
|
|
272
|
+
* as none, so none reaches a `render`, where it would be a count no string
|
|
273
|
+
* can repeat.
|
|
274
|
+
*/
|
|
275
|
+
function holding(options) {
|
|
276
|
+
const { onPresent } = options;
|
|
277
|
+
return {
|
|
278
|
+
phase: { pass: phaseHere().pass, effects: [] },
|
|
279
|
+
drawing: { ...options, maxWidth: 0, onPresent: onPresent && ((content) => onPresent(content, "held")) },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* What `measurable` reports under `options`: asked in a render pass, as
|
|
284
|
+
* `render` draws in one, with `measuring`'s options.
|
|
285
|
+
*
|
|
286
|
+
* [LAW:single-enforcer] Every call of a `Measurable`'s `measure` in `src/`
|
|
287
|
+
* goes through here, as every `render` goes through `render`;
|
|
288
|
+
* `test/seam/render-calls.test.ts` holds both.
|
|
289
|
+
*/
|
|
290
|
+
export function measure(measurable, options) {
|
|
291
|
+
// The pass's identity, with effects nothing runs: a measurement commits nothing.
|
|
292
|
+
return runIn({ pass: phaseHere().pass, effects: [] }, () => measurable.measure(measuring(options)));
|
|
144
293
|
}
|
|
145
294
|
/**
|
|
146
295
|
* `options` as a measurement is asked with. A `measure` that renders to learn
|
|
147
296
|
* its shape draws nothing on the frame, so it reports nothing to `onPresent` —
|
|
148
297
|
* one that did would put a widget in the view at the moment a container
|
|
149
|
-
* measured it, and keep one the frame then cropped away.
|
|
298
|
+
* measured it, and keep one the frame then cropped away. It still tells its
|
|
299
|
+
* hold (`onHold`): a layout decided by a measurement changes when it does.
|
|
150
300
|
*
|
|
151
|
-
* [LAW:single-enforcer] Every measurement starts here: `
|
|
152
|
-
* `withBoundedWidth` ask through it, and each `measure` passes it on.
|
|
301
|
+
* [LAW:single-enforcer] Every measurement starts here: `measure` asks with it.
|
|
153
302
|
*/
|
|
154
|
-
|
|
303
|
+
function measuring(options) {
|
|
155
304
|
return { ...options, onPresent: undefined };
|
|
156
305
|
}
|
|
157
306
|
/**
|
|
@@ -193,7 +342,7 @@ export function withBoundedWidth(options, self) {
|
|
|
193
342
|
const parsed = withCellWidth(options);
|
|
194
343
|
if (Number.isFinite(parsed.maxWidth))
|
|
195
344
|
return parsed;
|
|
196
|
-
const natural =
|
|
345
|
+
const natural = measure(self, parsed).maximum;
|
|
197
346
|
if (!Number.isFinite(natural)) {
|
|
198
347
|
throw new RangeError("maxWidth is unbounded and this renderable has no natural width to fall " +
|
|
199
348
|
"back on: its content does not implement measure(), or something inside " +
|
|
@@ -220,6 +369,3 @@ export function isScrollable(obj) {
|
|
|
220
369
|
"scrollBy" in obj &&
|
|
221
370
|
typeof obj.scrollBy === "function");
|
|
222
371
|
}
|
|
223
|
-
export function isComposite(obj) {
|
|
224
|
-
return "contents" in obj && typeof obj.contents === "function";
|
|
225
|
-
}
|
package/dist/core/render.d.ts
CHANGED
|
@@ -40,13 +40,16 @@
|
|
|
40
40
|
* removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
|
|
41
41
|
* follows OSC 8. This is where the port departs from the reference, whose
|
|
42
42
|
* `Style.render` returns bare text for a null colour system.
|
|
43
|
+
*
|
|
44
|
+
* The third fact, `reducedMotion`, changes the segment rather than its
|
|
45
|
+
* encoding: `shownOn` takes the style's blink out before it is encoded.
|
|
43
46
|
*/
|
|
44
47
|
import { ColorDepth } from "./color.js";
|
|
45
48
|
import type { ColorSystemName, Destination } from "./color.js";
|
|
46
49
|
import type { Env } from "./env.js";
|
|
47
|
-
import
|
|
50
|
+
import { Segment } from "./segment.js";
|
|
48
51
|
import type { Renderable, RenderOptions } from "./protocol.js";
|
|
49
|
-
export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "theme" | "onStyleError" | "highlight" | "highlighter"> {
|
|
52
|
+
export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "theme" | "onStyleError" | "highlight" | "highlighter"> {
|
|
50
53
|
/** Cell width to render into. Default 80. */
|
|
51
54
|
width?: number;
|
|
52
55
|
/**
|
|
@@ -77,6 +80,16 @@ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" |
|
|
|
77
80
|
*/
|
|
78
81
|
hyperlinks?: boolean;
|
|
79
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* `segment` as `destination` shows it: under reduced motion its style
|
|
85
|
+
* `reduced()`, so the terminal is never asked to blink it.
|
|
86
|
+
*
|
|
87
|
+
* [LAW:single-enforcer] The one place reduced motion reaches a style's own
|
|
88
|
+
* motion. Drawing cannot do it: a console lays its print and base styles over
|
|
89
|
+
* what was drawn, after `render`. The encoder writes what this returns, and a
|
|
90
|
+
* recording console records it, so every export replays what was written.
|
|
91
|
+
*/
|
|
92
|
+
export declare function shownOn(segment: Segment, destination: Destination): Segment;
|
|
80
93
|
/**
|
|
81
94
|
* Encodes one segment as ANSI bytes for `destination`: the reference's
|
|
82
95
|
* `Style.render` for that segment, and nothing for a control segment.
|
package/dist/core/render.js
CHANGED
|
@@ -40,11 +40,28 @@
|
|
|
40
40
|
* removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
|
|
41
41
|
* follows OSC 8. This is where the port departs from the reference, whose
|
|
42
42
|
* `Style.render` returns bare text for a null colour system.
|
|
43
|
+
*
|
|
44
|
+
* The third fact, `reducedMotion`, changes the segment rather than its
|
|
45
|
+
* encoding: `shownOn` takes the style's blink out before it is encoded.
|
|
43
46
|
*/
|
|
44
47
|
import { ColorDepth, resolveDestination } from "./color.js";
|
|
48
|
+
import { Segment } from "./segment.js";
|
|
45
49
|
import { render } from "./protocol.js";
|
|
46
50
|
import { OSC8_CLOSE, osc8Open } from "./osc8.js";
|
|
47
51
|
const DEFAULT_WIDTH = 80;
|
|
52
|
+
/**
|
|
53
|
+
* `segment` as `destination` shows it: under reduced motion its style
|
|
54
|
+
* `reduced()`, so the terminal is never asked to blink it.
|
|
55
|
+
*
|
|
56
|
+
* [LAW:single-enforcer] The one place reduced motion reaches a style's own
|
|
57
|
+
* motion. Drawing cannot do it: a console lays its print and base styles over
|
|
58
|
+
* what was drawn, after `render`. The encoder writes what this returns, and a
|
|
59
|
+
* recording console records it, so every export replays what was written.
|
|
60
|
+
*/
|
|
61
|
+
export function shownOn(segment, destination) {
|
|
62
|
+
const style = destination.reducedMotion ? segment.style?.reduced() : segment.style;
|
|
63
|
+
return style === segment.style ? segment : new Segment(segment.text, style, segment.control);
|
|
64
|
+
}
|
|
48
65
|
/**
|
|
49
66
|
* Encodes one segment as ANSI bytes for `destination`: the reference's
|
|
50
67
|
* `Style.render` for that segment, and nothing for a control segment.
|
|
@@ -52,7 +69,7 @@ const DEFAULT_WIDTH = 80;
|
|
|
52
69
|
export function segmentToString(segment, destination) {
|
|
53
70
|
if (segment.isControl)
|
|
54
71
|
return "";
|
|
55
|
-
const { text, style } = segment;
|
|
72
|
+
const { text, style } = shownOn(segment, destination);
|
|
56
73
|
// As the reference's `if not text` and `if style:` — no pair around nothing.
|
|
57
74
|
if (text.length === 0 || style === undefined)
|
|
58
75
|
return text;
|
|
@@ -75,18 +92,19 @@ export function renderToString(renderable, options) {
|
|
|
75
92
|
// [LAW:dataflow-not-control-flow] An absent spec is truecolor; an explicit
|
|
76
93
|
// `null` stays null (`??` would collapse it).
|
|
77
94
|
// [LAW:single-enforcer] Specs resolve through `resolveDestination`.
|
|
78
|
-
const
|
|
95
|
+
const destinationOptions = { reducedMotion: options?.reducedMotion };
|
|
79
96
|
if (options?.env !== undefined)
|
|
80
|
-
|
|
97
|
+
destinationOptions.env = options.env;
|
|
81
98
|
if (options?.isTTY !== undefined)
|
|
82
|
-
|
|
99
|
+
destinationOptions.isTTY = options.isTTY;
|
|
83
100
|
const rawSpec = options?.colorSystem;
|
|
84
|
-
const resolved = resolveDestination(rawSpec === undefined ? ColorDepth.TRUECOLOR : rawSpec,
|
|
101
|
+
const resolved = resolveDestination(rawSpec === undefined ? ColorDepth.TRUECOLOR : rawSpec, destinationOptions);
|
|
85
102
|
// [LAW:single-enforcer] This call's overrides are applied here, once, to
|
|
86
103
|
// make the one destination everything below is drawn for. `noColor` is a
|
|
87
104
|
// colour choice, so it overrides the depth and leaves hyperlinks to the
|
|
88
105
|
// destination; an explicit `hyperlinks` overrides what the destination takes.
|
|
89
106
|
const destination = {
|
|
107
|
+
...resolved,
|
|
90
108
|
colorSystem: options?.noColor ? null : resolved.colorSystem,
|
|
91
109
|
hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
|
|
92
110
|
};
|
|
@@ -94,6 +112,8 @@ export function renderToString(renderable, options) {
|
|
|
94
112
|
maxWidth: width,
|
|
95
113
|
isTerminal: false,
|
|
96
114
|
asciiOnly: options?.asciiOnly ?? false,
|
|
115
|
+
// [LAW:one-source-of-truth] What is drawn and how it is encoded take one preference.
|
|
116
|
+
reducedMotion: destination.reducedMotion,
|
|
97
117
|
theme: options?.theme,
|
|
98
118
|
onStyleError: options?.onStyleError,
|
|
99
119
|
highlight: options?.highlight,
|
package/dist/core/segment.d.ts
CHANGED
|
@@ -23,12 +23,12 @@ export declare enum ControlType {
|
|
|
23
23
|
ERASE_IN_LINE = "erase_in_line",
|
|
24
24
|
SET_WINDOW_TITLE = "set_window_title"
|
|
25
25
|
}
|
|
26
|
-
export type ControlCode = [ControlType, ...unknown[]];
|
|
26
|
+
export type ControlCode = readonly [ControlType, ...unknown[]];
|
|
27
27
|
export declare class Segment {
|
|
28
28
|
readonly text: string;
|
|
29
29
|
readonly style: Style | undefined;
|
|
30
|
-
readonly control: ControlCode[] | undefined;
|
|
31
|
-
constructor(text: string, style?: Style, control?: ControlCode[]);
|
|
30
|
+
readonly control: readonly ControlCode[] | undefined;
|
|
31
|
+
constructor(text: string, style?: Style, control?: readonly ControlCode[]);
|
|
32
32
|
get cellLength(): number;
|
|
33
33
|
get hasText(): boolean;
|
|
34
34
|
get isControl(): boolean;
|
|
@@ -51,7 +51,7 @@ export declare class Segment {
|
|
|
51
51
|
* further into whatever drew it, and its anchor says so; see `./anchor.ts`.
|
|
52
52
|
*/
|
|
53
53
|
private cutAt;
|
|
54
|
-
private static _line;
|
|
54
|
+
private static readonly _line;
|
|
55
55
|
static line(): Segment;
|
|
56
56
|
/**
|
|
57
57
|
* `lines`, as drawn by `owner`: every segment stamped with the row and
|
|
@@ -62,7 +62,7 @@ export declare class Segment {
|
|
|
62
62
|
static anchorLines(lines: Segment[][], owner: object, firstRow?: number): Segment[][];
|
|
63
63
|
/**
|
|
64
64
|
* When what `segments` show next changes, in seconds on the timeline of
|
|
65
|
-
* whoever drew them: the soonest `Style.
|
|
65
|
+
* whoever drew them: the soonest `Style.hold` among them, read off
|
|
66
66
|
* the composed frame the way `anchorAt` reads an owner — `t` itself for a
|
|
67
67
|
* frame something in moves every frame, `Infinity` for one that never
|
|
68
68
|
* changes. What a host that drew the frame at `t` asks before drawing
|
|
@@ -70,6 +70,14 @@ export declare class Segment {
|
|
|
70
70
|
* `t` until it is done, still content never.
|
|
71
71
|
*/
|
|
72
72
|
static nextChange(segments: Iterable<Segment>): number;
|
|
73
|
+
/**
|
|
74
|
+
* Since when what `segments` show has been what it is, on the same
|
|
75
|
+
* timeline: the latest `Style.hold` among them, the other end of the
|
|
76
|
+
* hold `nextChange` reads — `t` for a frame something in moves every
|
|
77
|
+
* frame, `-Infinity` for one that always was. What a host running its
|
|
78
|
+
* timeline backwards asks before drawing again.
|
|
79
|
+
*/
|
|
80
|
+
static lastChange(segments: Iterable<Segment>): number;
|
|
73
81
|
/**
|
|
74
82
|
* The anchor of the cell at column `x` of row `y` in a composed frame, or
|
|
75
83
|
* `undefined` when that cell was drawn by no owner or lies outside the frame.
|
package/dist/core/segment.js
CHANGED
|
@@ -65,9 +65,9 @@ export class Segment {
|
|
|
65
65
|
return cutCells(this.text, positions).map((text, piece) => new Segment(text, piece === 0 ? this.style : this.style?.shiftedBy(positions[piece - 1])));
|
|
66
66
|
}
|
|
67
67
|
// --- Static factories ---
|
|
68
|
-
static _line;
|
|
68
|
+
static _line = new Segment("\n");
|
|
69
69
|
static line() {
|
|
70
|
-
return
|
|
70
|
+
return Segment._line;
|
|
71
71
|
}
|
|
72
72
|
// --- Static transformations ---
|
|
73
73
|
/**
|
|
@@ -92,7 +92,7 @@ export class Segment {
|
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* When what `segments` show next changes, in seconds on the timeline of
|
|
95
|
-
* whoever drew them: the soonest `Style.
|
|
95
|
+
* whoever drew them: the soonest `Style.hold` among them, read off
|
|
96
96
|
* the composed frame the way `anchorAt` reads an owner — `t` itself for a
|
|
97
97
|
* frame something in moves every frame, `Infinity` for one that never
|
|
98
98
|
* changes. What a host that drew the frame at `t` asks before drawing
|
|
@@ -102,9 +102,22 @@ export class Segment {
|
|
|
102
102
|
static nextChange(segments) {
|
|
103
103
|
let soonest = Number.POSITIVE_INFINITY;
|
|
104
104
|
for (const segment of segments)
|
|
105
|
-
soonest = Math.min(soonest, segment.style?.nextChange ?? Number.POSITIVE_INFINITY);
|
|
105
|
+
soonest = Math.min(soonest, segment.style?.hold.nextChange ?? Number.POSITIVE_INFINITY);
|
|
106
106
|
return soonest;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Since when what `segments` show has been what it is, on the same
|
|
110
|
+
* timeline: the latest `Style.hold` among them, the other end of the
|
|
111
|
+
* hold `nextChange` reads — `t` for a frame something in moves every
|
|
112
|
+
* frame, `-Infinity` for one that always was. What a host running its
|
|
113
|
+
* timeline backwards asks before drawing again.
|
|
114
|
+
*/
|
|
115
|
+
static lastChange(segments) {
|
|
116
|
+
let latest = Number.NEGATIVE_INFINITY;
|
|
117
|
+
for (const segment of segments)
|
|
118
|
+
latest = Math.max(latest, segment.style?.hold.lastChange ?? Number.NEGATIVE_INFINITY);
|
|
119
|
+
return latest;
|
|
120
|
+
}
|
|
108
121
|
/**
|
|
109
122
|
* The anchor of the cell at column `x` of row `y` in a composed frame, or
|
|
110
123
|
* `undefined` when that cell was drawn by no owner or lies outside the frame.
|
|
@@ -186,9 +199,8 @@ export class Segment {
|
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
}
|
|
189
|
-
if (currentLine.length > 0)
|
|
202
|
+
if (currentLine.length > 0)
|
|
190
203
|
lines.push(currentLine);
|
|
191
|
-
}
|
|
192
204
|
return lines;
|
|
193
205
|
}
|
|
194
206
|
/**
|
|
@@ -203,7 +215,10 @@ export class Segment {
|
|
|
203
215
|
return line;
|
|
204
216
|
return [...line, new Segment(" ".repeat(width - currentWidth), style)];
|
|
205
217
|
}
|
|
206
|
-
// Crop to width
|
|
218
|
+
// Crop to width: the cells past it go, and every piece of no cells stays —
|
|
219
|
+
// a control, or an empty piece of text, writes nothing there, and may be
|
|
220
|
+
// all that says when its frame changes (`Style.hold`). Not the
|
|
221
|
+
// reference, which stops at the cut.
|
|
207
222
|
const result = [];
|
|
208
223
|
let remaining = width;
|
|
209
224
|
for (const segment of line) {
|
|
@@ -216,11 +231,10 @@ export class Segment {
|
|
|
216
231
|
result.push(segment);
|
|
217
232
|
remaining -= segWidth;
|
|
218
233
|
}
|
|
219
|
-
else {
|
|
234
|
+
else if (remaining > 0) {
|
|
220
235
|
const [left] = segment.splitCells(asCellCol(remaining));
|
|
221
236
|
result.push(left);
|
|
222
237
|
remaining = 0;
|
|
223
|
-
break;
|
|
224
238
|
}
|
|
225
239
|
}
|
|
226
240
|
return result;
|
|
@@ -8,5 +8,5 @@ export interface SpinnerData {
|
|
|
8
8
|
}
|
|
9
9
|
/** The spinner an ASCII-only output draws in place of any other. */
|
|
10
10
|
export declare const LINE_SPINNER: SpinnerData;
|
|
11
|
-
export declare const SPINNERS: Record<string, SpinnerData
|
|
11
|
+
export declare const SPINNERS: Readonly<Record<string, SpinnerData>>;
|
|
12
12
|
export declare const DEFAULT_SPINNER = "dots";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state — `State<T>`, the one place a renderable keeps state that changes after
|
|
3
|
+
* it is built, and the refusal of a write to it while something is drawn.
|
|
4
|
+
*
|
|
5
|
+
* A render changes nothing. React refuses a state update during render because
|
|
6
|
+
* state changes only through a setter, and the setter knows the render phase;
|
|
7
|
+
* this is that setter. `set` throws while a render or measure is in progress
|
|
8
|
+
* (`activePass`, owned by `render` and `measure` in `./protocol.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Ownership is React's line too: a state made during a pass belongs to that
|
|
11
|
+
* pass, and the pass may set it. A render that builds a fresh value and fills
|
|
12
|
+
* it as it draws — a `Table` it adds rows to — changes nothing anyone held
|
|
13
|
+
* before it began. Only a state that existed before the pass is refused.
|
|
14
|
+
*
|
|
15
|
+
* `ListState` is the same for a list a renderable appends to — a table's rows,
|
|
16
|
+
* a text's spans — which a `State<readonly T[]>` would copy on every append.
|
|
17
|
+
*
|
|
18
|
+
* `refuseRenderWrite` is that refusal, written once: a widget's observables
|
|
19
|
+
* refuse a write through it too (`src/widgets/observable.ts`).
|
|
20
|
+
*
|
|
21
|
+
* `test/seam/render-purity.test.ts` is the static half: nothing else reachable
|
|
22
|
+
* from a renderable, or from module scope, may be writable.
|
|
23
|
+
*/
|
|
24
|
+
import { type Pass } from "./protocol.js";
|
|
25
|
+
/** State was written during a render or measure that did not make it. */
|
|
26
|
+
export declare class RenderWriteError extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
export declare class State<T> {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(value: T);
|
|
32
|
+
get(): T;
|
|
33
|
+
set(value: T): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A list kept as `State` keeps a value, appended to in place: a `State` of a
|
|
37
|
+
* readonly array copies it on every append, which makes filling a 10,000-row
|
|
38
|
+
* table quadratic. `get` hands out the list itself, typed readonly, so a
|
|
39
|
+
* reader that keeps it sees later appends; `set` replaces it, leaving a kept
|
|
40
|
+
* one as it was.
|
|
41
|
+
*/
|
|
42
|
+
export declare class ListState<T> {
|
|
43
|
+
#private;
|
|
44
|
+
constructor(items?: Iterable<T>);
|
|
45
|
+
get(): readonly T[];
|
|
46
|
+
push(item: T): void;
|
|
47
|
+
set(items: Iterable<T>): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Throws unless state made in `owner` — the pass in progress when it was made,
|
|
51
|
+
* if any — may be written now: between passes, or in the pass that made it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function refuseRenderWrite(owner: Pass | undefined): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state — `State<T>`, the one place a renderable keeps state that changes after
|
|
3
|
+
* it is built, and the refusal of a write to it while something is drawn.
|
|
4
|
+
*
|
|
5
|
+
* A render changes nothing. React refuses a state update during render because
|
|
6
|
+
* state changes only through a setter, and the setter knows the render phase;
|
|
7
|
+
* this is that setter. `set` throws while a render or measure is in progress
|
|
8
|
+
* (`activePass`, owned by `render` and `measure` in `./protocol.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Ownership is React's line too: a state made during a pass belongs to that
|
|
11
|
+
* pass, and the pass may set it. A render that builds a fresh value and fills
|
|
12
|
+
* it as it draws — a `Table` it adds rows to — changes nothing anyone held
|
|
13
|
+
* before it began. Only a state that existed before the pass is refused.
|
|
14
|
+
*
|
|
15
|
+
* `ListState` is the same for a list a renderable appends to — a table's rows,
|
|
16
|
+
* a text's spans — which a `State<readonly T[]>` would copy on every append.
|
|
17
|
+
*
|
|
18
|
+
* `refuseRenderWrite` is that refusal, written once: a widget's observables
|
|
19
|
+
* refuse a write through it too (`src/widgets/observable.ts`).
|
|
20
|
+
*
|
|
21
|
+
* `test/seam/render-purity.test.ts` is the static half: nothing else reachable
|
|
22
|
+
* from a renderable, or from module scope, may be writable.
|
|
23
|
+
*/
|
|
24
|
+
import { activePass } from "./protocol.js";
|
|
25
|
+
/** State was written during a render or measure that did not make it. */
|
|
26
|
+
export class RenderWriteError extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("State was written during a render or measure. A render changes nothing: " +
|
|
29
|
+
"change state in an event handler, an update, or between frames.");
|
|
30
|
+
this.name = "RenderWriteError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class State {
|
|
34
|
+
#value;
|
|
35
|
+
#owner = activePass();
|
|
36
|
+
constructor(value) {
|
|
37
|
+
this.#value = value;
|
|
38
|
+
}
|
|
39
|
+
get() {
|
|
40
|
+
return this.#value;
|
|
41
|
+
}
|
|
42
|
+
set(value) {
|
|
43
|
+
refuseRenderWrite(this.#owner);
|
|
44
|
+
this.#value = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A list kept as `State` keeps a value, appended to in place: a `State` of a
|
|
49
|
+
* readonly array copies it on every append, which makes filling a 10,000-row
|
|
50
|
+
* table quadratic. `get` hands out the list itself, typed readonly, so a
|
|
51
|
+
* reader that keeps it sees later appends; `set` replaces it, leaving a kept
|
|
52
|
+
* one as it was.
|
|
53
|
+
*/
|
|
54
|
+
export class ListState {
|
|
55
|
+
#items;
|
|
56
|
+
#owner = activePass();
|
|
57
|
+
constructor(items = []) {
|
|
58
|
+
this.#items = [...items];
|
|
59
|
+
}
|
|
60
|
+
get() {
|
|
61
|
+
return this.#items;
|
|
62
|
+
}
|
|
63
|
+
// One item, not a rest parameter: a caller spreading a long list into one overflows the call stack.
|
|
64
|
+
push(item) {
|
|
65
|
+
refuseRenderWrite(this.#owner);
|
|
66
|
+
this.#items.push(item);
|
|
67
|
+
}
|
|
68
|
+
set(items) {
|
|
69
|
+
refuseRenderWrite(this.#owner);
|
|
70
|
+
this.#items = [...items];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Throws unless state made in `owner` — the pass in progress when it was made,
|
|
75
|
+
* if any — may be written now: between passes, or in the pass that made it.
|
|
76
|
+
*/
|
|
77
|
+
export function refuseRenderWrite(owner) {
|
|
78
|
+
const pass = activePass();
|
|
79
|
+
// [LAW:no-silent-failure] Refused loudly, as React refuses a setState in render.
|
|
80
|
+
if (pass !== undefined && pass !== owner)
|
|
81
|
+
throw new RenderWriteError();
|
|
82
|
+
}
|