@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
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Columns — arranges renderables in a multi-column layout.
|
|
3
|
+
*
|
|
4
|
+
* A port of Rich's `Columns`. What it decides is how many columns, and which
|
|
5
|
+
* item goes in which cell. Everything after that — how wide each column is, how
|
|
6
|
+
* far apart, what `expand` does, how rows are separated — is the grid's: the
|
|
7
|
+
* items are laid out as a `Table.grid`, as the reference lays them out.
|
|
8
|
+
* [LAW:one-source-of-truth] A second copy of that geometry here is how the two
|
|
9
|
+
* drifted: an empty item's column was drawn with no width where the grid draws one cell.
|
|
3
10
|
*/
|
|
4
|
-
import { Segment } from "../core/segment.js";
|
|
5
11
|
import { Measurement } from "../core/measure.js";
|
|
6
|
-
import {
|
|
12
|
+
import { normalizePadding } from "./padding.js";
|
|
13
|
+
import { withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
7
14
|
import { cellCount } from "../core/cells.js";
|
|
8
|
-
import { embed } from "./embed.js";
|
|
15
|
+
import { EmbeddedText, embed } from "./embed.js";
|
|
16
|
+
import { Constrain } from "./constrain.js";
|
|
17
|
+
import { Table } from "./table.js";
|
|
18
|
+
/**
|
|
19
|
+
* The item in slot `slot` of the grid for `count` columns, counting row by row;
|
|
20
|
+
* `undefined` is a slot no item fills. Constant time, so a search that stops at
|
|
21
|
+
* the first slot that overflows pays for no slot after it.
|
|
22
|
+
*/
|
|
23
|
+
function slotItem(slot, items, count, columnFirst) {
|
|
24
|
+
const row = Math.floor(slot / count);
|
|
25
|
+
const col = slot % count;
|
|
26
|
+
// Column-first fills each column top to bottom, and the columns that take one
|
|
27
|
+
// item more than the rest are the leading ones — Rich's `column_lengths`.
|
|
28
|
+
const base = Math.floor(items / count);
|
|
29
|
+
const extra = items % count;
|
|
30
|
+
const length = base + (col < extra ? 1 : 0);
|
|
31
|
+
const index = !columnFirst ? slot : row < length ? col * base + Math.min(col, extra) + row : items;
|
|
32
|
+
return index < items ? index : undefined;
|
|
33
|
+
}
|
|
34
|
+
/** How many slots the grid for `count` columns has: every row full, the last one padded. */
|
|
35
|
+
function slotCount(items, count) {
|
|
36
|
+
return Math.ceil(items / count) * count;
|
|
37
|
+
}
|
|
9
38
|
export class Columns {
|
|
10
39
|
renderables;
|
|
11
40
|
expand;
|
|
12
41
|
equal;
|
|
13
42
|
colWidth;
|
|
14
43
|
columnFirst;
|
|
15
|
-
|
|
44
|
+
padding;
|
|
16
45
|
constructor(items, options) {
|
|
17
46
|
this.renderables = items ? [...items].map(embed) : [];
|
|
18
47
|
this.expand = options?.expand ?? false;
|
|
@@ -26,127 +55,137 @@ export class Columns {
|
|
|
26
55
|
// read it as a declared zero.
|
|
27
56
|
this.colWidth = options?.width === undefined ? undefined : cellCount(options.width);
|
|
28
57
|
this.columnFirst = options?.columnFirst ?? false;
|
|
29
|
-
|
|
58
|
+
// [LAW:parse-dont-validate] Rich's default is `(0, 1)`: one cell between
|
|
59
|
+
// columns and none between rows.
|
|
60
|
+
this.padding = normalizePadding(options?.padding ?? [0, 1]);
|
|
30
61
|
}
|
|
31
62
|
*render(rawOptions) {
|
|
32
|
-
|
|
33
|
-
if (items.length === 0)
|
|
63
|
+
if (this.renderables.length === 0)
|
|
34
64
|
return;
|
|
35
|
-
// The parsed options, not just a parsed local: `
|
|
65
|
+
// The parsed options, not just a parsed local: `_layout` hands them to
|
|
36
66
|
// `Measurement.get`, and a raw NaN reaching it comes back as a NaN item
|
|
37
|
-
// width, then a NaN column count, then `Invalid array length`
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// parse here is the bounded one.
|
|
67
|
+
// width, then a NaN column count, then `Invalid array length` before a
|
|
68
|
+
// single column is laid out. An unbounded width reaches the same place by
|
|
69
|
+
// the same route, which is why the parse here is the bounded one.
|
|
41
70
|
const options = withBoundedWidth(rawOptions, this);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
yield* Segment.mergeHorizontal(cells, this.gutterWidth);
|
|
62
|
-
}
|
|
71
|
+
yield* this._grid(options).render(options);
|
|
72
|
+
}
|
|
73
|
+
measure(rawOptions) {
|
|
74
|
+
// The maximum is the grid's, which reports its width before `expand`: a
|
|
75
|
+
// stretch is what a Columns does with a width it is given, not a width it
|
|
76
|
+
// asks for. Reported as the offer instead, `Panel` in fit mode drew a
|
|
77
|
+
// 40-cell frame around five cells of content and an unbounded offer came
|
|
78
|
+
// back unbounded.
|
|
79
|
+
//
|
|
80
|
+
// A floor of one cell is what Columns asks for, since it can always fall
|
|
81
|
+
// back to one column, and the ceiling wins: a bare `minimum: 1` measured
|
|
82
|
+
// into no width at all reported the range 1..0 — a floor above its own
|
|
83
|
+
// ceiling, which the parent that asked cannot divide.
|
|
84
|
+
const parsed = withCellWidth(rawOptions);
|
|
85
|
+
if (this.renderables.length === 0)
|
|
86
|
+
return { minimum: 0, maximum: 0 };
|
|
87
|
+
const { maximum } = this._grid(parsed).measure(parsed);
|
|
88
|
+
return { minimum: Math.min(1, maximum), maximum };
|
|
63
89
|
}
|
|
64
90
|
/**
|
|
65
|
-
* The
|
|
91
|
+
* The items at the width offered, as the reference's grid: `_layout`'s
|
|
92
|
+
* cells in a `Table.grid`.
|
|
93
|
+
*
|
|
94
|
+
* [LAW:single-enforcer] `render` draws this and `measure` reports its width,
|
|
95
|
+
* so the two cannot disagree about how wide a Columns is.
|
|
66
96
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
97
|
+
* A declared `width` is each column's natural width rather than a Table
|
|
98
|
+
* column `width`, which this port never stretches: the reference's grid
|
|
99
|
+
* stretches a Columns' declared width under `expand` like any other.
|
|
100
|
+
* `equal` holds every item to the widest, Rich's `Constrain`; without it
|
|
101
|
+
* the cap is `undefined` and holds nothing.
|
|
71
102
|
*/
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
103
|
+
_grid(options) {
|
|
104
|
+
const { columns, rows, widest } = this._layout(options);
|
|
105
|
+
const grid = Table.grid({ padding: this.padding, expand: this.expand });
|
|
75
106
|
const declared = this._declaredWidth(options);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return { numCols, colWidths: new Array(numCols).fill(declared) };
|
|
107
|
+
for (let col = 0; col < columns; col++) {
|
|
108
|
+
grid.addColumn(undefined, { minWidth: declared, maxWidth: declared });
|
|
79
109
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
const cap = this.equal ? widest : undefined;
|
|
111
|
+
// A string item is read here, under the options Columns is drawn with,
|
|
112
|
+
// before the grid gets it: Rich's `Columns` reads its strings through
|
|
113
|
+
// `render_str`, so they are highlighted where a table cell's are not.
|
|
114
|
+
const items = this.renderables.map((item) => (item instanceof EmbeddedText ? item.text(options) : item));
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
grid.addRow(...row.map((index) => (index === undefined ? "" : new Constrain(items[index], cap))));
|
|
85
117
|
}
|
|
86
|
-
|
|
87
|
-
// count. Sized from the content rather than from a flat four cells per
|
|
88
|
-
// column, because `_naturalWidth` below is the inverse of *this* expression
|
|
89
|
-
// and a constant has no inverse: six two-cell items reported a natural
|
|
90
|
-
// width of 22 and then wrapped into two rows at 22, since `floor(22 / 4)`
|
|
91
|
-
// is five columns no matter how narrow the items are.
|
|
92
|
-
const itemWidth = this._itemWidth(options);
|
|
93
|
-
const numCols = Math.min(this.renderables.length, Math.max(1, Math.floor((maxWidth + gutter) / (itemWidth + gutter))));
|
|
94
|
-
const colW = Math.floor((maxWidth - gutter * (numCols - 1)) / numCols);
|
|
95
|
-
return { numCols, colWidths: new Array(numCols).fill(colW) };
|
|
118
|
+
return grid;
|
|
96
119
|
}
|
|
97
120
|
/**
|
|
98
|
-
*
|
|
121
|
+
* How many columns fit the width offered, and which item goes in each cell,
|
|
122
|
+
* decided the way Rich's `Columns` decides them.
|
|
99
123
|
*
|
|
100
|
-
*
|
|
101
|
-
* `
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
124
|
+
* Every item carries a width. It is what the item measures, or the declared
|
|
125
|
+
* `width` for all of them. `equal` counts every item as the widest when
|
|
126
|
+
* choosing how many columns fit, and the grid still sizes each column by
|
|
127
|
+
* what is in it — which is why an `equal` layout is not a grid of equal
|
|
128
|
+
* columns, in the reference or here.
|
|
129
|
+
*
|
|
130
|
+
* The count starts at one column per item and drops whenever the columns
|
|
131
|
+
* filled so far, and the gaps between them, pass the width offered. Two
|
|
132
|
+
* columns stand the wider of the padding's left and right sides apart, which
|
|
133
|
+
* is what the collapsed grid leaves between them. One departure from the
|
|
134
|
+
* reference: a declared `width` is searched the same way rather than by
|
|
135
|
+
* `max_width // (width + gap)`, which laid out empty trailing columns and
|
|
136
|
+
* charged the last column a gap it does not have.
|
|
106
137
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
_layout(options) {
|
|
139
|
+
const count = this.renderables.length;
|
|
140
|
+
const maxWidth = options.maxWidth;
|
|
141
|
+
const [, right, , left] = this.padding;
|
|
142
|
+
const gap = Math.max(left, right);
|
|
143
|
+
const declared = this._declaredWidth(options);
|
|
144
|
+
// An item that cannot measure itself wants the whole offer. Counting it as one
|
|
145
|
+
// cell instead resolved an unbounded offer to a single column and cropped
|
|
146
|
+
// forty cells of content down to `"x"` with no error at all.
|
|
147
|
+
const sizes = this.renderables.map((item) => declared ?? Measurement.get(options, item).maximum);
|
|
148
|
+
// A fold, not `Math.max(...sizes)`: spreading one argument per item
|
|
149
|
+
// overflows the call stack at a few hundred thousand items.
|
|
150
|
+
const widest = sizes.reduce((w, size) => Math.max(w, size), 0);
|
|
151
|
+
const fits = this.equal ? sizes.map(() => widest) : sizes;
|
|
152
|
+
// One attempt either fits in the offer or names the smaller count to try
|
|
153
|
+
// next. `total` is the columns so far and the gaps between them, kept as
|
|
154
|
+
// the widths grow so that a slot costs the same however many columns came
|
|
155
|
+
// before it.
|
|
156
|
+
const attempt = (columns) => {
|
|
157
|
+
const widths = [];
|
|
158
|
+
let total = 0;
|
|
159
|
+
for (let slot = 0; slot < slotCount(count, columns); slot++) {
|
|
160
|
+
const col = slot % columns;
|
|
161
|
+
const index = slotItem(slot, count, columns, this.columnFirst);
|
|
162
|
+
const held = widths[col];
|
|
163
|
+
const width = Math.max(held ?? 0, index === undefined ? 0 : fits[index]);
|
|
164
|
+
total += width - (held ?? 0) + (held === undefined && col > 0 ? gap : 0);
|
|
165
|
+
widths[col] = width;
|
|
166
|
+
// Never zero: no item measures wider than the offer, so the first
|
|
167
|
+
// column alone always fits.
|
|
168
|
+
if (total > maxWidth)
|
|
169
|
+
return widths.length - 1;
|
|
170
|
+
}
|
|
171
|
+
return columns;
|
|
172
|
+
};
|
|
173
|
+
let columns = count;
|
|
174
|
+
for (let next = attempt(columns); next < columns; next = attempt(columns))
|
|
175
|
+
columns = next;
|
|
176
|
+
const rows = Array.from({ length: slotCount(count, columns) / columns }, (_, row) => Array.from({ length: columns }, (_, col) => slotItem(row * columns + col, count, columns, this.columnFirst)));
|
|
177
|
+
return { columns, rows, widest };
|
|
115
178
|
}
|
|
116
179
|
/**
|
|
117
180
|
* The declared column width, bounded by the width offered.
|
|
118
181
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* reported three, and the terminal's soft wrap took the frame of everything
|
|
125
|
-
* printed after it.
|
|
182
|
+
* A declared width is what a column asks for, not what it takes — the
|
|
183
|
+
* contract `Table._outerWidth` keeps for a declared table width. Laid out at
|
|
184
|
+
* the raw declared width, a six-cell column offered three emitted six-cell
|
|
185
|
+
* lines while `measure` reported three, and the terminal's soft wrap took the
|
|
186
|
+
* frame of everything printed after it.
|
|
126
187
|
*/
|
|
127
188
|
_declaredWidth(options) {
|
|
128
189
|
return this.colWidth === undefined ? undefined : Math.min(this.colWidth, options.maxWidth);
|
|
129
190
|
}
|
|
130
|
-
/** The width at which every item sits on one row: n columns and the gutters between them. */
|
|
131
|
-
_naturalWidth(options) {
|
|
132
|
-
const count = this.renderables.length;
|
|
133
|
-
if (count === 0)
|
|
134
|
-
return 0;
|
|
135
|
-
const width = this._declaredWidth(options) ?? this._itemWidth(options);
|
|
136
|
-
return count * width + this.gutterWidth * (count - 1);
|
|
137
|
-
}
|
|
138
|
-
measure(rawOptions) {
|
|
139
|
-
// A floor of one cell is what Columns asks for; the ceiling is what it was
|
|
140
|
-
// offered, and the ceiling wins. Stated as a bare `minimum: 1`, a Columns
|
|
141
|
-
// measured into no width at all reported the range 1..0 — a floor above
|
|
142
|
-
// its own ceiling, which the parent that asked cannot divide.
|
|
143
|
-
//
|
|
144
|
-
// The ceiling is not the whole answer, though: reported as the offer alone,
|
|
145
|
-
// a Columns claimed every cell it was shown, so `Panel` in fit mode drew a
|
|
146
|
-
// 40-cell frame around five cells of content and an unbounded offer came
|
|
147
|
-
// back unbounded.
|
|
148
|
-
const parsed = withCellWidth(rawOptions);
|
|
149
|
-
const maximum = Math.min(this._naturalWidth(parsed), parsed.maxWidth);
|
|
150
|
-
return { minimum: Math.min(1, maximum), maximum };
|
|
151
|
-
}
|
|
152
191
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Constrain — wraps a renderable and constrains its maximum width.
|
|
3
3
|
*/
|
|
4
4
|
import { Measurement } from "../core/measure.js";
|
|
5
|
-
import { isMeasurable } from "../core/protocol.js";
|
|
6
5
|
export class Constrain {
|
|
7
6
|
renderable;
|
|
8
7
|
width;
|
|
@@ -22,16 +21,11 @@ export class Constrain {
|
|
|
22
21
|
yield* this.renderable.render(innerOptions);
|
|
23
22
|
}
|
|
24
23
|
measure(options) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
minimum: measurement.minimum,
|
|
32
|
-
maximum: Math.min(measurement.maximum, maxWidth),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
return { minimum: 1, maximum: options.maxWidth };
|
|
24
|
+
// Measured at the width it will be drawn at, as Rich's `update_width` does;
|
|
25
|
+
// `Measurement.get` then holds the answer inside that offer.
|
|
26
|
+
const maxWidth = this.width !== undefined
|
|
27
|
+
? Math.min(this.width, options.maxWidth)
|
|
28
|
+
: options.maxWidth;
|
|
29
|
+
return Measurement.get({ ...options, maxWidth }, this.renderable);
|
|
36
30
|
}
|
|
37
31
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effected — a renderable whose finished cells' colours pass through an
|
|
3
|
+
* effect at a moment `t`.
|
|
4
|
+
*
|
|
5
|
+
* An effect is one pure function: a cell's colours, where the cell is, and a
|
|
6
|
+
* time in, the cell's new colours out. Shimmer, pulse, fade and dissolve are
|
|
7
|
+
* all this one shape, differing by what the function does with its inputs
|
|
8
|
+
* [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
|
|
9
|
+
* another, which is the same as calling one after the other.
|
|
10
|
+
*
|
|
11
|
+
* [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
|
|
12
|
+
* clock read. Whoever draws frames builds the view for each frame's `t`, so
|
|
13
|
+
* the same `t` always draws the same bytes, at 30 frames a second or one
|
|
14
|
+
* every two seconds.
|
|
15
|
+
*
|
|
16
|
+
* What an effect may not do is the contract this module holds:
|
|
17
|
+
*
|
|
18
|
+
* - It changes how cells look, never which cells exist or who drew them. The
|
|
19
|
+
* child's text is walked by grapheme cluster, so a wide glyph is one cell
|
|
20
|
+
* to the effect and is never cut, and a run cut out of a segment keeps that
|
|
21
|
+
* segment's link and meta and carries its anchor shifted to the run's first
|
|
22
|
+
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
23
|
+
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
24
|
+
* and a segment whose every cell is unchanged is yielded as it was, so the
|
|
25
|
+
* identity effect is byte-identical. Neighbouring cells the output depth
|
|
26
|
+
* writes alike stay one segment: a segment is cut only where the written
|
|
27
|
+
* colours change, so two colours that land on one index are one run.
|
|
28
|
+
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
29
|
+
* string, so no frame adds an entry to a parse cache.
|
|
30
|
+
* - With no colour output (`colorSystem: null`) there is nothing for an
|
|
31
|
+
* effect to change, and the child's segments pass through untouched.
|
|
32
|
+
*
|
|
33
|
+
* Positions are the child's own: row and column count from the top-left of
|
|
34
|
+
* what the child drew, so a sweep crosses the element, not the screen.
|
|
35
|
+
*/
|
|
36
|
+
import { type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
37
|
+
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
38
|
+
import { Segment } from "../core/segment.js";
|
|
39
|
+
/**
|
|
40
|
+
* A cell's foreground and background as they appear on screen — the glyph's
|
|
41
|
+
* colour and the colour behind it — which are the colours an effect moves.
|
|
42
|
+
* Under `reverse` they are the style's two colours swapped.
|
|
43
|
+
*/
|
|
44
|
+
export interface CellColors {
|
|
45
|
+
readonly fg: ColorRgba;
|
|
46
|
+
readonly bg: ColorRgba;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Where a cell is in the output of the renderable an effect wraps, its seed:
|
|
50
|
+
* a number in [0, 1) that is the same for that cell on every frame, for an
|
|
51
|
+
* effect that varies cell by cell (a per-cell offset in time, a sparkle) —
|
|
52
|
+
* and which of its on-screen colours are the terminal's.
|
|
53
|
+
*/
|
|
54
|
+
export interface EffectCell {
|
|
55
|
+
readonly row: number;
|
|
56
|
+
readonly col: number;
|
|
57
|
+
readonly seed: number;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the glyph's colour and the colour behind it, as the screen shows
|
|
60
|
+
* them, are the terminal's own: a slot the element's style leaves unset or
|
|
61
|
+
* sets to `default`. Such a colour is resolved through the theme, so its
|
|
62
|
+
* hex can equal one the element wrote; this is what tells them apart.
|
|
63
|
+
*/
|
|
64
|
+
readonly terminal: {
|
|
65
|
+
readonly fg: boolean;
|
|
66
|
+
readonly bg: boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A cell's colours at time `t`, in seconds. Return `colors` to leave it be. */
|
|
70
|
+
export type Effect = (colors: CellColors, cell: EffectCell, t: number) => CellColors;
|
|
71
|
+
export interface EffectedOptions {
|
|
72
|
+
/** The moment to draw, in seconds. */
|
|
73
|
+
readonly t: number;
|
|
74
|
+
/**
|
|
75
|
+
* Names the element for its seeds, so two elements showing the same text do
|
|
76
|
+
* not twinkle in lockstep. The same key gives the same seeds on every frame.
|
|
77
|
+
*/
|
|
78
|
+
readonly key: string;
|
|
79
|
+
/**
|
|
80
|
+
* The terminal the frame is drawn on: the colours a cell that sets none is
|
|
81
|
+
* drawn in, and the sixteen an ANSI colour names. The terminal's own palette
|
|
82
|
+
* is never queried, so the caller says what it is.
|
|
83
|
+
*/
|
|
84
|
+
readonly theme: TerminalTheme;
|
|
85
|
+
}
|
|
86
|
+
export declare class Effected implements Renderable, Measurable {
|
|
87
|
+
readonly renderable: Renderable;
|
|
88
|
+
readonly effect: Effect;
|
|
89
|
+
readonly t: number;
|
|
90
|
+
readonly key: string;
|
|
91
|
+
readonly theme: TerminalTheme;
|
|
92
|
+
constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
|
|
93
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
94
|
+
measure(options: RenderOptions): {
|
|
95
|
+
minimum: number;
|
|
96
|
+
maximum: number;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
100
|
+
* `from` with `wire`: a slot the effect left alone keeps its written spec,
|
|
101
|
+
* and a moved one is laid on what is beneath it — the ground on
|
|
102
|
+
* `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
|
|
103
|
+
* translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
|
|
104
|
+
* that the terminal shows (`ColorSpec.matchOn`). A move the depth cannot
|
|
105
|
+
* show is no move: where the written spec is as near the moved colour as
|
|
106
|
+
* that match, the slot keeps it, so below truecolor a colour moved a little
|
|
107
|
+
* off any colour it can write — a theme slot, the default colour, the cube —
|
|
108
|
+
* is still drawn as that one.
|
|
109
|
+
*/
|
|
110
|
+
private respec;
|
|
111
|
+
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
112
|
+
private seed;
|
|
113
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effected — a renderable whose finished cells' colours pass through an
|
|
3
|
+
* effect at a moment `t`.
|
|
4
|
+
*
|
|
5
|
+
* An effect is one pure function: a cell's colours, where the cell is, and a
|
|
6
|
+
* time in, the cell's new colours out. Shimmer, pulse, fade and dissolve are
|
|
7
|
+
* all this one shape, differing by what the function does with its inputs
|
|
8
|
+
* [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
|
|
9
|
+
* another, which is the same as calling one after the other.
|
|
10
|
+
*
|
|
11
|
+
* [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
|
|
12
|
+
* clock read. Whoever draws frames builds the view for each frame's `t`, so
|
|
13
|
+
* the same `t` always draws the same bytes, at 30 frames a second or one
|
|
14
|
+
* every two seconds.
|
|
15
|
+
*
|
|
16
|
+
* What an effect may not do is the contract this module holds:
|
|
17
|
+
*
|
|
18
|
+
* - It changes how cells look, never which cells exist or who drew them. The
|
|
19
|
+
* child's text is walked by grapheme cluster, so a wide glyph is one cell
|
|
20
|
+
* to the effect and is never cut, and a run cut out of a segment keeps that
|
|
21
|
+
* segment's link and meta and carries its anchor shifted to the run's first
|
|
22
|
+
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
23
|
+
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
24
|
+
* and a segment whose every cell is unchanged is yielded as it was, so the
|
|
25
|
+
* identity effect is byte-identical. Neighbouring cells the output depth
|
|
26
|
+
* writes alike stay one segment: a segment is cut only where the written
|
|
27
|
+
* colours change, so two colours that land on one index are one run.
|
|
28
|
+
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
29
|
+
* string, so no frame adds an entry to a parse cache.
|
|
30
|
+
* - With no colour output (`colorSystem: null`) there is nothing for an
|
|
31
|
+
* effect to change, and the child's segments pass through untouched.
|
|
32
|
+
*
|
|
33
|
+
* Positions are the child's own: row and column count from the top-left of
|
|
34
|
+
* what the child drew, so a sweep crosses the element, not the screen.
|
|
35
|
+
*/
|
|
36
|
+
import { cellLen, graphemes } from "../core/cells.js";
|
|
37
|
+
import { ColorDepth, ColorSpec, SURFACE_BLACK, rgbDistance } from "../core/color.js";
|
|
38
|
+
import { fnv1a } from "../core/fnv1a.js";
|
|
39
|
+
import { Measurement } from "../core/measure.js";
|
|
40
|
+
import { Segment } from "../core/segment.js";
|
|
41
|
+
import { Style } from "../core/style.js";
|
|
42
|
+
export class Effected {
|
|
43
|
+
renderable;
|
|
44
|
+
effect;
|
|
45
|
+
t;
|
|
46
|
+
key;
|
|
47
|
+
theme;
|
|
48
|
+
constructor(renderable, effect, options) {
|
|
49
|
+
this.renderable = renderable;
|
|
50
|
+
this.effect = effect;
|
|
51
|
+
if (!Number.isFinite(options.t)) {
|
|
52
|
+
throw new RangeError(`Effected needs a finite t, got ${options.t}`);
|
|
53
|
+
}
|
|
54
|
+
this.t = options.t;
|
|
55
|
+
this.key = options.key;
|
|
56
|
+
this.theme = options.theme;
|
|
57
|
+
}
|
|
58
|
+
*render(options) {
|
|
59
|
+
const segments = this.renderable.render(options);
|
|
60
|
+
if (options.colorSystem === null) {
|
|
61
|
+
yield* segments;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// No depth named is no downgrade, as `drawnColors` reads it.
|
|
65
|
+
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
66
|
+
let row = 0;
|
|
67
|
+
let col = 0;
|
|
68
|
+
for (const segment of segments) {
|
|
69
|
+
if (segment.isControl) {
|
|
70
|
+
yield segment;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const base = segment.style ?? Style.null();
|
|
74
|
+
// [LAW:single-enforcer] `drawnColors` is what the wire writes at this
|
|
75
|
+
// depth — alpha flattened, downgraded — and the theme resolves what it
|
|
76
|
+
// leaves to the terminal. A slot the effect leaves alone keeps the
|
|
77
|
+
// written spec, so it stays what the screen showed whatever moves
|
|
78
|
+
// beside it.
|
|
79
|
+
const wire = base.drawnColors(options.colorSystem ?? undefined);
|
|
80
|
+
const ink = (wire.color ?? DEFAULT).getTruecolor(this.theme, true);
|
|
81
|
+
const paper = (wire.bgcolor ?? DEFAULT).getTruecolor(this.theme, false);
|
|
82
|
+
const slots = { fg: ink, bg: paper };
|
|
83
|
+
const from = onScreen(slots, base);
|
|
84
|
+
const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, base);
|
|
85
|
+
// [LAW:one-source-of-truth] A run is cells the wire writes alike: each
|
|
86
|
+
// cell is drawn at the output depth first and merged on the colours that
|
|
87
|
+
// writes, so two truecolors landing on one index stay one run. A cell
|
|
88
|
+
// the effect gave its neighbour's colours is drawn as that neighbour was.
|
|
89
|
+
const runs = [];
|
|
90
|
+
for (const glyph of graphemes(segment.text)) {
|
|
91
|
+
let colors = slots;
|
|
92
|
+
if (glyph === "\n") {
|
|
93
|
+
row++;
|
|
94
|
+
col = 0;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
98
|
+
colors = onScreen(this.effect(from, { row, col, seed: this.seed(row, col), terminal }, this.t), base);
|
|
99
|
+
col += cellLen(glyph);
|
|
100
|
+
}
|
|
101
|
+
const last = runs[runs.length - 1];
|
|
102
|
+
const drawn = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
103
|
+
if (last !== undefined && writesAlike(last.drawn, drawn)) {
|
|
104
|
+
last.text += glyph;
|
|
105
|
+
last.cells += cellLen(glyph);
|
|
106
|
+
last.colors = colors;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (runs.every((run) => writesAlike(run.drawn, wire))) {
|
|
113
|
+
yield segment;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let offset = 0;
|
|
117
|
+
for (const run of runs) {
|
|
118
|
+
const shifted = base.shiftedBy(offset);
|
|
119
|
+
const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
|
|
120
|
+
yield new Segment(run.text, style);
|
|
121
|
+
offset += run.cells;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
measure(options) {
|
|
126
|
+
return Measurement.get(options, this.renderable);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
130
|
+
* `from` with `wire`: a slot the effect left alone keeps its written spec,
|
|
131
|
+
* and a moved one is laid on what is beneath it — the ground on
|
|
132
|
+
* `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
|
|
133
|
+
* translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
|
|
134
|
+
* that the terminal shows (`ColorSpec.matchOn`). A move the depth cannot
|
|
135
|
+
* show is no move: where the written spec is as near the moved colour as
|
|
136
|
+
* that match, the slot keeps it, so below truecolor a colour moved a little
|
|
137
|
+
* off any colour it can write — a theme slot, the default colour, the cube —
|
|
138
|
+
* is still drawn as that one.
|
|
139
|
+
*/
|
|
140
|
+
respec(wire, from, to, depth) {
|
|
141
|
+
// [LAW:one-source-of-truth] The writer's surface, so one translucent colour draws one way in a frame.
|
|
142
|
+
const ground = to.bg.compositeOver(SURFACE_BLACK);
|
|
143
|
+
const glyph = to.fg.compositeOver(ground);
|
|
144
|
+
const drawn = (spec, was, now, foreground) => {
|
|
145
|
+
const match = ColorSpec.matchOn(now, depth, this.theme, foreground);
|
|
146
|
+
return rgbDistance(was, now) <= rgbDistance(match.getTruecolor(this.theme, foreground), now) ? spec : match;
|
|
147
|
+
};
|
|
148
|
+
return { color: drawn(wire.color, from.fg, glyph, true), bgcolor: drawn(wire.bgcolor, from.bg, ground, false) };
|
|
149
|
+
}
|
|
150
|
+
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
151
|
+
seed(row, col) {
|
|
152
|
+
return Number.parseInt(fnv1a(`${this.key}:${row}:${col}`), 16) / 0x1_0000_0000;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const DEFAULT = ColorSpec.default();
|
|
156
|
+
/**
|
|
157
|
+
* A style's colour slots as the screen shows them, or the reverse: `reverse`
|
|
158
|
+
* swaps glyph and ground, and swapping twice is where you started.
|
|
159
|
+
*/
|
|
160
|
+
function onScreen(slots, style) {
|
|
161
|
+
return style.reverse === true ? { fg: slots.bg, bg: slots.fg } : slots;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Whether two cells' colours are written alike. A slot with no colour is not
|
|
165
|
+
* the default colour: it writes nothing, so a style a container lays beneath
|
|
166
|
+
* (`Segment.applyStyle`) still shows through it, where `39`/`49` would not.
|
|
167
|
+
*/
|
|
168
|
+
function writesAlike(a, b) {
|
|
169
|
+
return slotWritesAlike(a.color, b.color, true) && slotWritesAlike(a.bgcolor, b.bgcolor, false);
|
|
170
|
+
}
|
|
171
|
+
function slotWritesAlike(a, b, foreground) {
|
|
172
|
+
return a === b || written(a, foreground) === written(b, foreground);
|
|
173
|
+
}
|
|
174
|
+
function written(spec, foreground) {
|
|
175
|
+
return spec?.getAnsiCodes(foreground).join(";") ?? "";
|
|
176
|
+
}
|
|
177
|
+
function sameColors(a, b) {
|
|
178
|
+
return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
|
|
179
|
+
}
|
|
180
|
+
function sameColor(a, b) {
|
|
181
|
+
return a.red === b.red && a.green === b.green && a.blue === b.blue && a.alpha === b.alpha;
|
|
182
|
+
}
|