@promptctl/rich-js 0.24.0 → 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/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- 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 +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- 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 +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- 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/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- 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 +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- 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 +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- 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 +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- 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 +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -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 +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -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 +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- 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 +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
package/dist/core/protocol.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
|
|
4
4
|
*/
|
|
5
5
|
import { cellCount } from "./cells.js";
|
|
6
|
-
import { DEFAULT_THEME } from "./style.js";
|
|
6
|
+
import { DEFAULT_THEME, Style } from "./style.js";
|
|
7
7
|
/** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
|
|
8
8
|
export const isAscii = (text) => /^[\x00-\x7F]*$/.test(text);
|
|
9
9
|
export function drawable(options, chosen, ascii, glyphs = String) {
|
|
@@ -61,6 +61,8 @@ export function fitHeight(lines, height) {
|
|
|
61
61
|
export function getStyle(options, style) {
|
|
62
62
|
return (options.theme ?? DEFAULT_THEME).resolve(style);
|
|
63
63
|
}
|
|
64
|
+
/** The terminal's own foreground on its own background. */
|
|
65
|
+
const TERMINAL_GROUND = Style.parse("default on default");
|
|
64
66
|
/**
|
|
65
67
|
* `style` as the depth the render is encoded at can draw it. A style that
|
|
66
68
|
* paints its own ground in fixed colours chose its ink and ground as a pair;
|
|
@@ -69,7 +71,10 @@ export function getStyle(options, style) {
|
|
|
69
71
|
* every bundled theme. So where the depth would turn a fixed colour of the
|
|
70
72
|
* pair into a slot, the pair is dropped and the run is drawn on the
|
|
71
73
|
* terminal's own foreground and background, which its theme does make
|
|
72
|
-
* readable; its attributes still set it apart.
|
|
74
|
+
* readable; its attributes still set it apart. Both are named, not left
|
|
75
|
+
* unset, so the colour of whatever the run sits in — a quote, a link — does
|
|
76
|
+
* not show through: Rich's code keeps its own colours inside a quote, and the
|
|
77
|
+
* terminal's are the ones it keeps here. A style whose colours are
|
|
73
78
|
* slots already (`white on blue`), or that paints no ground (`bold magenta`),
|
|
74
79
|
* was written for the terminal's own colours and is drawn as written. Rich
|
|
75
80
|
* draws a highlighted line number the same way below 256 colours
|
|
@@ -79,7 +84,7 @@ export function groundedStyle(options, style) {
|
|
|
79
84
|
const drawn = style.drawnColors(options.colorSystem ?? undefined);
|
|
80
85
|
const lost = (written, shown) => written?.fixedValue !== undefined && shown?.fixedValue === undefined;
|
|
81
86
|
const pairLost = style.bgcolor !== undefined && (lost(style.color, drawn.color) || lost(style.bgcolor, drawn.bgcolor));
|
|
82
|
-
return pairLost ? style.
|
|
87
|
+
return pairLost ? style.add(TERMINAL_GROUND) : style;
|
|
83
88
|
}
|
|
84
89
|
/**
|
|
85
90
|
* The options a renderable should work from: the caller's, with `maxWidth`
|
|
@@ -104,6 +109,200 @@ export function groundedStyle(options, style) {
|
|
|
104
109
|
export function withCellWidth(options) {
|
|
105
110
|
return { ...options, maxWidth: cellCount(options.maxWidth) };
|
|
106
111
|
}
|
|
112
|
+
/** Under one column there is nothing to draw, or to measure. */
|
|
113
|
+
export function drawsNothing(options) {
|
|
114
|
+
return cellCount(options.maxWidth) < 1;
|
|
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]()));
|
|
218
|
+
/**
|
|
219
|
+
* `renderable` drawn under `options`: Rich's `Console.render`, which every
|
|
220
|
+
* renderable there is drawn through, so under one column nothing is.
|
|
221
|
+
*
|
|
222
|
+
* [LAW:single-enforcer] The one place that rule is kept. Every call of a
|
|
223
|
+
* `Renderable`'s `render` in `src/` goes through here — a container drawing
|
|
224
|
+
* its child, a renderable drawing the text it built, a host drawing a frame —
|
|
225
|
+
* and `test/seam/render-calls.test.ts` fails on one that does not. Before
|
|
226
|
+
* this, each container asked `drawsNothing` itself, and the ones that forgot
|
|
227
|
+
* drew a child handed no column anyway.
|
|
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
|
+
*
|
|
233
|
+
* It is also the one reporter of what the view holds (`onPresent`). Under one
|
|
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.
|
|
239
|
+
*/
|
|
240
|
+
export function render(renderable, options) {
|
|
241
|
+
const opens = active === undefined;
|
|
242
|
+
const presence = drawsNothing(options) ? "held" : "drawn";
|
|
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)));
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* `options` as a measurement is asked with. A `measure` that renders to learn
|
|
296
|
+
* its shape draws nothing on the frame, so it reports nothing to `onPresent` —
|
|
297
|
+
* one that did would put a widget in the view at the moment a container
|
|
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.
|
|
300
|
+
*
|
|
301
|
+
* [LAW:single-enforcer] Every measurement starts here: `measure` asks with it.
|
|
302
|
+
*/
|
|
303
|
+
function measuring(options) {
|
|
304
|
+
return { ...options, onPresent: undefined };
|
|
305
|
+
}
|
|
107
306
|
/**
|
|
108
307
|
* The options a renderable should *lay out against*: `withCellWidth`, with an
|
|
109
308
|
* unbounded offer resolved to the renderable's own natural width.
|
|
@@ -139,23 +338,11 @@ export function withCellWidth(options) {
|
|
|
139
338
|
* rather than zero, and saying so names the cause — where `String.repeat` and
|
|
140
339
|
* `new Array` only ever name their own argument.
|
|
141
340
|
*/
|
|
142
|
-
/**
|
|
143
|
-
* `options` as a measurement is asked with. A `measure` that renders to learn
|
|
144
|
-
* its shape draws nothing on the frame, so it reports no owner to `onDraw` —
|
|
145
|
-
* one that did would put a widget in document order at the moment a
|
|
146
|
-
* container measured it, and keep one the frame then cropped away.
|
|
147
|
-
*
|
|
148
|
-
* [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
|
|
149
|
-
* `withBoundedWidth` ask through it, and each `measure` passes it on.
|
|
150
|
-
*/
|
|
151
|
-
export function measuring(options) {
|
|
152
|
-
return { ...options, onDraw: undefined };
|
|
153
|
-
}
|
|
154
341
|
export function withBoundedWidth(options, self) {
|
|
155
342
|
const parsed = withCellWidth(options);
|
|
156
343
|
if (Number.isFinite(parsed.maxWidth))
|
|
157
344
|
return parsed;
|
|
158
|
-
const natural =
|
|
345
|
+
const natural = measure(self, parsed).maximum;
|
|
159
346
|
if (!Number.isFinite(natural)) {
|
|
160
347
|
throw new RangeError("maxWidth is unbounded and this renderable has no natural width to fall " +
|
|
161
348
|
"back on: its content does not implement measure(), or something inside " +
|
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,10 +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";
|
|
49
|
+
import { render } from "./protocol.js";
|
|
45
50
|
import { OSC8_CLOSE, osc8Open } from "./osc8.js";
|
|
46
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
|
+
}
|
|
47
65
|
/**
|
|
48
66
|
* Encodes one segment as ANSI bytes for `destination`: the reference's
|
|
49
67
|
* `Style.render` for that segment, and nothing for a control segment.
|
|
@@ -51,7 +69,7 @@ const DEFAULT_WIDTH = 80;
|
|
|
51
69
|
export function segmentToString(segment, destination) {
|
|
52
70
|
if (segment.isControl)
|
|
53
71
|
return "";
|
|
54
|
-
const { text, style } = segment;
|
|
72
|
+
const { text, style } = shownOn(segment, destination);
|
|
55
73
|
// As the reference's `if not text` and `if style:` — no pair around nothing.
|
|
56
74
|
if (text.length === 0 || style === undefined)
|
|
57
75
|
return text;
|
|
@@ -74,18 +92,19 @@ export function renderToString(renderable, options) {
|
|
|
74
92
|
// [LAW:dataflow-not-control-flow] An absent spec is truecolor; an explicit
|
|
75
93
|
// `null` stays null (`??` would collapse it).
|
|
76
94
|
// [LAW:single-enforcer] Specs resolve through `resolveDestination`.
|
|
77
|
-
const
|
|
95
|
+
const destinationOptions = { reducedMotion: options?.reducedMotion };
|
|
78
96
|
if (options?.env !== undefined)
|
|
79
|
-
|
|
97
|
+
destinationOptions.env = options.env;
|
|
80
98
|
if (options?.isTTY !== undefined)
|
|
81
|
-
|
|
99
|
+
destinationOptions.isTTY = options.isTTY;
|
|
82
100
|
const rawSpec = options?.colorSystem;
|
|
83
|
-
const resolved = resolveDestination(rawSpec === undefined ? ColorDepth.TRUECOLOR : rawSpec,
|
|
101
|
+
const resolved = resolveDestination(rawSpec === undefined ? ColorDepth.TRUECOLOR : rawSpec, destinationOptions);
|
|
84
102
|
// [LAW:single-enforcer] This call's overrides are applied here, once, to
|
|
85
103
|
// make the one destination everything below is drawn for. `noColor` is a
|
|
86
104
|
// colour choice, so it overrides the depth and leaves hyperlinks to the
|
|
87
105
|
// destination; an explicit `hyperlinks` overrides what the destination takes.
|
|
88
106
|
const destination = {
|
|
107
|
+
...resolved,
|
|
89
108
|
colorSystem: options?.noColor ? null : resolved.colorSystem,
|
|
90
109
|
hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
|
|
91
110
|
};
|
|
@@ -93,6 +112,8 @@ export function renderToString(renderable, options) {
|
|
|
93
112
|
maxWidth: width,
|
|
94
113
|
isTerminal: false,
|
|
95
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,
|
|
96
117
|
theme: options?.theme,
|
|
97
118
|
onStyleError: options?.onStyleError,
|
|
98
119
|
highlight: options?.highlight,
|
|
@@ -101,5 +122,5 @@ export function renderToString(renderable, options) {
|
|
|
101
122
|
// so a renderable measures what this very call will draw.
|
|
102
123
|
colorSystem: destination.colorSystem,
|
|
103
124
|
};
|
|
104
|
-
return segmentsToString(
|
|
125
|
+
return segmentsToString(render(renderable, renderOptions), destination);
|
|
105
126
|
}
|
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;
|
|
@@ -42,7 +42,16 @@ export declare class Segment {
|
|
|
42
42
|
* whatever drew it, and its anchor says so; see `./anchor.ts`.
|
|
43
43
|
*/
|
|
44
44
|
splitCells(position: CellCol): [Segment, Segment];
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Cuts at each of `positions`, non-decreasing and inside this segment's
|
|
47
|
+
* cells: one piece more than there are positions, each exactly as wide as
|
|
48
|
+
* the distance between its cuts, the way `splitCells` cuts at one.
|
|
49
|
+
*
|
|
50
|
+
* [LAW:single-enforcer] Each piece but the first starts that many cells
|
|
51
|
+
* further into whatever drew it, and its anchor says so; see `./anchor.ts`.
|
|
52
|
+
*/
|
|
53
|
+
private cutAt;
|
|
54
|
+
private static readonly _line;
|
|
46
55
|
static line(): Segment;
|
|
47
56
|
/**
|
|
48
57
|
* `lines`, as drawn by `owner`: every segment stamped with the row and
|
|
@@ -53,7 +62,7 @@ export declare class Segment {
|
|
|
53
62
|
static anchorLines(lines: Segment[][], owner: object, firstRow?: number): Segment[][];
|
|
54
63
|
/**
|
|
55
64
|
* When what `segments` show next changes, in seconds on the timeline of
|
|
56
|
-
* whoever drew them: the soonest `Style.
|
|
65
|
+
* whoever drew them: the soonest `Style.hold` among them, read off
|
|
57
66
|
* the composed frame the way `anchorAt` reads an owner — `t` itself for a
|
|
58
67
|
* frame something in moves every frame, `Infinity` for one that never
|
|
59
68
|
* changes. What a host that drew the frame at `t` asks before drawing
|
|
@@ -61,6 +70,14 @@ export declare class Segment {
|
|
|
61
70
|
* `t` until it is done, still content never.
|
|
62
71
|
*/
|
|
63
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;
|
|
64
81
|
/**
|
|
65
82
|
* The anchor of the cell at column `x` of row `y` in a composed frame, or
|
|
66
83
|
* `undefined` when that cell was drawn by no owner or lies outside the frame.
|
|
@@ -150,7 +167,15 @@ export declare class Segment {
|
|
|
150
167
|
*/
|
|
151
168
|
static removeColor(segments: Iterable<Segment>): Iterable<Segment>;
|
|
152
169
|
/**
|
|
153
|
-
* Divides segments at cell positions
|
|
170
|
+
* Divides segments at cell positions: a section ending at each cut, and the
|
|
171
|
+
* cells after the last cut when there are any. A segment wider than zero
|
|
172
|
+
* that starts at a cut opens the section after it; one with no cells stays
|
|
173
|
+
* in the section it ends.
|
|
174
|
+
*
|
|
175
|
+
* One walk, and each segment is cut once at every cut that falls inside it:
|
|
176
|
+
* cutting what is left of it again at each cut re-measures and copies the
|
|
177
|
+
* rest every time, so a long line wrapped into many rows took time quadratic
|
|
178
|
+
* in its length.
|
|
154
179
|
*/
|
|
155
180
|
static divide(segments: Segment[], cuts: number[]): Segment[][];
|
|
156
181
|
/**
|
package/dist/core/segment.js
CHANGED
|
@@ -59,16 +59,15 @@ export class Segment {
|
|
|
59
59
|
return [this, new Segment("")];
|
|
60
60
|
if (position <= 0)
|
|
61
61
|
return [new Segment(""), this];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
];
|
|
62
|
+
return this.cutAt([position]);
|
|
63
|
+
}
|
|
64
|
+
cutAt(positions) {
|
|
65
|
+
return cutCells(this.text, positions).map((text, piece) => new Segment(text, piece === 0 ? this.style : this.style?.shiftedBy(positions[piece - 1])));
|
|
67
66
|
}
|
|
68
67
|
// --- Static factories ---
|
|
69
|
-
static _line;
|
|
68
|
+
static _line = new Segment("\n");
|
|
70
69
|
static line() {
|
|
71
|
-
return
|
|
70
|
+
return Segment._line;
|
|
72
71
|
}
|
|
73
72
|
// --- Static transformations ---
|
|
74
73
|
/**
|
|
@@ -93,7 +92,7 @@ export class Segment {
|
|
|
93
92
|
}
|
|
94
93
|
/**
|
|
95
94
|
* When what `segments` show next changes, in seconds on the timeline of
|
|
96
|
-
* whoever drew them: the soonest `Style.
|
|
95
|
+
* whoever drew them: the soonest `Style.hold` among them, read off
|
|
97
96
|
* the composed frame the way `anchorAt` reads an owner — `t` itself for a
|
|
98
97
|
* frame something in moves every frame, `Infinity` for one that never
|
|
99
98
|
* changes. What a host that drew the frame at `t` asks before drawing
|
|
@@ -103,9 +102,22 @@ export class Segment {
|
|
|
103
102
|
static nextChange(segments) {
|
|
104
103
|
let soonest = Number.POSITIVE_INFINITY;
|
|
105
104
|
for (const segment of segments)
|
|
106
|
-
soonest = Math.min(soonest, segment.style?.nextChange ?? Number.POSITIVE_INFINITY);
|
|
105
|
+
soonest = Math.min(soonest, segment.style?.hold.nextChange ?? Number.POSITIVE_INFINITY);
|
|
107
106
|
return soonest;
|
|
108
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
|
+
}
|
|
109
121
|
/**
|
|
110
122
|
* The anchor of the cell at column `x` of row `y` in a composed frame, or
|
|
111
123
|
* `undefined` when that cell was drawn by no owner or lies outside the frame.
|
|
@@ -129,8 +141,11 @@ export class Segment {
|
|
|
129
141
|
*/
|
|
130
142
|
static *applyStyle(segments, style, postStyle) {
|
|
131
143
|
// Applying no style is the identity, so the segments pass through as they
|
|
132
|
-
// are instead of each being copied into an equal one.
|
|
133
|
-
|
|
144
|
+
// are instead of each being copied into an equal one. A null style — a
|
|
145
|
+
// theme's plain entry, such as `markdown.item` — is no style.
|
|
146
|
+
const pre = style?.isNull ? undefined : style;
|
|
147
|
+
const post = postStyle?.isNull ? undefined : postStyle;
|
|
148
|
+
if (!pre && !post) {
|
|
134
149
|
yield* segments;
|
|
135
150
|
return;
|
|
136
151
|
}
|
|
@@ -140,10 +155,10 @@ export class Segment {
|
|
|
140
155
|
continue;
|
|
141
156
|
}
|
|
142
157
|
let s = segment.style;
|
|
143
|
-
if (
|
|
144
|
-
s =
|
|
145
|
-
if (
|
|
146
|
-
s = s ? s.add(
|
|
158
|
+
if (pre)
|
|
159
|
+
s = pre.add(s);
|
|
160
|
+
if (post)
|
|
161
|
+
s = s ? s.add(post) : post;
|
|
147
162
|
yield new Segment(segment.text, s, segment.control);
|
|
148
163
|
}
|
|
149
164
|
}
|
|
@@ -184,9 +199,8 @@ export class Segment {
|
|
|
184
199
|
}
|
|
185
200
|
}
|
|
186
201
|
}
|
|
187
|
-
if (currentLine.length > 0)
|
|
202
|
+
if (currentLine.length > 0)
|
|
188
203
|
lines.push(currentLine);
|
|
189
|
-
}
|
|
190
204
|
return lines;
|
|
191
205
|
}
|
|
192
206
|
/**
|
|
@@ -201,7 +215,10 @@ export class Segment {
|
|
|
201
215
|
return line;
|
|
202
216
|
return [...line, new Segment(" ".repeat(width - currentWidth), style)];
|
|
203
217
|
}
|
|
204
|
-
// 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.
|
|
205
222
|
const result = [];
|
|
206
223
|
let remaining = width;
|
|
207
224
|
for (const segment of line) {
|
|
@@ -214,11 +231,10 @@ export class Segment {
|
|
|
214
231
|
result.push(segment);
|
|
215
232
|
remaining -= segWidth;
|
|
216
233
|
}
|
|
217
|
-
else {
|
|
234
|
+
else if (remaining > 0) {
|
|
218
235
|
const [left] = segment.splitCells(asCellCol(remaining));
|
|
219
236
|
result.push(left);
|
|
220
237
|
remaining = 0;
|
|
221
|
-
break;
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
return result;
|
|
@@ -398,45 +414,44 @@ export class Segment {
|
|
|
398
414
|
}
|
|
399
415
|
}
|
|
400
416
|
/**
|
|
401
|
-
* Divides segments at cell positions
|
|
417
|
+
* Divides segments at cell positions: a section ending at each cut, and the
|
|
418
|
+
* cells after the last cut when there are any. A segment wider than zero
|
|
419
|
+
* that starts at a cut opens the section after it; one with no cells stays
|
|
420
|
+
* in the section it ends.
|
|
421
|
+
*
|
|
422
|
+
* One walk, and each segment is cut once at every cut that falls inside it:
|
|
423
|
+
* cutting what is left of it again at each cut re-measures and copies the
|
|
424
|
+
* rest every time, so a long line wrapped into many rows took time quadratic
|
|
425
|
+
* in its length.
|
|
402
426
|
*/
|
|
403
427
|
static divide(segments, cuts) {
|
|
404
428
|
if (cuts.length === 0)
|
|
405
429
|
return [segments];
|
|
406
|
-
const
|
|
407
|
-
let
|
|
408
|
-
let
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (currentSegment && !currentSegment.isControl && cellOffset < cut) {
|
|
419
|
-
const splitAt = asCellCol(cut - cellOffset);
|
|
420
|
-
const [left, right] = currentSegment.splitCells(splitAt);
|
|
421
|
-
if (left.hasText)
|
|
422
|
-
section.push(left);
|
|
423
|
-
cellOffset += left.cellLength;
|
|
424
|
-
currentSegment = right;
|
|
430
|
+
const sections = [[]];
|
|
431
|
+
let offset = 0;
|
|
432
|
+
let next = 0;
|
|
433
|
+
for (const segment of segments) {
|
|
434
|
+
const end = offset + segment.cellLength;
|
|
435
|
+
const inside = [];
|
|
436
|
+
for (; next < cuts.length && cuts[next] < end; next++) {
|
|
437
|
+
const cut = cuts[next];
|
|
438
|
+
if (cut <= offset)
|
|
439
|
+
sections.push([]);
|
|
440
|
+
else
|
|
441
|
+
inside.push(asCellCol(cut - offset));
|
|
425
442
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
segmentIndex++;
|
|
433
|
-
while (segmentIndex < segments.length) {
|
|
434
|
-
tail.push(segments[segmentIndex]);
|
|
435
|
-
segmentIndex++;
|
|
443
|
+
const [head, ...rest] = inside.length === 0 ? [segment] : segment.cutAt(inside);
|
|
444
|
+
sections[sections.length - 1].push(head);
|
|
445
|
+
// A piece between two cuts at one cell has no text: the section it is holds nothing.
|
|
446
|
+
for (const piece of rest)
|
|
447
|
+
sections.push(piece.hasText ? [piece] : []);
|
|
448
|
+
offset = end;
|
|
436
449
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
450
|
+
for (; next < cuts.length; next++)
|
|
451
|
+
sections.push([]);
|
|
452
|
+
if (sections[sections.length - 1].length === 0)
|
|
453
|
+
sections.pop();
|
|
454
|
+
return sections;
|
|
440
455
|
}
|
|
441
456
|
// --- Layout helpers ---
|
|
442
457
|
/**
|
|
@@ -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";
|