@promptctl/rich-js 0.18.1 → 0.20.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 +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
|
@@ -3,19 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import { embed } from "./embed.js";
|
|
6
|
-
import { fitHeight,
|
|
6
|
+
import { fitHeight, regionRows, stackedHeight, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
|
|
7
7
|
import { Measurement } from "../core/measure.js";
|
|
8
8
|
import { cellCount } from "../core/cells.js";
|
|
9
|
-
|
|
10
|
-
* A share weight, not a cell count: fractions divide space meaningfully, so
|
|
11
|
-
* this parses where `cellCount` would floor. A weight that cannot name a share
|
|
12
|
-
* — negative, NaN, infinite — reads as zero, which already means "this pane
|
|
13
|
-
* does not grow" and is filtered out before any division. That is what makes
|
|
14
|
-
* every ratio reaching `_distributeSpace` and `_rowBudgetFor` positive.
|
|
15
|
-
*/
|
|
16
|
-
function growthRatio(ratio) {
|
|
17
|
-
return Number.isFinite(ratio) && ratio > 0 ? ratio : 0;
|
|
18
|
-
}
|
|
9
|
+
import { ratioBudget, ratioResolve, shareWeight } from "./ratio.js";
|
|
19
10
|
/**
|
|
20
11
|
* A pane rendered into a region of `rows` and held to exactly that many lines,
|
|
21
12
|
* or — with `rows` undefined — rendered under the layout's own budget, as a
|
|
@@ -55,7 +46,7 @@ export class Layout {
|
|
|
55
46
|
* constructor. All three are public and a caller reaches them long after
|
|
56
47
|
* construction — `layout.getByName("pane")!.ratio = -1` walked straight past a
|
|
57
48
|
* constructor-only parse and put a negative weight back into the division that
|
|
58
|
-
* `
|
|
49
|
+
* `ratioResolve` and `ratioBudget` are written to trust.
|
|
59
50
|
*
|
|
60
51
|
* [LAW:parse-dont-validate] The setter is the border, so the guarantee holds
|
|
61
52
|
* for the object's whole lifetime and nothing downstream re-checks. `size` and
|
|
@@ -67,7 +58,7 @@ export class Layout {
|
|
|
67
58
|
return this._ratio;
|
|
68
59
|
}
|
|
69
60
|
set ratio(value) {
|
|
70
|
-
this._ratio =
|
|
61
|
+
this._ratio = shareWeight(value);
|
|
71
62
|
}
|
|
72
63
|
get size() {
|
|
73
64
|
return this._size;
|
|
@@ -125,13 +116,23 @@ export class Layout {
|
|
|
125
116
|
// would otherwise hand each of them the caller's raw number.
|
|
126
117
|
const options = withBoundedWidth(rawOptions, this);
|
|
127
118
|
if (this._isLeaf) {
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
119
|
+
// Shaped to its region, as Rich's `render_lines` shapes a pane: every
|
|
120
|
+
// line cropped and padded to the width, and held to the region's rows.
|
|
121
|
+
// The leaf sets its content's region, so it shapes what comes back, as
|
|
122
|
+
// `paneLines` shapes a pane for the split that set the pane's.
|
|
123
|
+
// Cropped rather than forwarded because content is free to ignore its
|
|
124
|
+
// offer, and a pane wider than its region is the one thing a layout may
|
|
125
|
+
// never emit — in a row split it overwrites the pane beside it. Held to
|
|
126
|
+
// the rows because content keeps its own `end`: `RichText("status:
|
|
127
|
+
// ok\n")` is two lines, and a one-row region cuts the second. Every line
|
|
128
|
+
// ended, as the split paths below end theirs: a leaf stacked in a
|
|
129
|
+
// `Group` is still a region of lines.
|
|
133
130
|
if (this._renderable) {
|
|
134
|
-
|
|
131
|
+
const lines = fitHeight(Segment.splitLines(this._renderable.render(options)), options.height);
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
yield* Segment.adjustLineLength(line, options.maxWidth);
|
|
134
|
+
yield Segment.line();
|
|
135
|
+
}
|
|
135
136
|
}
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
@@ -152,7 +153,7 @@ export class Layout {
|
|
|
152
153
|
const region = regionRows(options.height);
|
|
153
154
|
const shares = region === undefined
|
|
154
155
|
? children.map((child) => child.size)
|
|
155
|
-
:
|
|
156
|
+
: ratioResolve(region, children);
|
|
156
157
|
for (let i = 0; i < children.length; i++) {
|
|
157
158
|
for (const line of paneLines(children[i], options, shares[i])) {
|
|
158
159
|
yield* line;
|
|
@@ -166,7 +167,7 @@ export class Layout {
|
|
|
166
167
|
// width made every share NaN and the merge threw `Invalid array length`.
|
|
167
168
|
// Every pane stands in the whole region, and with none the merge pads the
|
|
168
169
|
// row to its tallest pane.
|
|
169
|
-
const widths =
|
|
170
|
+
const widths = ratioResolve(options.maxWidth, children);
|
|
170
171
|
const region = regionRows(options.height);
|
|
171
172
|
const cells = children.map((child, i) => ({
|
|
172
173
|
width: widths[i],
|
|
@@ -174,55 +175,6 @@ export class Layout {
|
|
|
174
175
|
}));
|
|
175
176
|
yield* Segment.mergeHorizontal(cells);
|
|
176
177
|
}
|
|
177
|
-
_distributeSpace(children, totalSpace) {
|
|
178
|
-
const sizes = new Array(children.length).fill(0);
|
|
179
|
-
let remaining = totalSpace;
|
|
180
|
-
// Children that do not grow are paid first: a declared `size`, and equally
|
|
181
|
-
// a `ratio` of 0, which is the caller saying this pane stays at its
|
|
182
|
-
// `minimumSize`. Left in the ratio pass below, a zero-ratio child divided
|
|
183
|
-
// by its own ratio and drew its minimum out of a budget the shares had
|
|
184
|
-
// already spent — three panes of `zzz`/`aaaaa`/`bbbbb` consumed
|
|
185
|
-
// `3 + 2*floor(T/2)` cells out of `T`, which no `T` satisfies, and the row
|
|
186
|
-
// only came out right at 16 because the `Math.min(remaining, ...)` below
|
|
187
|
-
// happened to clip the last share to the right number.
|
|
188
|
-
const growing = [];
|
|
189
|
-
for (let i = 0; i < children.length; i++) {
|
|
190
|
-
const child = children[i];
|
|
191
|
-
if (child.size !== undefined) {
|
|
192
|
-
sizes[i] = Math.min(child.size, remaining);
|
|
193
|
-
}
|
|
194
|
-
else if (child.ratio === 0) {
|
|
195
|
-
sizes[i] = Math.min(child.minimumSize, remaining);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
growing.push(i);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
remaining -= sizes[i];
|
|
202
|
-
}
|
|
203
|
-
// Distribute what is left by ratio. Each child's share comes off one
|
|
204
|
-
// budget and is bounded by what that budget still holds: `minimumSize` is
|
|
205
|
-
// what a child asks for, `remaining` is what there is to give, and the
|
|
206
|
-
// second beats the first. Applied without that bound — as
|
|
207
|
-
// `Math.max(minimumSize, share)` alone — every child claimed its whole
|
|
208
|
-
// minimum out of a budget that could not cover one of them, so two
|
|
209
|
-
// children of a Layout asked to fit one cell merged into a two-cell row.
|
|
210
|
-
if (growing.length > 0 && remaining > 0) {
|
|
211
|
-
const budget = remaining;
|
|
212
|
-
// Every ratio in here is positive — `growthRatio` parses the rest to zero
|
|
213
|
-
// at the constructor and zero is filtered out above — which is what makes
|
|
214
|
-
// the division safe and `_rowBudgetFor`'s inverse of it finite.
|
|
215
|
-
const totalRatio = growing.reduce((s, idx) => s + children[idx].ratio, 0);
|
|
216
|
-
for (const idx of growing) {
|
|
217
|
-
const child = children[idx];
|
|
218
|
-
const share = Math.floor(budget * child.ratio / totalRatio);
|
|
219
|
-
const allocated = Math.min(remaining, Math.max(child.minimumSize, share));
|
|
220
|
-
sizes[idx] = allocated;
|
|
221
|
-
remaining -= allocated;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return sizes;
|
|
225
|
-
}
|
|
226
178
|
/**
|
|
227
179
|
* The width this layout would take if nothing constrained it: its content's
|
|
228
180
|
* for a leaf, its children's laid out the way the split lays them out.
|
|
@@ -237,19 +189,14 @@ export class Layout {
|
|
|
237
189
|
if (this._isLeaf) {
|
|
238
190
|
if (this._renderable === undefined)
|
|
239
191
|
return 0;
|
|
240
|
-
|
|
241
|
-
// report, so it reports the offer — unbounded included, which is where
|
|
242
|
-
// `withBoundedWidth` says so rather than inventing a number.
|
|
243
|
-
return isMeasurable(this._renderable)
|
|
244
|
-
? Measurement.get(options, this._renderable).maximum
|
|
245
|
-
: options.maxWidth;
|
|
192
|
+
return Measurement.get(options, this._renderable).maximum;
|
|
246
193
|
}
|
|
247
194
|
const visible = this._children.filter((c) => c.visible);
|
|
248
195
|
if (visible.length === 0)
|
|
249
196
|
return 0;
|
|
250
197
|
const widths = visible.map((c) => c._naturalWidth(options));
|
|
251
198
|
if (this._splitDirection === "row")
|
|
252
|
-
return
|
|
199
|
+
return ratioBudget(visible, widths);
|
|
253
200
|
// Accumulated, not spread: a column split holds as many children as a caller
|
|
254
201
|
// made, and `Math.max(...widths)` passes one argument per child, so a
|
|
255
202
|
// generated dashboard deep enough overruns the engine's argument limit and
|
|
@@ -259,42 +206,6 @@ export class Layout {
|
|
|
259
206
|
widest = Math.max(widest, width);
|
|
260
207
|
return widest;
|
|
261
208
|
}
|
|
262
|
-
/**
|
|
263
|
-
* The budget at which `_distributeSpace` gives every child of a row at least
|
|
264
|
-
* the width it wants — the inverse of the ratio rule above, and the reason
|
|
265
|
-
* this is not the sum of those widths.
|
|
266
|
-
*
|
|
267
|
-
* [LAW:one-source-of-truth] A growing child receives `floor(budget * ratio /
|
|
268
|
-
* totalRatio)`, so to receive `want` cells it needs the budget to reach
|
|
269
|
-
* `want * totalRatio / ratio`, and one budget serves them all: the row needs
|
|
270
|
-
* the largest demand, never their total. Summed instead, a 1:1 split of
|
|
271
|
-
* "left" and "right" reported 9 and then rendered "right" into the 4 cells
|
|
272
|
-
* that `floor(9/2)` actually hands it.
|
|
273
|
-
*
|
|
274
|
-
* The panes that do not grow are paid first here for the same reason they
|
|
275
|
-
* are paid first there — and it is the reason this divides by no zero: a
|
|
276
|
-
* `ratio` of 0 never reaches the division, because a pane that does not grow
|
|
277
|
-
* demands nothing of a budget for growing.
|
|
278
|
-
*/
|
|
279
|
-
_rowBudgetFor(children, widths) {
|
|
280
|
-
const growing = children.filter((c) => c.size === undefined && c.ratio !== 0);
|
|
281
|
-
const totalRatio = growing.reduce((sum, c) => sum + c.ratio, 0);
|
|
282
|
-
let pinned = 0;
|
|
283
|
-
let budget = 0;
|
|
284
|
-
for (const [i, child] of children.entries()) {
|
|
285
|
-
if (child.size !== undefined) {
|
|
286
|
-
pinned += child.size;
|
|
287
|
-
}
|
|
288
|
-
else if (child.ratio === 0) {
|
|
289
|
-
pinned += child.minimumSize;
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
const want = Math.max(child.minimumSize, widths[i]);
|
|
293
|
-
budget = Math.max(budget, Math.ceil(want * totalRatio / child.ratio));
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return pinned + budget;
|
|
297
|
-
}
|
|
298
209
|
measure(rawOptions) {
|
|
299
210
|
// `minimumSize` is what this layout asks for and the ceiling is what it was
|
|
300
211
|
// offered; the ceiling wins. Unclamped, a layout measured into no width
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListView — items stacked down a `Viewport`, scrolled to keep the selected
|
|
3
|
+
* one in view. `moveSelection` (core/selection) is the cursor that picks it.
|
|
4
|
+
*
|
|
5
|
+
* An item can wrap, so the lines the selected one covers exist only at the
|
|
6
|
+
* width it is drawn at, which a render is handed and whoever builds the frame
|
|
7
|
+
* is not. So the items are drawn here, at the viewport's content width and
|
|
8
|
+
* with no height — the lines the viewport scrolls — and the viewport is handed
|
|
9
|
+
* those lines and the selected item's range.
|
|
10
|
+
* [LAW:single-enforcer] Where the window sits is the viewport's call; this
|
|
11
|
+
* only says which lines the selection is.
|
|
12
|
+
*
|
|
13
|
+
* The viewport is handed in rather than made here: the scroll position lives
|
|
14
|
+
* in it, and a frame rebuilt on every key would otherwise start each one at
|
|
15
|
+
* the top. The pieces stay apart — a cursor, a viewport, a list drawn through
|
|
16
|
+
* it — so a consumer can take one without the others.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
20
|
+
import type { Viewport } from "./viewport.js";
|
|
21
|
+
export interface ListViewOptions {
|
|
22
|
+
/** Blank lines between one item and the next. Absent, none. */
|
|
23
|
+
gap?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Lines kept in view above and below the selected item, where the list has
|
|
26
|
+
* them: the neighbour a cursor is about to move onto. Absent, none.
|
|
27
|
+
*/
|
|
28
|
+
context?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class ListView implements Renderable, Measurable {
|
|
31
|
+
readonly viewport: Viewport;
|
|
32
|
+
readonly items: readonly Renderable[];
|
|
33
|
+
readonly selected: number;
|
|
34
|
+
readonly gap: number;
|
|
35
|
+
readonly context: number;
|
|
36
|
+
constructor(viewport: Viewport, items: readonly Renderable[], selected: number, options?: ListViewOptions);
|
|
37
|
+
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
38
|
+
measure(rawOptions: RenderOptions): {
|
|
39
|
+
minimum: number;
|
|
40
|
+
maximum: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListView — items stacked down a `Viewport`, scrolled to keep the selected
|
|
3
|
+
* one in view. `moveSelection` (core/selection) is the cursor that picks it.
|
|
4
|
+
*
|
|
5
|
+
* An item can wrap, so the lines the selected one covers exist only at the
|
|
6
|
+
* width it is drawn at, which a render is handed and whoever builds the frame
|
|
7
|
+
* is not. So the items are drawn here, at the viewport's content width and
|
|
8
|
+
* with no height — the lines the viewport scrolls — and the viewport is handed
|
|
9
|
+
* those lines and the selected item's range.
|
|
10
|
+
* [LAW:single-enforcer] Where the window sits is the viewport's call; this
|
|
11
|
+
* only says which lines the selection is.
|
|
12
|
+
*
|
|
13
|
+
* The viewport is handed in rather than made here: the scroll position lives
|
|
14
|
+
* in it, and a frame rebuilt on every key would otherwise start each one at
|
|
15
|
+
* the top. The pieces stay apart — a cursor, a viewport, a list drawn through
|
|
16
|
+
* it — so a consumer can take one without the others.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import { Measurement, measureRenderables } from "../core/measure.js";
|
|
20
|
+
import { withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
21
|
+
function lineCount(name, value) {
|
|
22
|
+
if (!Number.isInteger(value) || value < 0)
|
|
23
|
+
throw new RangeError(`ListView: ${name} ${value} is not a whole number of lines`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export class ListView {
|
|
27
|
+
viewport;
|
|
28
|
+
items;
|
|
29
|
+
selected;
|
|
30
|
+
gap;
|
|
31
|
+
context;
|
|
32
|
+
constructor(viewport, items, selected, options = {}) {
|
|
33
|
+
this.viewport = viewport;
|
|
34
|
+
this.items = items;
|
|
35
|
+
this.selected = selected;
|
|
36
|
+
this.gap = lineCount("gap", options.gap ?? 0);
|
|
37
|
+
this.context = lineCount("context", options.context ?? 0);
|
|
38
|
+
// An empty list's selection is 0: there is nothing for it to name.
|
|
39
|
+
if (!Number.isInteger(selected) || selected < 0 || selected >= Math.max(1, items.length)) {
|
|
40
|
+
throw new RangeError(`ListView: selected ${selected} is not an index of ${items.length} items`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
*render(rawOptions) {
|
|
44
|
+
const bounded = withBoundedWidth(rawOptions, this);
|
|
45
|
+
const { height: _height, ...rest } = bounded;
|
|
46
|
+
const options = { ...rest, maxWidth: this.viewport.contentWidth(rest.maxWidth) };
|
|
47
|
+
const gap = Array.from({ length: this.gap }, () => []);
|
|
48
|
+
const drawn = this.items.map((item) => Segment.splitLines(item.render(options)));
|
|
49
|
+
// The gap sits between items, so every item before the selected one
|
|
50
|
+
// brings its lines and one gap.
|
|
51
|
+
const start = drawn.slice(0, this.selected).reduce((total, item) => total + item.length + this.gap, 0);
|
|
52
|
+
const end = start + (drawn[this.selected]?.length ?? 0);
|
|
53
|
+
const lines = drawn.flatMap((item, i) => (i === 0 ? item : [...gap, ...item]));
|
|
54
|
+
this.viewport.content = {
|
|
55
|
+
*render() {
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
yield* line;
|
|
58
|
+
yield Segment.line();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
// A range reaching past either end is clamped by the viewport, which holds
|
|
63
|
+
// its offset to the lines that exist. Followed, not ensured: the wheel can
|
|
64
|
+
// scroll away from the selection until the selection moves.
|
|
65
|
+
this.viewport.follow(start - this.context, end + this.context);
|
|
66
|
+
yield* this.viewport.render(bounded);
|
|
67
|
+
}
|
|
68
|
+
measure(rawOptions) {
|
|
69
|
+
// Stacked, the items are as wide as the widest of them, plus whatever
|
|
70
|
+
// gutter the viewport's scrollbar takes. `withCellWidth` and not
|
|
71
|
+
// `withBoundedWidth`: the bounded parse asks this very method.
|
|
72
|
+
const options = withCellWidth(rawOptions);
|
|
73
|
+
const contentWidth = this.viewport.contentWidth(options.maxWidth);
|
|
74
|
+
const gutter = this.viewport.gutter;
|
|
75
|
+
const items = measureRenderables({ ...options, maxWidth: contentWidth }, this.items);
|
|
76
|
+
return new Measurement(items.minimum + gutter, items.maximum + gutter).withMaximum(options.maxWidth);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,40 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live — animates a portion of the terminal by continuously re-rendering.
|
|
3
3
|
*
|
|
4
|
-
* Two modes
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Two modes, each painting every frame over the last in place
|
|
5
|
+
* (`core/paint`):
|
|
6
|
+
* - **Inline** (default): the frame starts at the cursor's line and keeps its
|
|
7
|
+
* own height. Good for spinners/progress bars below other output.
|
|
7
8
|
* - **Alt-screen** (`altScreen: true`): enters the alternate screen buffer
|
|
8
|
-
* on start
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* on start and restores the original buffer on stop. The screen is the
|
|
10
|
+
* frame's region, so a `Layout` fills it. Good for full-screen TUI apps.
|
|
11
|
+
*
|
|
12
|
+
* On a console that is not interactive — a file, a pipe, a CI log — neither
|
|
13
|
+
* mode paints: what is printed goes out plain, and the frame is written once,
|
|
14
|
+
* when the display stops, as Rich's `Live` does.
|
|
15
|
+
*
|
|
16
|
+
* Live is not built on `App`, though both take a terminal, paint a frame over
|
|
17
|
+
* the last and hand the terminal back. That much is the `Painter`'s, and both
|
|
18
|
+
* hold one. The rest differs in who owns what. An `App` owns its terminal: it
|
|
19
|
+
* starts and stops a `TerminalHost`, sets raw mode, reads keys and pointers,
|
|
20
|
+
* suspends, and builds its own `Console` from the host. A `Live` owns none of
|
|
21
|
+
* that. It draws into a `Console` its caller owns and goes on writing
|
|
22
|
+
* through — a sink that may be a file, whose every other write has to step
|
|
23
|
+
* around the frame (`Console.claimLiveRegion`) — and it reads no input. An
|
|
24
|
+
* `App` over that console would need a host that cannot start, stop, read or
|
|
25
|
+
* suspend, and a second owner of the console's writes.
|
|
11
26
|
*/
|
|
27
|
+
import { type Clock } from "../core/clock.js";
|
|
12
28
|
import { Console } from "../core/console.js";
|
|
13
29
|
import { type Renderable } from "../core/protocol.js";
|
|
14
30
|
export interface LiveOptions {
|
|
31
|
+
/** Frames a second while auto-refreshing: positive and finite, fractional included. */
|
|
15
32
|
refreshPerSecond?: number;
|
|
33
|
+
/** What the auto-refresh ticks on. The platform's, unless given. */
|
|
34
|
+
clock?: Clock;
|
|
16
35
|
autoRefresh?: boolean;
|
|
17
36
|
transient?: boolean;
|
|
18
37
|
console?: Console;
|
|
19
38
|
verticalOverflow?: "crop" | "ellipsis" | "visible";
|
|
20
39
|
/** When true, Live enters the alternate screen buffer on start() and
|
|
21
|
-
* restores the original on stop().
|
|
22
|
-
*
|
|
40
|
+
* restores the original on stop(). The screen is the frame's region, so
|
|
41
|
+
* a full-screen layout fills it. */
|
|
23
42
|
altScreen?: boolean;
|
|
24
43
|
}
|
|
25
44
|
export declare class Live {
|
|
26
45
|
private _renderable;
|
|
27
46
|
private _console;
|
|
28
|
-
private
|
|
47
|
+
private readonly _rate;
|
|
48
|
+
private readonly _clock;
|
|
29
49
|
private _autoRefresh;
|
|
30
50
|
private _transient;
|
|
31
51
|
private _verticalOverflow;
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
private _started;
|
|
36
|
-
private _firstRefresh;
|
|
52
|
+
private readonly _painter;
|
|
53
|
+
private _stopTicking;
|
|
54
|
+
private _held;
|
|
37
55
|
constructor(renderable?: Renderable, options?: LiveOptions);
|
|
56
|
+
/**
|
|
57
|
+
* The console to print through while this Live runs: what it prints lands
|
|
58
|
+
* above an inline frame. The alternate screen is all frame, and paints over it.
|
|
59
|
+
*/
|
|
38
60
|
get console(): Console;
|
|
39
61
|
get renderable(): Renderable | undefined;
|
|
40
62
|
start(): void;
|
|
@@ -43,8 +65,10 @@ export declare class Live {
|
|
|
43
65
|
refresh?: boolean;
|
|
44
66
|
}): void;
|
|
45
67
|
refresh(): void;
|
|
68
|
+
private _frame;
|
|
69
|
+
private _stepAround;
|
|
70
|
+
private _paint;
|
|
71
|
+
private _screen;
|
|
46
72
|
private _overflow;
|
|
47
|
-
private _erase;
|
|
48
73
|
private _release;
|
|
49
|
-
private _writeCursorControl;
|
|
50
74
|
}
|