@promptctl/rich-js 0.18.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/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 +143 -0
- package/dist/core/box.js +379 -0
- package/dist/core/cells.d.ts +159 -0
- package/dist/core/cells.js +287 -0
- package/dist/core/color.d.ts +246 -0
- package/dist/core/color.js +1038 -0
- package/dist/core/console.d.ts +165 -0
- package/dist/core/console.js +527 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +200 -0
- package/dist/core/export-lines.d.ts +121 -0
- package/dist/core/export-lines.js +141 -0
- package/dist/core/highlighter.d.ts +33 -0
- package/dist/core/highlighter.js +124 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +119 -0
- package/dist/core/markup.js +731 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +62 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +90 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +144 -0
- package/dist/core/pretty.js +476 -0
- package/dist/core/protocol.d.ts +267 -0
- package/dist/core/protocol.js +163 -0
- package/dist/core/render.d.ts +84 -0
- package/dist/core/render.js +145 -0
- package/dist/core/segment.d.ts +161 -0
- package/dist/core/segment.js +492 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +141 -0
- package/dist/core/strip.js +305 -0
- package/dist/core/style.d.ts +169 -0
- package/dist/core/style.js +695 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +256 -0
- package/dist/core/text.js +976 -0
- package/dist/core/wrap.d.ts +41 -0
- package/dist/core/wrap.js +127 -0
- 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 +5 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +179 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +107 -0
- package/dist/node/prompt.d.ts +21 -0
- package/dist/node/prompt.js +30 -0
- package/dist/node/save.d.ts +17 -0
- package/dist/node/save.js +21 -0
- package/dist/node/terminal-host.d.ts +113 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +37 -0
- package/dist/node/traceback.js +134 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +33 -0
- package/dist/renderables/columns.d.ts +61 -0
- package/dist/renderables/columns.js +152 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +37 -0
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +11 -0
- package/dist/renderables/group.js +17 -0
- package/dist/renderables/layout.d.ts +92 -0
- package/dist/renderables/layout.js +312 -0
- package/dist/renderables/live.d.ts +50 -0
- package/dist/renderables/live.js +175 -0
- package/dist/renderables/markdown.d.ts +24 -0
- package/dist/renderables/markdown.js +184 -0
- package/dist/renderables/padding.d.ts +38 -0
- package/dist/renderables/padding.js +141 -0
- package/dist/renderables/panel.d.ts +116 -0
- package/dist/renderables/panel.js +307 -0
- package/dist/renderables/progress.d.ts +90 -0
- package/dist/renderables/progress.js +238 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +53 -0
- package/dist/renderables/prompt.d.ts +49 -0
- package/dist/renderables/prompt.js +125 -0
- package/dist/renderables/rule.d.ts +25 -0
- package/dist/renderables/rule.js +90 -0
- package/dist/renderables/spinner.d.ts +31 -0
- package/dist/renderables/spinner.js +70 -0
- package/dist/renderables/status.d.ts +23 -0
- package/dist/renderables/status.js +60 -0
- package/dist/renderables/syntax.d.ts +35 -0
- package/dist/renderables/syntax.js +115 -0
- package/dist/renderables/table.d.ts +194 -0
- package/dist/renderables/table.js +795 -0
- package/dist/renderables/traceback.d.ts +31 -0
- package/dist/renderables/traceback.js +108 -0
- package/dist/renderables/tree.d.ts +68 -0
- package/dist/renderables/tree.js +150 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.d.ts +87 -0
- package/dist/template-bindings/color-funcs.js +217 -0
- package/dist/template-bindings/helpers.d.ts +24 -0
- package/dist/template-bindings/helpers.js +48 -0
- package/dist/template-bindings/index.d.ts +107 -0
- package/dist/template-bindings/index.js +146 -0
- package/dist/template-bindings/palette-funcs.d.ts +94 -0
- package/dist/template-bindings/palette-funcs.js +141 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +155 -0
- package/dist/themes/buildPalette.d.ts +36 -0
- package/dist/themes/buildPalette.js +56 -0
- package/dist/themes/colorMath.d.ts +82 -0
- package/dist/themes/colorMath.js +254 -0
- package/dist/themes/colorRef.d.ts +51 -0
- package/dist/themes/colorRef.js +96 -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.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +21 -0
- package/dist/themes/palette.js +28 -0
- package/dist/themes/ramp.d.ts +83 -0
- package/dist/themes/ramp.js +122 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +94 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +49 -0
- package/dist/widgets/button.js +208 -0
- package/dist/widgets/checkbox.d.ts +41 -0
- package/dist/widgets/checkbox.js +157 -0
- package/dist/widgets/dropdown.d.ts +67 -0
- package/dist/widgets/dropdown.js +450 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/slider.d.ts +57 -0
- package/dist/widgets/slider.js +270 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +274 -0
- package/dist/widgets/text-input.js +1165 -0
- package/dist/widgets/toggle.d.ts +44 -0
- package/dist/widgets/toggle.js +180 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +40 -0
- package/dist/widgets/widget-base.js +165 -0
- package/package.json +6 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traceback — renders error tracebacks with formatting.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:effects-at-boundaries] Pure rendering: an `Error` in, `Segment`s out.
|
|
5
|
+
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
|
+
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
|
+
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
8
|
+
* main barrel, importable in a browser.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
|
|
11
|
+
* reads. There is no `showLocals`: an `Error` carries a stack of locations and
|
|
12
|
+
* nothing of the values in scope at them. The one way to get those in node —
|
|
13
|
+
* an inspector session pausing on exceptions — would have to pause on every
|
|
14
|
+
* throw, caught ones included, to serve `new Traceback(caughtError)`, and it
|
|
15
|
+
* cannot exist in a browser at all. Nor is there a `width` or `theme`: those
|
|
16
|
+
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import type { Renderable, RenderOptions } from "../core/protocol.js";
|
|
20
|
+
export interface TracebackOptions {
|
|
21
|
+
suppress?: string[];
|
|
22
|
+
maxFrames?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class Traceback implements Renderable {
|
|
25
|
+
readonly error: Error;
|
|
26
|
+
readonly maxFrames: number;
|
|
27
|
+
readonly suppress: string[];
|
|
28
|
+
constructor(error: Error, options?: TracebackOptions);
|
|
29
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
30
|
+
private _renderFrame;
|
|
31
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traceback — renders error tracebacks with formatting.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:effects-at-boundaries] Pure rendering: an `Error` in, `Segment`s out.
|
|
5
|
+
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
|
+
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
|
+
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
8
|
+
* main barrel, importable in a browser.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
|
|
11
|
+
* reads. There is no `showLocals`: an `Error` carries a stack of locations and
|
|
12
|
+
* nothing of the values in scope at them. The one way to get those in node —
|
|
13
|
+
* an inspector session pausing on exceptions — would have to pause on every
|
|
14
|
+
* throw, caught ones included, to serve `new Traceback(caughtError)`, and it
|
|
15
|
+
* cannot exist in a browser at all. Nor is there a `width` or `theme`: those
|
|
16
|
+
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import { Style } from "../core/style.js";
|
|
20
|
+
import { getStyle } from "../core/protocol.js";
|
|
21
|
+
function parseStack(error) {
|
|
22
|
+
const stack = error.stack ?? "";
|
|
23
|
+
const lines = stack.split("\n");
|
|
24
|
+
const frames = [];
|
|
25
|
+
for (const line of lines) {
|
|
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());
|
|
32
|
+
if (match) {
|
|
33
|
+
frames.push({
|
|
34
|
+
function: match[1] || undefined,
|
|
35
|
+
file: match[2],
|
|
36
|
+
line: parseInt(match[3], 10),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return frames;
|
|
41
|
+
}
|
|
42
|
+
export class Traceback {
|
|
43
|
+
error;
|
|
44
|
+
maxFrames;
|
|
45
|
+
suppress;
|
|
46
|
+
constructor(error, options) {
|
|
47
|
+
this.error = error;
|
|
48
|
+
this.maxFrames = options?.maxFrames ?? 100;
|
|
49
|
+
this.suppress = options?.suppress ?? [];
|
|
50
|
+
}
|
|
51
|
+
*render(options) {
|
|
52
|
+
const excTypeStyle = getStyle(options, "traceback.exc_type");
|
|
53
|
+
const textStyle = getStyle(options, "traceback.text");
|
|
54
|
+
// Error type and message
|
|
55
|
+
const errorName = this.error.name || "Error";
|
|
56
|
+
const errorMessage = this.error.message || "";
|
|
57
|
+
yield new Segment(errorName, excTypeStyle);
|
|
58
|
+
yield new Segment(": ");
|
|
59
|
+
yield new Segment(errorMessage, textStyle);
|
|
60
|
+
yield Segment.line();
|
|
61
|
+
yield Segment.line();
|
|
62
|
+
// Stack frames — suppressed frames show file/line only (not removed)
|
|
63
|
+
const frames = parseStack(this.error);
|
|
64
|
+
const filteredFrames = this.suppress.length > 0
|
|
65
|
+
? frames.map((f) => ({
|
|
66
|
+
...f,
|
|
67
|
+
suppressed: this.suppress.some((s) => f.file.includes(s)),
|
|
68
|
+
}))
|
|
69
|
+
: frames;
|
|
70
|
+
let displayFrames = filteredFrames;
|
|
71
|
+
if (this.maxFrames > 0 && displayFrames.length > this.maxFrames) {
|
|
72
|
+
// Spend the budget exactly: the tail takes `maxFrames - head` rather than
|
|
73
|
+
// a second `head`, so an odd budget shows every frame it counts, and it
|
|
74
|
+
// slices from an index because `slice(-0)` is the whole array.
|
|
75
|
+
const head = Math.floor(this.maxFrames / 2);
|
|
76
|
+
const first = displayFrames.slice(0, head);
|
|
77
|
+
const last = displayFrames.slice(displayFrames.length - (this.maxFrames - head));
|
|
78
|
+
const omitted = displayFrames.length - this.maxFrames;
|
|
79
|
+
for (const frame of first) {
|
|
80
|
+
yield* this._renderFrame(frame, options);
|
|
81
|
+
}
|
|
82
|
+
yield new Segment(` ... ${omitted} frames omitted ...`, textStyle);
|
|
83
|
+
yield Segment.line();
|
|
84
|
+
for (const frame of last) {
|
|
85
|
+
yield* this._renderFrame(frame, options);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (const frame of displayFrames) {
|
|
90
|
+
yield* this._renderFrame(frame, options);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
*_renderFrame(frame, options) {
|
|
95
|
+
const pathStyle = Style.parse("dim");
|
|
96
|
+
const lineNoStyle = getStyle(options, "traceback.offset");
|
|
97
|
+
yield new Segment(" ");
|
|
98
|
+
// Spec: suppressed frames show file and line only — no function name
|
|
99
|
+
if (frame.function && !frame.suppressed) {
|
|
100
|
+
yield new Segment(frame.function, Style.parse("bold"));
|
|
101
|
+
yield new Segment(" ");
|
|
102
|
+
}
|
|
103
|
+
yield new Segment(frame.file, pathStyle);
|
|
104
|
+
yield new Segment(":");
|
|
105
|
+
yield new Segment(String(frame.line), lineNoStyle);
|
|
106
|
+
yield Segment.line();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree — hierarchical view with guide lines.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "../core/segment.js";
|
|
5
|
+
import { Style } from "../core/style.js";
|
|
6
|
+
import { RichText } from "../core/text.js";
|
|
7
|
+
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
8
|
+
export interface TreeOptions {
|
|
9
|
+
expanded?: boolean;
|
|
10
|
+
hideRoot?: boolean;
|
|
11
|
+
guide_style?: string | Style;
|
|
12
|
+
style?: string | Style;
|
|
13
|
+
}
|
|
14
|
+
export declare class Tree implements Renderable, Measurable {
|
|
15
|
+
readonly label: Renderable;
|
|
16
|
+
readonly children: Tree[];
|
|
17
|
+
expanded: boolean;
|
|
18
|
+
readonly hideRoot: boolean;
|
|
19
|
+
readonly guideStyle: string | Style;
|
|
20
|
+
readonly style: string | Style;
|
|
21
|
+
constructor(label: string | RichText | Renderable, options?: TreeOptions);
|
|
22
|
+
add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
|
|
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;
|
|
31
|
+
/**
|
|
32
|
+
* The tree walked into the rows it emits, one row per label, guides first.
|
|
33
|
+
*
|
|
34
|
+
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
35
|
+
* leads them is here and only here. `render` turns each row into segments and
|
|
36
|
+
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
37
|
+
* come to disagree about how many rows there are or how wide the guides on
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
private _rows;
|
|
45
|
+
/**
|
|
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.
|
|
48
|
+
*
|
|
49
|
+
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
50
|
+
* Before this, each call site emitted its guides and then handed the label
|
|
51
|
+
* `options` unchanged — the label was told it had the whole outer width while
|
|
52
|
+
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
53
|
+
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
54
|
+
* and the same fix: divide the requested width once and let every part read
|
|
55
|
+
* its share off the division. `options` arrives parsed from `render`.
|
|
56
|
+
*
|
|
57
|
+
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
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.
|
|
62
|
+
*/
|
|
63
|
+
private _renderRow;
|
|
64
|
+
measure(options: RenderOptions): {
|
|
65
|
+
minimum: number;
|
|
66
|
+
maximum: number;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree — hierarchical view with guide lines.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "../core/segment.js";
|
|
5
|
+
import { cellFit, cellLen, asCellCol } from "../core/cells.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
|
+
import { Measurement } from "../core/measure.js";
|
|
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;
|
|
19
|
+
/**
|
|
20
|
+
* How wide a label wants to be. A label with no `measure` cannot say, and the
|
|
21
|
+
* offer is the honest stand-in — including when the offer is unbounded, which
|
|
22
|
+
* is where `withBoundedWidth` reports it rather than guessing a number.
|
|
23
|
+
*/
|
|
24
|
+
function labelWidth(options, label) {
|
|
25
|
+
if (!isMeasurable(label))
|
|
26
|
+
return options.maxWidth;
|
|
27
|
+
return Measurement.get(options, label).maximum;
|
|
28
|
+
}
|
|
29
|
+
export class Tree {
|
|
30
|
+
label;
|
|
31
|
+
children;
|
|
32
|
+
expanded;
|
|
33
|
+
hideRoot;
|
|
34
|
+
guideStyle;
|
|
35
|
+
style;
|
|
36
|
+
constructor(label, options) {
|
|
37
|
+
this.label = embed(label);
|
|
38
|
+
this.children = [];
|
|
39
|
+
this.expanded = options?.expanded !== false;
|
|
40
|
+
this.hideRoot = options?.hideRoot ?? false;
|
|
41
|
+
this.guideStyle = options?.guide_style ?? NULL_STYLE;
|
|
42
|
+
this.style = options?.style ?? NULL_STYLE;
|
|
43
|
+
}
|
|
44
|
+
add(label, options) {
|
|
45
|
+
const child = new Tree(label, {
|
|
46
|
+
guide_style: options?.guide_style,
|
|
47
|
+
style: options?.style,
|
|
48
|
+
expanded: options?.expanded,
|
|
49
|
+
});
|
|
50
|
+
this.children.push(child);
|
|
51
|
+
return child;
|
|
52
|
+
}
|
|
53
|
+
*render(rawOptions) {
|
|
54
|
+
const options = withBoundedWidth(rawOptions, this);
|
|
55
|
+
for (const row of this._walk(options)) {
|
|
56
|
+
yield* this._renderRow(options, row);
|
|
57
|
+
}
|
|
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
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The tree walked into the rows it emits, one row per label, guides first.
|
|
71
|
+
*
|
|
72
|
+
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
73
|
+
* leads them is here and only here. `render` turns each row into segments and
|
|
74
|
+
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
75
|
+
* come to disagree about how many rows there are or how wide the guides on
|
|
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.
|
|
81
|
+
*/
|
|
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);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
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.
|
|
95
|
+
*
|
|
96
|
+
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
97
|
+
* Before this, each call site emitted its guides and then handed the label
|
|
98
|
+
* `options` unchanged — the label was told it had the whole outer width while
|
|
99
|
+
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
100
|
+
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
101
|
+
* and the same fix: divide the requested width once and let every part read
|
|
102
|
+
* its share off the division. `options` arrives parsed from `render`.
|
|
103
|
+
*
|
|
104
|
+
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
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.
|
|
109
|
+
*/
|
|
110
|
+
*_renderRow(options, row) {
|
|
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();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
measure(options) {
|
|
132
|
+
// The ceiling is parsed for the same reason the rows are: an unparsed NaN
|
|
133
|
+
// ceiling yields a range no parent layout can divide.
|
|
134
|
+
const parsed = withCellWidth(options);
|
|
135
|
+
const ceiling = parsed.maxWidth;
|
|
136
|
+
// The widest row the walk emits, not the offer. Reported as the offer, a
|
|
137
|
+
// tree told any parent that asked "I want all of it" — so `Panel` in fit
|
|
138
|
+
// mode drew a 40-cell frame around nine cells of tree, and an unbounded
|
|
139
|
+
// offer came back unbounded.
|
|
140
|
+
let natural = 0;
|
|
141
|
+
for (const row of this._walk(parsed)) {
|
|
142
|
+
const guideWidth = row.guides.reduce((sum, guide) => sum + cellLen(guide.first), 0);
|
|
143
|
+
natural = Math.max(natural, guideWidth + labelWidth(parsed, row.label));
|
|
144
|
+
}
|
|
145
|
+
const maximum = Math.min(natural, ceiling);
|
|
146
|
+
// A row is its guides plus at least one cell of label — but never more than
|
|
147
|
+
// this tree turned out to want, or the floor would sit above its own ceiling.
|
|
148
|
+
return { minimum: Math.min(4, maximum), maximum };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -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
|
+
}
|