@promptctl/rich-js 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -26
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +0 -1
- package/dist/core/box.js +6 -18
- package/dist/core/cells.d.ts +16 -5
- package/dist/core/cells.js +31 -10
- package/dist/core/color.d.ts +4 -8
- package/dist/core/color.js +28 -19
- package/dist/core/console.d.ts +13 -4
- package/dist/core/console.js +91 -36
- package/dist/core/export-html.d.ts +40 -5
- package/dist/core/export-html.js +111 -17
- package/dist/core/markup.d.ts +27 -5
- package/dist/core/markup.js +282 -170
- package/dist/core/measure.d.ts +1 -1
- package/dist/core/measure.js +2 -1
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +3 -0
- package/dist/core/pretty.js +5 -1
- package/dist/core/protocol.d.ts +136 -6
- package/dist/core/protocol.js +64 -2
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +2 -2
- package/dist/core/segment.d.ts +23 -4
- package/dist/core/segment.js +75 -16
- package/dist/core/spinnerData.d.ts +2 -0
- package/dist/core/spinnerData.js +3 -1
- package/dist/core/strip.d.ts +3 -7
- package/dist/core/strip.js +32 -35
- package/dist/core/style.d.ts +25 -0
- package/dist/core/style.js +61 -89
- package/dist/core/text.d.ts +16 -15
- package/dist/core/text.js +62 -76
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +4 -2
- package/dist/host/index.js +2 -1
- package/dist/host/terminal-host.d.ts +43 -7
- package/dist/host/terminal-host.js +57 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/node/terminal-host.d.ts +31 -2
- package/dist/node/terminal-host.js +69 -2
- package/dist/node/traceback.d.ts +16 -6
- package/dist/node/traceback.js +30 -8
- package/dist/renderables/align.d.ts +2 -2
- package/dist/renderables/align.js +6 -19
- package/dist/renderables/columns.js +4 -20
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/group.js +3 -1
- package/dist/renderables/layout.js +31 -34
- package/dist/renderables/live.d.ts +6 -3
- package/dist/renderables/live.js +89 -45
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.js +6 -4
- package/dist/renderables/padding.js +3 -2
- package/dist/renderables/panel.js +26 -41
- package/dist/renderables/progress.js +5 -8
- package/dist/renderables/progressBar.js +6 -5
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +9 -9
- package/dist/renderables/spinner.d.ts +5 -3
- package/dist/renderables/spinner.js +14 -8
- package/dist/renderables/syntax.d.ts +1 -1
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +26 -14
- package/dist/renderables/table.js +239 -141
- package/dist/renderables/traceback.js +8 -9
- package/dist/renderables/tree.d.ts +17 -3
- package/dist/renderables/tree.js +62 -71
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.js +11 -13
- package/dist/template-bindings/index.js +4 -2
- package/dist/template-bindings/palette-funcs.js +6 -16
- package/dist/template-bindings/style-funcs.js +4 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.js +19 -0
- package/dist/themes/data/atom-one-light.js +19 -0
- package/dist/themes/data/catppuccin-frappe.js +19 -0
- package/dist/themes/data/catppuccin-latte.js +19 -0
- package/dist/themes/data/catppuccin-macchiato.js +19 -0
- package/dist/themes/data/catppuccin-mocha.js +19 -0
- package/dist/themes/data/cyberpunk.js +2 -0
- package/dist/themes/data/default.js +2 -0
- package/dist/themes/data/dracula.js +19 -0
- package/dist/themes/data/flexoki.js +19 -0
- package/dist/themes/data/gruvbox.js +19 -0
- package/dist/themes/data/monokai.js +19 -0
- package/dist/themes/data/nord.js +19 -0
- package/dist/themes/data/rose-pine-dawn.js +19 -0
- package/dist/themes/data/rose-pine-moon.js +19 -0
- package/dist/themes/data/rose-pine.js +19 -0
- package/dist/themes/data/solarized-dark.js +19 -0
- package/dist/themes/data/solarized-light.js +19 -0
- package/dist/themes/data/svg-export.js +19 -0
- package/dist/themes/data/textual-ansi.js +2 -0
- package/dist/themes/data/textual-dark.js +2 -0
- package/dist/themes/data/textual-light.js +2 -0
- package/dist/themes/data/tokyo-night.js +19 -0
- package/dist/themes/data/types.d.ts +16 -0
- package/dist/themes/data/types.js +10 -1
- package/dist/themes/registry.d.ts +3 -2
- package/dist/themes/registry.js +13 -1
- package/dist/themes/terminalThemes.d.ts +2 -7
- package/dist/themes/terminalThemes.js +4 -9
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +4 -2
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +5 -3
- package/dist/widgets/dropdown.d.ts +2 -2
- package/dist/widgets/dropdown.js +13 -16
- package/dist/widgets/event-router.d.ts +21 -28
- package/dist/widgets/event-router.js +80 -73
- package/dist/widgets/focus-manager.d.ts +25 -6
- package/dist/widgets/focus-manager.js +63 -65
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +5 -7
- package/dist/widgets/index.js +3 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/slider.js +7 -7
- package/dist/widgets/static-item.d.ts +9 -15
- package/dist/widgets/static-item.js +9 -15
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +17 -14
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +3 -2
- package/dist/widgets/types.d.ts +14 -35
- package/dist/widgets/types.js +0 -1
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +4 -6
- package/dist/widgets/widget-base.js +22 -30
- package/package.json +12 -4
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
|
@@ -23,16 +23,17 @@ function parseStack(error) {
|
|
|
23
23
|
const lines = stack.split("\n");
|
|
24
24
|
const frames = [];
|
|
25
25
|
for (const line of lines) {
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
26
|
+
// The two V8 frame shapes located by file:line:column: "at fn (file:…)"
|
|
27
|
+
// and "at file:…", either one prefixed "async " when the frame is an
|
|
28
|
+
// awaited call. The marker is not part of any name, so it is read and
|
|
29
|
+
// dropped; the lookahead keeps "at async (file:…)" — a function named
|
|
30
|
+
// `async` — a name.
|
|
31
|
+
const match = /^at\s+(?:async\s+(?!\())?(?:(.+?)\s+\()?(.+?):(\d+):\d+\)?/.exec(line.trim());
|
|
30
32
|
if (match) {
|
|
31
33
|
frames.push({
|
|
32
34
|
function: match[1] || undefined,
|
|
33
35
|
file: match[2],
|
|
34
36
|
line: parseInt(match[3], 10),
|
|
35
|
-
column: parseInt(match[4], 10),
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -100,10 +101,8 @@ export class Traceback {
|
|
|
100
101
|
yield new Segment(" ");
|
|
101
102
|
}
|
|
102
103
|
yield new Segment(frame.file, pathStyle);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
yield new Segment(String(frame.line), lineNoStyle);
|
|
106
|
-
}
|
|
104
|
+
yield new Segment(":");
|
|
105
|
+
yield new Segment(String(frame.line), lineNoStyle);
|
|
107
106
|
yield Segment.line();
|
|
108
107
|
}
|
|
109
108
|
}
|
|
@@ -21,6 +21,13 @@ export declare class Tree implements Renderable, Measurable {
|
|
|
21
21
|
constructor(label: string | RichText | Renderable, options?: TreeOptions);
|
|
22
22
|
add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
|
|
23
23
|
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
24
|
+
/**
|
|
25
|
+
* The rows `render` and `measure` both read, entered in exactly one place so
|
|
26
|
+
* the two cannot start the walk differently. A hidden root takes its own row
|
|
27
|
+
* and the column its children hang from with it, as Rich does — its children
|
|
28
|
+
* stand at the left edge, and its guide style still reaches their guides.
|
|
29
|
+
*/
|
|
30
|
+
private _walk;
|
|
24
31
|
/**
|
|
25
32
|
* The tree walked into the rows it emits, one row per label, guides first.
|
|
26
33
|
*
|
|
@@ -29,11 +36,15 @@ export declare class Tree implements Renderable, Measurable {
|
|
|
29
36
|
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
30
37
|
* come to disagree about how many rows there are or how wide the guides on
|
|
31
38
|
* them run — which they would the moment a second walk existed.
|
|
39
|
+
*
|
|
40
|
+
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
41
|
+
* this one recursion: its children get the columns above it, settled, plus a
|
|
42
|
+
* branch of their own, so no depth is drawn by a rule of its own.
|
|
32
43
|
*/
|
|
33
44
|
private _rows;
|
|
34
45
|
/**
|
|
35
|
-
* One row of the tree: its
|
|
36
|
-
* guides
|
|
46
|
+
* One row of the tree: its label in whatever width the guides left, each of
|
|
47
|
+
* the label's lines led by the guides.
|
|
37
48
|
*
|
|
38
49
|
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
39
50
|
* Before this, each call site emitted its guides and then handed the label
|
|
@@ -44,7 +55,10 @@ export declare class Tree implements Renderable, Measurable {
|
|
|
44
55
|
* its share off the division. `options` arrives parsed from `render`.
|
|
45
56
|
*
|
|
46
57
|
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
47
|
-
* depending on label renderables to invent trailing newlines
|
|
58
|
+
* depending on label renderables to invent trailing newlines: the label's
|
|
59
|
+
* output is cut into lines and every line ends where Tree ends it. A label
|
|
60
|
+
* that emits nothing still holds its row, and one that ends in a newline of
|
|
61
|
+
* its own does not add a blank one.
|
|
48
62
|
*/
|
|
49
63
|
private _renderRow;
|
|
50
64
|
measure(options: RenderOptions): {
|
package/dist/renderables/tree.js
CHANGED
|
@@ -3,30 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import { cellFit, cellLen, asCellCol } from "../core/cells.js";
|
|
6
|
-
import { NULL_STYLE } from "../core/style.js";
|
|
7
|
-
import {
|
|
8
|
-
import { getStyle, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
6
|
+
import { Style, NULL_STYLE } from "../core/style.js";
|
|
7
|
+
import { embed } from "./embed.js";
|
|
8
|
+
import { drawable, getStyle, isMeasurable, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
9
|
import { Measurement } from "../core/measure.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
function toRenderable(label) {
|
|
20
|
-
if (typeof label === "string")
|
|
21
|
-
return new RichText(label, { end: "" });
|
|
22
|
-
if (label instanceof RichText) {
|
|
23
|
-
// Ensure no trailing newline in tree labels
|
|
24
|
-
const copy = label.copy();
|
|
25
|
-
copy.end = "";
|
|
26
|
-
return copy;
|
|
27
|
-
}
|
|
28
|
-
return label;
|
|
29
|
-
}
|
|
10
|
+
const UNICODE_GUIDES = {
|
|
11
|
+
fork: { first: "├── ", rest: "│ " },
|
|
12
|
+
end: { first: "└── ", rest: " " },
|
|
13
|
+
};
|
|
14
|
+
const ASCII_GUIDES = {
|
|
15
|
+
fork: { first: "+-- ", rest: "| " },
|
|
16
|
+
end: { first: "`-- ", rest: " " },
|
|
17
|
+
};
|
|
18
|
+
const guideGlyphs = ({ fork, end }) => fork.first + fork.rest + end.first + end.rest;
|
|
30
19
|
/**
|
|
31
20
|
* How wide a label wants to be. A label with no `measure` cannot say, and the
|
|
32
21
|
* offer is the honest stand-in — including when the offer is unbounded, which
|
|
@@ -45,7 +34,7 @@ export class Tree {
|
|
|
45
34
|
guideStyle;
|
|
46
35
|
style;
|
|
47
36
|
constructor(label, options) {
|
|
48
|
-
this.label =
|
|
37
|
+
this.label = embed(label);
|
|
49
38
|
this.children = [];
|
|
50
39
|
this.expanded = options?.expanded !== false;
|
|
51
40
|
this.hideRoot = options?.hideRoot ?? false;
|
|
@@ -63,10 +52,20 @@ export class Tree {
|
|
|
63
52
|
}
|
|
64
53
|
*render(rawOptions) {
|
|
65
54
|
const options = withBoundedWidth(rawOptions, this);
|
|
66
|
-
for (const row of this.
|
|
55
|
+
for (const row of this._walk(options)) {
|
|
67
56
|
yield* this._renderRow(options, row);
|
|
68
57
|
}
|
|
69
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The rows `render` and `measure` both read, entered in exactly one place so
|
|
61
|
+
* the two cannot start the walk differently. A hidden root takes its own row
|
|
62
|
+
* and the column its children hang from with it, as Rich does — its children
|
|
63
|
+
* stand at the left edge, and its guide style still reaches their guides.
|
|
64
|
+
*/
|
|
65
|
+
_walk(options) {
|
|
66
|
+
const rows = [...this._rows(drawable(options, UNICODE_GUIDES, ASCII_GUIDES, guideGlyphs), [], [])];
|
|
67
|
+
return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
|
|
68
|
+
}
|
|
70
69
|
/**
|
|
71
70
|
* The tree walked into the rows it emits, one row per label, guides first.
|
|
72
71
|
*
|
|
@@ -75,41 +74,24 @@ export class Tree {
|
|
|
75
74
|
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
76
75
|
* come to disagree about how many rows there are or how wide the guides on
|
|
77
76
|
* them run — which they would the moment a second walk existed.
|
|
77
|
+
*
|
|
78
|
+
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
79
|
+
* this one recursion: its children get the columns above it, settled, plus a
|
|
80
|
+
* branch of their own, so no depth is drawn by a rule of its own.
|
|
78
81
|
*/
|
|
79
|
-
*_rows(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const child = this.children[i];
|
|
88
|
-
const isLast = i === this.children.length - 1;
|
|
89
|
-
const branch = ascii
|
|
90
|
-
? (isLast ? GUIDE_LAST_ASCII : GUIDE_BRANCH_ASCII)
|
|
91
|
-
: (isLast ? GUIDE_LAST : GUIDE_BRANCH);
|
|
92
|
-
const continuation = ascii
|
|
93
|
-
? (isLast ? GUIDE_SPACE : GUIDE_VERT_ASCII)
|
|
94
|
-
: (isLast ? GUIDE_SPACE : GUIDE_VERT);
|
|
95
|
-
// Child label with branch guide
|
|
96
|
-
yield {
|
|
97
|
-
guides: [...prefixes, branch],
|
|
98
|
-
guideStyles: [child.guideStyle, this.guideStyle],
|
|
99
|
-
label: child.label,
|
|
100
|
-
};
|
|
101
|
-
// Grandchildren with continuation prefix
|
|
102
|
-
if (child.expanded && child.children.length > 0) {
|
|
103
|
-
const grandPrefixes = [...prefixes, continuation];
|
|
104
|
-
for (let j = 0; j < child.children.length; j++) {
|
|
105
|
-
yield* child.children[j]._rows(options, grandPrefixes, true);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
82
|
+
*_rows(glyphs, guides, inherited) {
|
|
83
|
+
yield { guides, label: this.label };
|
|
84
|
+
const children = this.expanded ? this.children : [];
|
|
85
|
+
const styles = [...inherited, this.guideStyle];
|
|
86
|
+
const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
|
|
87
|
+
for (let i = 0; i < children.length; i++) {
|
|
88
|
+
const branch = i === children.length - 1 ? glyphs.end : glyphs.fork;
|
|
89
|
+
yield* children[i]._rows(glyphs, [...above, { ...branch, styles }], styles);
|
|
108
90
|
}
|
|
109
91
|
}
|
|
110
92
|
/**
|
|
111
|
-
* One row of the tree: its
|
|
112
|
-
* guides
|
|
93
|
+
* One row of the tree: its label in whatever width the guides left, each of
|
|
94
|
+
* the label's lines led by the guides.
|
|
113
95
|
*
|
|
114
96
|
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
115
97
|
* Before this, each call site emitted its guides and then handed the label
|
|
@@ -120,22 +102,31 @@ export class Tree {
|
|
|
120
102
|
* its share off the division. `options` arrives parsed from `render`.
|
|
121
103
|
*
|
|
122
104
|
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
123
|
-
* depending on label renderables to invent trailing newlines
|
|
105
|
+
* depending on label renderables to invent trailing newlines: the label's
|
|
106
|
+
* output is cut into lines and every line ends where Tree ends it. A label
|
|
107
|
+
* that emits nothing still holds its row, and one that ends in a newline of
|
|
108
|
+
* its own does not add a blank one.
|
|
124
109
|
*/
|
|
125
110
|
*_renderRow(options, row) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
const styles = row.guides.map((guide) => Style.combine(guide.styles.map((style) => getStyle(options, style))));
|
|
112
|
+
const width = Math.max(0, options.maxWidth - row.guides.reduce((sum, guide) => sum + cellLen(guide.first), 0));
|
|
113
|
+
// The guides are cropped by `cellFit`; the label is cropped too, so a label
|
|
114
|
+
// that ignores the width it is handed cannot push the row past the offer the
|
|
115
|
+
// guides were fitted into.
|
|
116
|
+
const lines = Segment.splitLines(Segment.cropLines(row.label.render({ ...options, maxWidth: width, height: stackedHeight(options.height) }), width));
|
|
117
|
+
if (lines.length === 0)
|
|
118
|
+
lines.push([]);
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
let left = options.maxWidth;
|
|
121
|
+
for (let g = 0; g < row.guides.length; g++) {
|
|
122
|
+
const piece = cellFit(i === 0 ? row.guides[g].first : row.guides[g].rest, asCellCol(left));
|
|
123
|
+
if (piece.length > 0)
|
|
124
|
+
yield new Segment(piece, styles[g]);
|
|
125
|
+
left -= cellLen(piece);
|
|
126
|
+
}
|
|
127
|
+
yield* lines[i];
|
|
128
|
+
yield Segment.line();
|
|
133
129
|
}
|
|
134
|
-
// The guides above are cropped by `cellFit`; the label was not, so a label
|
|
135
|
-
// that ignores the width it is handed pushed the row past the offer the
|
|
136
|
-
// guides had just been fitted into.
|
|
137
|
-
yield* Segment.cropLines(row.label.render({ ...options, maxWidth: left }), left);
|
|
138
|
-
yield Segment.line();
|
|
139
130
|
}
|
|
140
131
|
measure(options) {
|
|
141
132
|
// The ceiling is parsed for the same reason the rows are: an unparsed NaN
|
|
@@ -147,8 +138,8 @@ export class Tree {
|
|
|
147
138
|
// mode drew a 40-cell frame around nine cells of tree, and an unbounded
|
|
148
139
|
// offer came back unbounded.
|
|
149
140
|
let natural = 0;
|
|
150
|
-
for (const row of this.
|
|
151
|
-
const guideWidth = row.guides.reduce((sum,
|
|
141
|
+
for (const row of this._walk(parsed)) {
|
|
142
|
+
const guideWidth = row.guides.reduce((sum, guide) => sum + cellLen(guide.first), 0);
|
|
152
143
|
natural = Math.max(natural, guideWidth + labelWidth(parsed, row.label));
|
|
153
144
|
}
|
|
154
145
|
const maximum = Math.min(natural, ceiling);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport — a fixed number of rows onto a taller renderable, scrolled by an
|
|
3
|
+
* offset that cannot leave the content.
|
|
4
|
+
*
|
|
5
|
+
* Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
|
|
6
|
+
* when it is given one; otherwise its own configured rows, or the content's
|
|
7
|
+
* full length when it has none, capped by any ceiling. It renders its content
|
|
8
|
+
* with no height, because the content's natural length is the scroll extent,
|
|
9
|
+
* and pads or crops what comes back to its rows — its rows are its own height,
|
|
10
|
+
* not a region someone else will shape.
|
|
11
|
+
*
|
|
12
|
+
* Its width is the width it is given, in both directions: each row is cropped
|
|
13
|
+
* or padded to it, so content that ignores its width cannot spill past the
|
|
14
|
+
* viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
|
|
15
|
+
* down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
|
|
16
|
+
* has a gutter zero cells wide, and every row takes the same path either way.
|
|
17
|
+
*
|
|
18
|
+
* [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
|
|
19
|
+
* against exists before a render: the rows come from the budget the render is
|
|
20
|
+
* handed, and the content's length from rendering it at the width it is handed.
|
|
21
|
+
* So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
|
|
22
|
+
* are called. Each queues a move, and the next render resolves the queue in
|
|
23
|
+
* call order, clamping after every move, against the rows and length that
|
|
24
|
+
* render found. Calling an operation before the first render, or three of them
|
|
25
|
+
* between two renders, is the same code path as calling one.
|
|
26
|
+
*
|
|
27
|
+
* Every cell of its rows, gutter included, is stamped as its own
|
|
28
|
+
* (`Segment.anchorLines`), around whatever anchors its content drew. So the
|
|
29
|
+
* composed frame names the viewport under any of its cells however deep it is
|
|
30
|
+
* nested, and a cell of a widget inside it names both: that is how the wheel
|
|
31
|
+
* finds the viewport to scroll. `canScrollBy` is how it passes over one that
|
|
32
|
+
* cannot move that way, to the viewport around it.
|
|
33
|
+
*/
|
|
34
|
+
import { Segment } from "../core/segment.js";
|
|
35
|
+
import type { Style } from "../core/style.js";
|
|
36
|
+
import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
|
|
37
|
+
/** How one part of a scrollbar draws each row it covers. */
|
|
38
|
+
export interface ScrollbarPart {
|
|
39
|
+
readonly glyph: string;
|
|
40
|
+
/** A style, or a theme name resolved when the viewport renders. */
|
|
41
|
+
readonly style: string | Style;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The look of a scrollbar: the thumb, over the rows of the track that stand for
|
|
45
|
+
* the lines in view, and the track, over the rest. Its gutter is as wide as the
|
|
46
|
+
* wider of the two glyphs.
|
|
47
|
+
*/
|
|
48
|
+
export interface Scrollbar {
|
|
49
|
+
readonly thumb: ScrollbarPart;
|
|
50
|
+
readonly track: ScrollbarPart;
|
|
51
|
+
}
|
|
52
|
+
/** A heavy line for the thumb on a light line for the track. */
|
|
53
|
+
export declare const SCROLLBAR: Scrollbar;
|
|
54
|
+
export interface ViewportOptions {
|
|
55
|
+
/**
|
|
56
|
+
* The rows shown when the viewport is given no region. Absent, it shows the
|
|
57
|
+
* content's full length. A ceiling caps either.
|
|
58
|
+
*/
|
|
59
|
+
rows?: number;
|
|
60
|
+
/** A scrollbar down the right edge. Absent, the content has the full width. */
|
|
61
|
+
scrollbar?: Scrollbar;
|
|
62
|
+
}
|
|
63
|
+
export declare class Viewport implements Renderable, Measurable, Scrollable {
|
|
64
|
+
/**
|
|
65
|
+
* What the viewport shows. Replace it to show new content from the same
|
|
66
|
+
* scroll position — a view rebuilt every frame keeps one `Viewport`.
|
|
67
|
+
*/
|
|
68
|
+
content: Renderable;
|
|
69
|
+
readonly rows: number | undefined;
|
|
70
|
+
readonly scrollbar: Scrollbar;
|
|
71
|
+
private _offset;
|
|
72
|
+
private _extent;
|
|
73
|
+
private _moves;
|
|
74
|
+
constructor(content: Renderable, options?: ViewportOptions);
|
|
75
|
+
/**
|
|
76
|
+
* The first content line the last render showed. A move requested since is
|
|
77
|
+
* not reflected until the next render resolves it.
|
|
78
|
+
*/
|
|
79
|
+
get offset(): number;
|
|
80
|
+
/** Scroll so `line` is the first line shown. */
|
|
81
|
+
scrollTo(line: number): void;
|
|
82
|
+
/** Scroll by `lines`: down when positive, up when negative. */
|
|
83
|
+
scrollBy(lines: number): void;
|
|
84
|
+
/**
|
|
85
|
+
* Whether `scrollBy(lines)` would move the viewport from where the moves
|
|
86
|
+
* queued so far leave it, against the rows and length the last render
|
|
87
|
+
* found.
|
|
88
|
+
*/
|
|
89
|
+
canScrollBy(lines: number): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Scroll the least distance that shows lines `start` up to but not
|
|
92
|
+
* including `end` — lines of the content as it renders at the viewport's
|
|
93
|
+
* width, so an item that wraps spans more than one. A range already in view does not move; one taller than
|
|
94
|
+
* the viewport shows its first line at the top.
|
|
95
|
+
*/
|
|
96
|
+
ensureVisible(start: number, end: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* The width its content renders at, out of `width`: what the scrollbar's
|
|
99
|
+
* gutter leaves. Lines handed to `ensureVisible` are lines at this width.
|
|
100
|
+
*/
|
|
101
|
+
contentWidth(width: number): number;
|
|
102
|
+
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
103
|
+
/**
|
|
104
|
+
* [LAW:one-source-of-truth] Where the queued moves leave the offset against
|
|
105
|
+
* `extent`: what a render commits, and what `canScrollBy` asks from.
|
|
106
|
+
* [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
|
|
107
|
+
* queued moves: content that shrank since the last render pulls it back.
|
|
108
|
+
*/
|
|
109
|
+
private resolve;
|
|
110
|
+
measure(rawOptions: RenderOptions): {
|
|
111
|
+
minimum: number;
|
|
112
|
+
maximum: number;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport — a fixed number of rows onto a taller renderable, scrolled by an
|
|
3
|
+
* offset that cannot leave the content.
|
|
4
|
+
*
|
|
5
|
+
* Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
|
|
6
|
+
* when it is given one; otherwise its own configured rows, or the content's
|
|
7
|
+
* full length when it has none, capped by any ceiling. It renders its content
|
|
8
|
+
* with no height, because the content's natural length is the scroll extent,
|
|
9
|
+
* and pads or crops what comes back to its rows — its rows are its own height,
|
|
10
|
+
* not a region someone else will shape.
|
|
11
|
+
*
|
|
12
|
+
* Its width is the width it is given, in both directions: each row is cropped
|
|
13
|
+
* or padded to it, so content that ignores its width cannot spill past the
|
|
14
|
+
* viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
|
|
15
|
+
* down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
|
|
16
|
+
* has a gutter zero cells wide, and every row takes the same path either way.
|
|
17
|
+
*
|
|
18
|
+
* [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
|
|
19
|
+
* against exists before a render: the rows come from the budget the render is
|
|
20
|
+
* handed, and the content's length from rendering it at the width it is handed.
|
|
21
|
+
* So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
|
|
22
|
+
* are called. Each queues a move, and the next render resolves the queue in
|
|
23
|
+
* call order, clamping after every move, against the rows and length that
|
|
24
|
+
* render found. Calling an operation before the first render, or three of them
|
|
25
|
+
* between two renders, is the same code path as calling one.
|
|
26
|
+
*
|
|
27
|
+
* Every cell of its rows, gutter included, is stamped as its own
|
|
28
|
+
* (`Segment.anchorLines`), around whatever anchors its content drew. So the
|
|
29
|
+
* composed frame names the viewport under any of its cells however deep it is
|
|
30
|
+
* nested, and a cell of a widget inside it names both: that is how the wheel
|
|
31
|
+
* finds the viewport to scroll. `canScrollBy` is how it passes over one that
|
|
32
|
+
* cannot move that way, to the viewport around it.
|
|
33
|
+
*/
|
|
34
|
+
import { Segment } from "../core/segment.js";
|
|
35
|
+
import { Measurement } from "../core/measure.js";
|
|
36
|
+
import { cellCount, cellLen } from "../core/cells.js";
|
|
37
|
+
import { NULL_STYLE } from "../core/style.js";
|
|
38
|
+
import { drawable, fitHeight, getStyle, isMeasurable, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
|
|
39
|
+
/** A heavy line for the thumb on a light line for the track. */
|
|
40
|
+
export const SCROLLBAR = {
|
|
41
|
+
thumb: { glyph: "┃", style: "scrollbar.thumb" },
|
|
42
|
+
track: { glyph: "│", style: "scrollbar.track" },
|
|
43
|
+
};
|
|
44
|
+
/** The absence of a scrollbar, as a gutter zero cells wide. */
|
|
45
|
+
const NO_SCROLLBAR = {
|
|
46
|
+
thumb: { glyph: "", style: NULL_STYLE },
|
|
47
|
+
track: { glyph: "", style: NULL_STYLE },
|
|
48
|
+
};
|
|
49
|
+
export class Viewport {
|
|
50
|
+
/**
|
|
51
|
+
* What the viewport shows. Replace it to show new content from the same
|
|
52
|
+
* scroll position — a view rebuilt every frame keeps one `Viewport`.
|
|
53
|
+
*/
|
|
54
|
+
content;
|
|
55
|
+
rows;
|
|
56
|
+
scrollbar;
|
|
57
|
+
_offset = 0;
|
|
58
|
+
// Nothing rendered, nothing shown: no move can take until the first render.
|
|
59
|
+
_extent = { rows: 0, lines: 0 };
|
|
60
|
+
_moves = [];
|
|
61
|
+
constructor(content, options = {}) {
|
|
62
|
+
this.content = content;
|
|
63
|
+
this.rows = options.rows;
|
|
64
|
+
this.scrollbar = options.scrollbar ?? NO_SCROLLBAR;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The first content line the last render showed. A move requested since is
|
|
68
|
+
* not reflected until the next render resolves it.
|
|
69
|
+
*/
|
|
70
|
+
get offset() {
|
|
71
|
+
return this._offset;
|
|
72
|
+
}
|
|
73
|
+
/** Scroll so `line` is the first line shown. */
|
|
74
|
+
scrollTo(line) {
|
|
75
|
+
this._moves.push(() => line);
|
|
76
|
+
}
|
|
77
|
+
/** Scroll by `lines`: down when positive, up when negative. */
|
|
78
|
+
scrollBy(lines) {
|
|
79
|
+
this._moves.push((offset) => offset + lines);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether `scrollBy(lines)` would move the viewport from where the moves
|
|
83
|
+
* queued so far leave it, against the rows and length the last render
|
|
84
|
+
* found.
|
|
85
|
+
*/
|
|
86
|
+
canScrollBy(lines) {
|
|
87
|
+
const offset = this.resolve(this._extent);
|
|
88
|
+
return clampOffset(offset + lines, this._extent) !== offset;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Scroll the least distance that shows lines `start` up to but not
|
|
92
|
+
* including `end` — lines of the content as it renders at the viewport's
|
|
93
|
+
* width, so an item that wraps spans more than one. A range already in view does not move; one taller than
|
|
94
|
+
* the viewport shows its first line at the top.
|
|
95
|
+
*/
|
|
96
|
+
ensureVisible(start, end) {
|
|
97
|
+
this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The width its content renders at, out of `width`: what the scrollbar's
|
|
101
|
+
* gutter leaves. Lines handed to `ensureVisible` are lines at this width.
|
|
102
|
+
*/
|
|
103
|
+
contentWidth(width) {
|
|
104
|
+
return cellCount(width - gutterWidth(this.scrollbar));
|
|
105
|
+
}
|
|
106
|
+
*render(rawOptions) {
|
|
107
|
+
const { height, ...options } = withBoundedWidth(rawOptions, this);
|
|
108
|
+
const gutter = gutterWidth(this.scrollbar);
|
|
109
|
+
const contentWidth = this.contentWidth(options.maxWidth);
|
|
110
|
+
const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
|
|
111
|
+
const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
|
|
112
|
+
this._extent = extent;
|
|
113
|
+
this._offset = this.resolve(extent);
|
|
114
|
+
this._moves = [];
|
|
115
|
+
const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
|
|
116
|
+
// [LAW:no-ambient-temporal-coupling] The thumb is drawn from the offset
|
|
117
|
+
// this render just resolved, not from `offset` as it stood before it.
|
|
118
|
+
const thumb = thumbRows(extent, this._offset);
|
|
119
|
+
// The content's cells and the gutter's drawn cells sum to the offer: a
|
|
120
|
+
// gutter wider than the whole offer is drawn cropped to it.
|
|
121
|
+
const drawn = Math.min(gutter, options.maxWidth);
|
|
122
|
+
// The style reaches the padding too, so a glyph narrower than the gutter
|
|
123
|
+
// still fills its cell with its part's background.
|
|
124
|
+
const cell = ({ glyph, style }) => {
|
|
125
|
+
const resolved = getStyle(options, style);
|
|
126
|
+
return Segment.adjustLineLength([new Segment(glyph, resolved)], drawn, resolved);
|
|
127
|
+
};
|
|
128
|
+
// The gutter keeps the chosen scrollbar's width, so an ASCII stand-in
|
|
129
|
+
// padded into it leaves the content where `contentWidth` said it would be.
|
|
130
|
+
const bar = drawable(options, this.scrollbar, asciiScrollbar(this.scrollbar), scrollbarGlyphs);
|
|
131
|
+
const thumbCell = cell(bar.thumb);
|
|
132
|
+
const trackCell = cell(bar.track);
|
|
133
|
+
const rows = shown.map((line, row) => [
|
|
134
|
+
...Segment.adjustLineLength(line, contentWidth),
|
|
135
|
+
...(row >= thumb.start && row < thumb.end ? thumbCell : trackCell),
|
|
136
|
+
]);
|
|
137
|
+
for (const line of Segment.anchorLines(rows, this)) {
|
|
138
|
+
yield* line;
|
|
139
|
+
yield Segment.line();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* [LAW:one-source-of-truth] Where the queued moves leave the offset against
|
|
144
|
+
* `extent`: what a render commits, and what `canScrollBy` asks from.
|
|
145
|
+
* [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
|
|
146
|
+
* queued moves: content that shrank since the last render pulls it back.
|
|
147
|
+
*/
|
|
148
|
+
resolve(extent) {
|
|
149
|
+
return this._moves.reduce((offset, move) => clampOffset(move(offset, extent), extent), clampOffset(this._offset, extent));
|
|
150
|
+
}
|
|
151
|
+
measure(rawOptions) {
|
|
152
|
+
// Its width is its content's and its gutter's. `withCellWidth` and not
|
|
153
|
+
// `withBoundedWidth`: the bounded parse asks this very method for the
|
|
154
|
+
// natural width.
|
|
155
|
+
const options = withCellWidth(rawOptions);
|
|
156
|
+
const gutter = gutterWidth(this.scrollbar);
|
|
157
|
+
const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
|
|
158
|
+
const content = isMeasurable(this.content)
|
|
159
|
+
? Measurement.get(inner, this.content)
|
|
160
|
+
: new Measurement(Math.min(1, inner.maxWidth), inner.maxWidth);
|
|
161
|
+
return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
|
|
165
|
+
/** `scrollbar` in its own styles, drawn in ASCII. */
|
|
166
|
+
function asciiScrollbar({ thumb, track }) {
|
|
167
|
+
return { thumb: { ...thumb, glyph: "#" }, track: { ...track, glyph: "|" } };
|
|
168
|
+
}
|
|
169
|
+
/** The cells a scrollbar's gutter takes from the content: its wider glyph. */
|
|
170
|
+
function gutterWidth({ thumb, track }) {
|
|
171
|
+
return Math.max(cellLen(thumb.glyph), cellLen(track.glyph));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The rows of the track the thumb covers, from `start` up to but not including
|
|
175
|
+
* `end`: its length is the share of the content in view, and its position the
|
|
176
|
+
* share of the scroll travelled. Content that fits fills the track.
|
|
177
|
+
*
|
|
178
|
+
* An end of the track means an end of the content. Rounded alone, the position
|
|
179
|
+
* reached the bottom while lines were still hidden below — at 10 rows of 20
|
|
180
|
+
* lines, offset 9 rounds onto the last position. So an offset short of an end
|
|
181
|
+
* is held one row off it, wherever the track has a row between its ends.
|
|
182
|
+
*/
|
|
183
|
+
function thumbRows({ rows, lines }, offset) {
|
|
184
|
+
const size = Math.min(rows, Math.max(1, Math.round((rows * rows) / Math.max(lines, rows, 1))));
|
|
185
|
+
const travel = rows - size;
|
|
186
|
+
const last = Math.max(1, lines - rows);
|
|
187
|
+
const proportional = Math.round((offset * travel) / last);
|
|
188
|
+
const start = Math.max(Math.min(proportional, travel - Math.min(last - offset, 1)), Math.min(offset, 1, travel));
|
|
189
|
+
return { start, end: start + size };
|
|
190
|
+
}
|
|
191
|
+
/** The rows a viewport shows, from its budget, its own configured rows, and its content's length. */
|
|
192
|
+
function viewRows(height, rows, lines) {
|
|
193
|
+
return regionRows(height) ?? cellCount(Math.min(rows ?? lines, height?.rows ?? Infinity));
|
|
194
|
+
}
|
|
195
|
+
/** An offset held to the lines that exist: never above the first, never past the last full view. */
|
|
196
|
+
function clampOffset(offset, { rows, lines }) {
|
|
197
|
+
return cellCount(Math.min(offset, lines - rows));
|
|
198
|
+
}
|
|
@@ -77,20 +77,19 @@ function asColor(value, func) {
|
|
|
77
77
|
}
|
|
78
78
|
return parseHexColor(value);
|
|
79
79
|
}
|
|
80
|
+
// [LAW:single-enforcer] Every colour function is fixed-arity, so the engine's
|
|
81
|
+
// arity gate refuses a missing or surplus argument before the body runs.
|
|
80
82
|
function colorFunc(argTypes, fn) {
|
|
81
|
-
return { fn, argTypes, returnType: "string" };
|
|
83
|
+
return { fn, argTypes, arity: { kind: "exact" }, returnType: "string" };
|
|
82
84
|
}
|
|
83
85
|
/**
|
|
84
86
|
* The gate every *numeric* argument crosses, with an optional inclusive range.
|
|
85
87
|
*
|
|
86
|
-
* [LAW:single-enforcer] One check for
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* — loud, but pointing at a channel the author never wrote, three layers from
|
|
92
|
-
* the missing argument. An error that does not locate its cause is barely
|
|
93
|
-
* better than a silent one. [LAW:no-silent-failure]
|
|
88
|
+
* [LAW:single-enforcer] One check for every `"float"` slot. An `"int"` slot is
|
|
89
|
+
* a safe integer by the engine's gate and needs none, but a `"float"` slot admits
|
|
90
|
+
* NaN and Infinity (legitimate IEEE-754 values), which would otherwise surface
|
|
91
|
+
* as `ColorRgba.red must be an integer in [0, 255]; got NaN` — loud, but
|
|
92
|
+
* pointing at a channel the author never wrote. [LAW:no-silent-failure]
|
|
94
93
|
*/
|
|
95
94
|
function asAmount(value, func, param, range) {
|
|
96
95
|
const inRange = range === undefined || (value >= range.min && value <= range.max);
|
|
@@ -98,8 +97,7 @@ function asAmount(value, func, param, range) {
|
|
|
98
97
|
const bound = range === undefined
|
|
99
98
|
? "a finite number"
|
|
100
99
|
: `within ${range.min}..${range.max} (${range.note})`;
|
|
101
|
-
throw new RangeError(`${func}'s ${param} must be ${bound}, got ${value}`
|
|
102
|
-
(value === undefined ? ` — ${func} takes it as its last argument` : ""));
|
|
100
|
+
throw new RangeError(`${func}'s ${param} must be ${bound}, got ${value}`);
|
|
103
101
|
}
|
|
104
102
|
return value;
|
|
105
103
|
}
|
|
@@ -109,8 +107,8 @@ function asAmount(value, func, param, range) {
|
|
|
109
107
|
// second one (colorMath's `lighten` is literally `darken(c, -levels)`). Both
|
|
110
108
|
// names ship because "lighten by 2" and "darken by -2" are not equally
|
|
111
109
|
// readable at a call site, and the operation is genuinely bidirectional.
|
|
112
|
-
const darkenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "darken"),
|
|
113
|
-
const lightenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "lighten"), -
|
|
110
|
+
const darkenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "darken"), levels).hex));
|
|
111
|
+
const lightenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "lighten"), -levels).hex));
|
|
114
112
|
// --- Blending ---
|
|
115
113
|
//
|
|
116
114
|
// [LAW:one-type-per-behavior] There is no separate `alphaBlend` binding.
|
|
@@ -120,11 +120,13 @@ export function renderTemplate(engine, source, scope = {}, options) {
|
|
|
120
120
|
return Array.from(rt.render({
|
|
121
121
|
maxWidth: options?.maxWidth ?? 400,
|
|
122
122
|
isTerminal: true,
|
|
123
|
-
encoding: "utf-8",
|
|
124
123
|
}));
|
|
125
124
|
}
|
|
126
125
|
catch (e) {
|
|
127
|
-
|
|
126
|
+
// An engine error's `message` is its one-line summary; `String(e)` adds a
|
|
127
|
+
// multi-line code frame, which would break this single segment across lines.
|
|
128
|
+
const summary = e instanceof Error ? e.message : String(e);
|
|
129
|
+
return [new Segment(`[error: ${summary.slice(0, 80)}]`, safeErrorStyle(options?.errorStyle))];
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
// [LAW:single-enforcer] The error-path Style must not itself throw, or
|