@promptctl/rich-js 0.18.1 → 0.19.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 +9 -7
- 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 +98 -18
- package/dist/core/color.js +331 -144
- 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/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 +150 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +105 -2
- package/dist/core/pretty.js +527 -97
- package/dist/core/protocol.d.ts +41 -3
- package/dist/core/protocol.js +20 -0
- 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/strip.d.ts +13 -2
- package/dist/core/strip.js +44 -23
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +97 -34
- 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 +31 -19
- package/dist/host/app.js +90 -60
- package/dist/host/index.d.ts +2 -1
- package/dist/host/terminal-host.d.ts +3 -2
- package/dist/index.d.ts +17 -10
- package/dist/index.js +11 -4
- 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 +3 -2
- 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 +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +87 -30
- package/dist/renderables/embed.js +134 -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/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 +104 -16
- package/dist/renderables/progress.js +205 -81
- package/dist/renderables/progressBar.d.ts +2 -2
- package/dist/renderables/progressBar.js +30 -19
- 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 +1 -0
- package/dist/renderables/rule.js +44 -48
- 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 +370 -192
- 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.js +2 -4
- 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 +14 -48
- 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/widget-app.d.ts +2 -0
- package/dist/widgets/widget-app.js +13 -6
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +11 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Traceback — renders error tracebacks with formatting.
|
|
3
3
|
*
|
|
4
|
-
* [LAW:effects-at-boundaries] Pure rendering:
|
|
4
|
+
* [LAW:effects-at-boundaries] Pure rendering: a caught value in, `Segment`s out.
|
|
5
5
|
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
6
|
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
7
|
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
@@ -22,10 +22,11 @@ export interface TracebackOptions {
|
|
|
22
22
|
maxFrames?: number;
|
|
23
23
|
}
|
|
24
24
|
export declare class Traceback implements Renderable {
|
|
25
|
-
|
|
25
|
+
/** What was caught, as it was handed in. */
|
|
26
|
+
readonly error: unknown;
|
|
26
27
|
readonly maxFrames: number;
|
|
27
28
|
readonly suppress: string[];
|
|
28
|
-
|
|
29
|
+
private readonly caught;
|
|
30
|
+
constructor(error: unknown, options?: TracebackOptions);
|
|
29
31
|
render(options: RenderOptions): Iterable<Segment>;
|
|
30
|
-
private _renderFrame;
|
|
31
32
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Traceback — renders error tracebacks with formatting.
|
|
3
3
|
*
|
|
4
|
-
* [LAW:effects-at-boundaries] Pure rendering:
|
|
4
|
+
* [LAW:effects-at-boundaries] Pure rendering: a caught value in, `Segment`s out.
|
|
5
5
|
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
6
|
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
7
|
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
@@ -16,93 +16,264 @@
|
|
|
16
16
|
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
17
|
*/
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
import { RichText } from "../core/text.js";
|
|
20
|
+
import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
|
|
21
|
+
import { layoutPadding } from "./padding.js";
|
|
22
|
+
/**
|
|
23
|
+
* [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
|
|
24
|
+
* JavaScript lets a program throw any value — and the report. Anything with a
|
|
25
|
+
* string `name` and a `message` is reported as the error it presents itself
|
|
26
|
+
* as: that is every native error from any realm, which `instanceof Error`
|
|
27
|
+
* would refuse for one from a `node:vm` context or an iframe, and any thrower
|
|
28
|
+
* shaped like one. A message that is not a string — a native error's, assigned
|
|
29
|
+
* after construction — is formatted by `Pretty` under the error's own name.
|
|
30
|
+
* Anything else is reported as the value it is, under `NonError`, formatted by
|
|
31
|
+
* `Pretty` so it reads the same in a browser as in node — with the frames of a
|
|
32
|
+
* stack it carries, when it carries one.
|
|
33
|
+
*/
|
|
34
|
+
function readCaught(value) {
|
|
35
|
+
const fields = readFields(value);
|
|
36
|
+
if (fields !== undefined && typeof fields.name === "string" && fields.hasMessage) {
|
|
37
|
+
const { name, message } = fields;
|
|
38
|
+
const text = typeof message === "string" ? message : undefined;
|
|
39
|
+
return {
|
|
40
|
+
name: name || "Error",
|
|
41
|
+
message: text === undefined
|
|
42
|
+
? prettyMessage(message)
|
|
43
|
+
: () => new RichText(text, { style: "traceback.text", end: "" }),
|
|
44
|
+
frames: parseStack(fields.stack, name, text),
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
|
-
return frames;
|
|
47
|
+
return { name: "NonError", message: prettyMessage(value), frames: parseStack(fields?.stack ?? "", undefined, undefined) };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The fields the report reads off a caught object, or `undefined` when it is
|
|
51
|
+
* not an object or reading them throws — a getter that throws, a `Proxy` whose
|
|
52
|
+
* trap does. Such a value is reported under `NonError`, where `Pretty` reads
|
|
53
|
+
* it again and marks the read that threw as `[Threw: …]`, so the fault stays
|
|
54
|
+
* in the report instead of taking the crash handler down with it.
|
|
55
|
+
*/
|
|
56
|
+
function readFields(value) {
|
|
57
|
+
if (typeof value !== "object" || value === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const record = value;
|
|
61
|
+
const stack = record.stack;
|
|
62
|
+
return {
|
|
63
|
+
name: record.name,
|
|
64
|
+
hasMessage: "message" in record,
|
|
65
|
+
message: record.message,
|
|
66
|
+
stack: typeof stack === "string" ? stack : "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function prettyMessage(value) {
|
|
74
|
+
const pretty = new Pretty(value, UNSEEN_DATA_BOUNDS);
|
|
75
|
+
return (options) => pretty.toText(options);
|
|
76
|
+
}
|
|
77
|
+
/** The first line V8 writes into an error's stack: `String(error)` at the throw. */
|
|
78
|
+
function header(name, message) {
|
|
79
|
+
if (!name)
|
|
80
|
+
return message;
|
|
81
|
+
if (!message)
|
|
82
|
+
return name;
|
|
83
|
+
return `${name}: ${message}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Where the header that opens an error's stack ends, or `undefined` when the
|
|
87
|
+
* stack does not open with the header the error now has.
|
|
88
|
+
*
|
|
89
|
+
* V8 writes `String(error)`; node writes its own errors' code into the name
|
|
90
|
+
* there (`RangeError [ERR_X]: message`), so after the name may come one line of
|
|
91
|
+
* anything before `: ` and the message.
|
|
92
|
+
*/
|
|
93
|
+
function headerEnd(stack, name, message) {
|
|
94
|
+
const endsLine = (at) => at === stack.length || stack[at] === "\n";
|
|
95
|
+
const plain = header(name, message);
|
|
96
|
+
if (stack.startsWith(plain) && endsLine(plain.length))
|
|
97
|
+
return plain.length;
|
|
98
|
+
if (!name || !stack.startsWith(name))
|
|
99
|
+
return undefined;
|
|
100
|
+
const tail = `: ${message}`;
|
|
101
|
+
const at = stack.indexOf(tail, name.length);
|
|
102
|
+
if (at < 0 || stack.slice(name.length, at).includes("\n"))
|
|
103
|
+
return undefined;
|
|
104
|
+
return endsLine(at + tail.length) ? at + tail.length : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether a stack line is shaped as V8's header — a name, then `:` or nothing —
|
|
108
|
+
* rather than as a SpiderMonkey or JavaScriptCore frame, whose `@` comes before
|
|
109
|
+
* the first `:` of its location. Read by shape, not by the error's name,
|
|
110
|
+
* because the name may have been reassigned after V8 wrote the header too.
|
|
111
|
+
*/
|
|
112
|
+
function isHeaderShaped(line) {
|
|
113
|
+
const colon = line.indexOf(":");
|
|
114
|
+
return !(colon < 0 ? line : line.slice(0, colon)).includes("@");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The frames of a stack, in order.
|
|
118
|
+
*
|
|
119
|
+
* V8 opens a stack with the error's header, and a message can span lines that
|
|
120
|
+
* look like frames, so the header comes off first, as the exact text it is.
|
|
121
|
+
* When it does not open the stack, the message was reassigned after the throw
|
|
122
|
+
* and what precedes the first V8 frame is that stale header — all of it, when
|
|
123
|
+
* V8 recorded no frame, as under `Error.stackTraceLimit = 0`. SpiderMonkey and
|
|
124
|
+
* JavaScriptCore write no header, so a stack that opens with one of their
|
|
125
|
+
* frames is all frames. A value that is not an error has no name, so its stack is
|
|
126
|
+
* read from its first V8 frame, or whole when it has none.
|
|
127
|
+
*/
|
|
128
|
+
function parseStack(stack, name, message) {
|
|
129
|
+
const end = name !== undefined && message !== undefined ? headerEnd(stack, name, message) : undefined;
|
|
130
|
+
const lines = stack.slice(end ?? 0).split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
131
|
+
if (end !== undefined)
|
|
132
|
+
return lines.map(parseFrame);
|
|
133
|
+
const firstV8 = lines.findIndex((line) => V8_FRAME.test(line));
|
|
134
|
+
if (firstV8 >= 0)
|
|
135
|
+
return lines.slice(firstV8).map(parseFrame);
|
|
136
|
+
return name !== undefined && lines[0] !== undefined && isHeaderShaped(lines[0]) ? [] : lines.map(parseFrame);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* V8 writes `at [async] [fn (]location[)]`. The `async` marker is not part of
|
|
140
|
+
* any name, so it is read and dropped; the lookahead keeps `at async (…)` — a
|
|
141
|
+
* function named `async` — a name.
|
|
142
|
+
*/
|
|
143
|
+
const V8_FRAME = /^at\s+(?:async\s+(?!\())?(.*)$/;
|
|
144
|
+
/**
|
|
145
|
+
* SpiderMonkey and JavaScriptCore write `[fn]@location`, Firefox prefixing an
|
|
146
|
+
* awaited call's name with a cause up to a `*` (`async*`, `promise callback*`),
|
|
147
|
+
* which is dropped as V8's `async` is.
|
|
148
|
+
*/
|
|
149
|
+
const AT_SIGN_FRAME = /^(?:[^@]*\*)?([^@]*)@(.*)$/;
|
|
150
|
+
function parseFrame(text) {
|
|
151
|
+
const v8 = V8_FRAME.exec(text);
|
|
152
|
+
if (v8) {
|
|
153
|
+
const body = v8[1];
|
|
154
|
+
const open = openingParen(body);
|
|
155
|
+
return open > 0
|
|
156
|
+
? locate(body.slice(0, open).trim() || undefined, body.slice(open + 1, -1))
|
|
157
|
+
: locate(undefined, body);
|
|
158
|
+
}
|
|
159
|
+
const atSign = AT_SIGN_FRAME.exec(text);
|
|
160
|
+
if (atSign)
|
|
161
|
+
return locate(atSign[1] || undefined, atSign[2]);
|
|
162
|
+
return { function: undefined, location: text, line: undefined };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Where the parenthesised location that closes a V8 frame opens, or -1 when
|
|
166
|
+
* the frame does not end in one. Matched from the end, because an eval
|
|
167
|
+
* frame's location nests parentheses: `eval (eval at f (file:8:22), …)`.
|
|
168
|
+
*/
|
|
169
|
+
function openingParen(body) {
|
|
170
|
+
if (!body.endsWith(")"))
|
|
171
|
+
return -1;
|
|
172
|
+
let depth = 0;
|
|
173
|
+
for (let i = body.length - 1; i >= 0; i--) {
|
|
174
|
+
if (body[i] === ")")
|
|
175
|
+
depth++;
|
|
176
|
+
else if (body[i] === "(" && --depth === 0)
|
|
177
|
+
return i;
|
|
178
|
+
}
|
|
179
|
+
return -1;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The shapes a location takes, each read as its file and line, first match
|
|
183
|
+
* wins. Code run by `eval` or `new Function` has no file of its own, so its
|
|
184
|
+
* frame is placed at the call that ran it, which is where the source is: V8
|
|
185
|
+
* nests that site inside the location (`eval at f (file:8:22), <anonymous>:1:1`,
|
|
186
|
+
* innermost first-parenthesised for an eval inside an eval), and Firefox appends
|
|
187
|
+
* the code to it (`file line 8 > eval:1:1`).
|
|
188
|
+
*/
|
|
189
|
+
const LOCATIONS = [
|
|
190
|
+
/^eval at [^]*?\(([^()]+):(\d+):\d+\)/,
|
|
191
|
+
/^(.*?) line (\d+) > (?:eval|Function)/,
|
|
192
|
+
/^(.*?):(\d+)(?::\d+)?$/,
|
|
193
|
+
];
|
|
194
|
+
function locate(fn, location) {
|
|
195
|
+
for (const shape of LOCATIONS) {
|
|
196
|
+
const match = shape.exec(location);
|
|
197
|
+
if (match)
|
|
198
|
+
return { function: fn, location: match[1], line: parseInt(match[2], 10) };
|
|
199
|
+
}
|
|
200
|
+
return { function: fn, location, line: undefined };
|
|
41
201
|
}
|
|
42
202
|
export class Traceback {
|
|
203
|
+
/** What was caught, as it was handed in. */
|
|
43
204
|
error;
|
|
44
205
|
maxFrames;
|
|
45
206
|
suppress;
|
|
207
|
+
caught;
|
|
46
208
|
constructor(error, options) {
|
|
47
209
|
this.error = error;
|
|
210
|
+
this.caught = readCaught(error);
|
|
48
211
|
this.maxFrames = options?.maxFrames ?? 100;
|
|
49
212
|
this.suppress = options?.suppress ?? [];
|
|
50
213
|
}
|
|
214
|
+
// [LAW:single-enforcer] The report is `RichText`, so fitting it to the width
|
|
215
|
+
// is the one fitting every text in the library gets: a line wider than the
|
|
216
|
+
// report wraps, and a location wider than the report folds across lines
|
|
217
|
+
// whole, as Rich's does. Written as bare segments, each line was cropped at
|
|
218
|
+
// the edge instead, and a frame lost its file and line to a long path.
|
|
51
219
|
*render(options) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
yield
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
const frames =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
: frames;
|
|
70
|
-
let displayFrames = filteredFrames;
|
|
71
|
-
if (this.maxFrames > 0 && displayFrames.length > this.maxFrames) {
|
|
220
|
+
const header = new RichText("", { end: "" });
|
|
221
|
+
const { name, message } = this.caught;
|
|
222
|
+
header.append(name, "traceback.exc_type");
|
|
223
|
+
header.append(": ");
|
|
224
|
+
// The message starts after the name, so a value `Pretty` lays out fits the
|
|
225
|
+
// width that is left on that line rather than overrunning it.
|
|
226
|
+
header.append(message({ ...options, maxWidth: Math.max(1, options.maxWidth - header.cellLength) }));
|
|
227
|
+
header.append("\n\n");
|
|
228
|
+
yield* header.render(options);
|
|
229
|
+
// A suppressed frame keeps its place and shows its file and line only: it
|
|
230
|
+
// is the same frame with no function name to show.
|
|
231
|
+
const frames = this.caught.frames.map((frame) => this.suppress.some((s) => frame.location.includes(s)) ? { ...frame, function: undefined } : frame);
|
|
232
|
+
// One entry per row of the frame block, so a report with no frames has no
|
|
233
|
+
// rows: empty text is one line, as in Rich, and a block built as one text
|
|
234
|
+
// drew a blank indented row under every frameless report.
|
|
235
|
+
let entries;
|
|
236
|
+
if (this.maxFrames > 0 && frames.length > this.maxFrames) {
|
|
72
237
|
// Spend the budget exactly: the tail takes `maxFrames - head` rather than
|
|
73
238
|
// a second `head`, so an odd budget shows every frame it counts, and it
|
|
74
239
|
// slices from an index because `slice(-0)` is the whole array.
|
|
75
240
|
const head = Math.floor(this.maxFrames / 2);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
}
|
|
241
|
+
const omitted = frames.length - this.maxFrames;
|
|
242
|
+
entries = [
|
|
243
|
+
...frames.slice(0, head).map(frameEntry),
|
|
244
|
+
new RichText(`... ${omitted} frames omitted ...`, { style: "traceback.text", end: "" }),
|
|
245
|
+
...frames.slice(frames.length - (this.maxFrames - head)).map(frameEntry),
|
|
246
|
+
];
|
|
87
247
|
}
|
|
88
248
|
else {
|
|
89
|
-
|
|
90
|
-
|
|
249
|
+
entries = frames.map(frameEntry);
|
|
250
|
+
}
|
|
251
|
+
// The frames are one block set in from the report's edge, so a frame that
|
|
252
|
+
// wraps continues under itself instead of at the edge, where it read as a
|
|
253
|
+
// frame of its own. The width is divided as `Padding` divides it; the rows
|
|
254
|
+
// are not padded out on the right, as `Padding`'s are, because trailing
|
|
255
|
+
// blanks on every line of a crash report are bytes nobody asked for.
|
|
256
|
+
const { left, contentWidth } = layoutPadding(options.maxWidth, 2, 0);
|
|
257
|
+
const indent = new Segment(" ".repeat(left));
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
for (const line of Segment.splitLines(entry.render({ ...options, maxWidth: contentWidth }))) {
|
|
260
|
+
yield indent;
|
|
261
|
+
yield* line;
|
|
262
|
+
yield Segment.line();
|
|
91
263
|
}
|
|
92
264
|
}
|
|
93
265
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
yield new Segment(String(frame.line), lineNoStyle);
|
|
106
|
-
yield Segment.line();
|
|
266
|
+
}
|
|
267
|
+
function frameEntry(frame) {
|
|
268
|
+
const entry = new RichText("", { end: "" });
|
|
269
|
+
if (frame.function) {
|
|
270
|
+
entry.append(frame.function, "bold");
|
|
271
|
+
entry.append(" ");
|
|
272
|
+
}
|
|
273
|
+
entry.append(frame.location, "dim");
|
|
274
|
+
if (frame.line !== undefined) {
|
|
275
|
+
entry.append(":");
|
|
276
|
+
entry.append(String(frame.line), "traceback.offset");
|
|
107
277
|
}
|
|
278
|
+
return entry;
|
|
108
279
|
}
|
|
@@ -39,7 +39,9 @@ export declare class Tree implements Renderable, Measurable {
|
|
|
39
39
|
*
|
|
40
40
|
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
41
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.
|
|
42
|
+
* branch of their own, so no depth is drawn by a rule of its own. A node's
|
|
43
|
+
* style and its guide style are handed down the same way, as one more entry
|
|
44
|
+
* on the stack its ancestors built.
|
|
43
45
|
*/
|
|
44
46
|
private _rows;
|
|
45
47
|
/**
|
|
@@ -58,7 +60,13 @@ export declare class Tree implements Renderable, Measurable {
|
|
|
58
60
|
* depending on label renderables to invent trailing newlines: the label's
|
|
59
61
|
* output is cut into lines and every line ends where Tree ends it. A label
|
|
60
62
|
* that emits nothing still holds its row, and one that ends in a newline of
|
|
61
|
-
* its own
|
|
63
|
+
* its own gets the blank row that line is, as in Rich — padded to the label's
|
|
64
|
+
* width when the label is justified.
|
|
65
|
+
*
|
|
66
|
+
* The node's style sits under the label's own, as Rich's `Styled` puts it, so
|
|
67
|
+
* markup in the label still wins; its background alone reaches under the
|
|
68
|
+
* guides, so a row with a background is filled from its first guide to the
|
|
69
|
+
* end of its label.
|
|
62
70
|
*/
|
|
63
71
|
private _renderRow;
|
|
64
72
|
measure(options: RenderOptions): {
|
package/dist/renderables/tree.js
CHANGED
|
@@ -5,25 +5,31 @@ import { Segment } from "../core/segment.js";
|
|
|
5
5
|
import { cellFit, cellLen, asCellCol } from "../core/cells.js";
|
|
6
6
|
import { Style, NULL_STYLE } from "../core/style.js";
|
|
7
7
|
import { embed } from "./embed.js";
|
|
8
|
-
import { drawable, getStyle,
|
|
8
|
+
import { drawable, getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
9
|
import { Measurement } from "../core/measure.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
end: { first: "`-- ", rest: " " },
|
|
17
|
-
};
|
|
18
|
-
const guideGlyphs = ({ fork, end }) => fork.first + fork.rest + end.first + end.rest;
|
|
10
|
+
/** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
|
|
11
|
+
const GUIDE_CELLS = 4;
|
|
12
|
+
const LIGHT_GUIDES = { fork: "├── ", end: "└── ", continue: "│ ", space: " " };
|
|
13
|
+
const HEAVY_GUIDES = { fork: "┣━━ ", end: "┗━━ ", continue: "┃ ", space: " " };
|
|
14
|
+
const DOUBLE_GUIDES = { fork: "╠══ ", end: "╚══ ", continue: "║ ", space: " " };
|
|
15
|
+
const ASCII_GUIDES = { fork: "+-- ", end: "`-- ", continue: "| ", space: " " };
|
|
19
16
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* is where `withBoundedWidth` reports it rather than guessing a number.
|
|
17
|
+
* The guide attributes a glyph set stands for, switched off on the guide itself
|
|
18
|
+
* as Rich's `remove_guide_styles` does: a bold guide is drawn heavy, not bold.
|
|
23
19
|
*/
|
|
20
|
+
const REMOVE_GUIDE_STYLES = new Style({ bold: false, underline2: false });
|
|
21
|
+
const guideGlyphs = (glyphs) => Object.values(glyphs).join("");
|
|
22
|
+
/**
|
|
23
|
+
* The glyphs a column of guide is drawn with, read off its resolved guide style
|
|
24
|
+
* as Rich does: heavy when bold, double when underline2, light otherwise — and
|
|
25
|
+
* ASCII whenever the terminal cannot draw the chosen set.
|
|
26
|
+
*/
|
|
27
|
+
function glyphsFor(options, style) {
|
|
28
|
+
const weight = style.bold === true ? HEAVY_GUIDES : style.underline2 === true ? DOUBLE_GUIDES : LIGHT_GUIDES;
|
|
29
|
+
return drawable(options, weight, ASCII_GUIDES, guideGlyphs);
|
|
30
|
+
}
|
|
31
|
+
/** How wide a label wants to be. */
|
|
24
32
|
function labelWidth(options, label) {
|
|
25
|
-
if (!isMeasurable(label))
|
|
26
|
-
return options.maxWidth;
|
|
27
33
|
return Measurement.get(options, label).maximum;
|
|
28
34
|
}
|
|
29
35
|
export class Tree {
|
|
@@ -52,7 +58,7 @@ export class Tree {
|
|
|
52
58
|
}
|
|
53
59
|
*render(rawOptions) {
|
|
54
60
|
const options = withBoundedWidth(rawOptions, this);
|
|
55
|
-
for (const row of this._walk(
|
|
61
|
+
for (const row of this._walk()) {
|
|
56
62
|
yield* this._renderRow(options, row);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
@@ -62,8 +68,8 @@ export class Tree {
|
|
|
62
68
|
* and the column its children hang from with it, as Rich does — its children
|
|
63
69
|
* stand at the left edge, and its guide style still reaches their guides.
|
|
64
70
|
*/
|
|
65
|
-
_walk(
|
|
66
|
-
const rows = [...this._rows(
|
|
71
|
+
_walk() {
|
|
72
|
+
const rows = [...this._rows([], [], [])];
|
|
67
73
|
return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
@@ -77,16 +83,21 @@ export class Tree {
|
|
|
77
83
|
*
|
|
78
84
|
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
79
85
|
* 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.
|
|
86
|
+
* branch of their own, so no depth is drawn by a rule of its own. A node's
|
|
87
|
+
* style and its guide style are handed down the same way, as one more entry
|
|
88
|
+
* on the stack its ancestors built.
|
|
81
89
|
*/
|
|
82
|
-
*_rows(
|
|
83
|
-
|
|
90
|
+
*_rows(guides, inheritedGuideStyles, inheritedStyles) {
|
|
91
|
+
const styles = [...inheritedStyles, this.style];
|
|
92
|
+
yield { guides, label: this.label, styles };
|
|
84
93
|
const children = this.expanded ? this.children : [];
|
|
85
|
-
const
|
|
94
|
+
const guideStyles = [...inheritedGuideStyles, this.guideStyle];
|
|
86
95
|
const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
|
|
87
96
|
for (let i = 0; i < children.length; i++) {
|
|
88
|
-
const branch = i === children.length - 1
|
|
89
|
-
|
|
97
|
+
const branch = i === children.length - 1
|
|
98
|
+
? { first: "end", rest: "space", styles: guideStyles }
|
|
99
|
+
: { first: "fork", rest: "continue", styles: guideStyles };
|
|
100
|
+
yield* children[i]._rows([...above, branch], guideStyles, styles);
|
|
90
101
|
}
|
|
91
102
|
}
|
|
92
103
|
/**
|
|
@@ -105,23 +116,40 @@ export class Tree {
|
|
|
105
116
|
* depending on label renderables to invent trailing newlines: the label's
|
|
106
117
|
* output is cut into lines and every line ends where Tree ends it. A label
|
|
107
118
|
* that emits nothing still holds its row, and one that ends in a newline of
|
|
108
|
-
* its own
|
|
119
|
+
* its own gets the blank row that line is, as in Rich — padded to the label's
|
|
120
|
+
* width when the label is justified.
|
|
121
|
+
*
|
|
122
|
+
* The node's style sits under the label's own, as Rich's `Styled` puts it, so
|
|
123
|
+
* markup in the label still wins; its background alone reaches under the
|
|
124
|
+
* guides, so a row with a background is filled from its first guide to the
|
|
125
|
+
* end of its label.
|
|
109
126
|
*/
|
|
110
127
|
*_renderRow(options, row) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
128
|
+
const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
|
|
129
|
+
const style = resolve(row.styles);
|
|
130
|
+
const drawn = row.guides.map((guide) => {
|
|
131
|
+
const guideStyle = resolve(guide.styles);
|
|
132
|
+
return {
|
|
133
|
+
glyphs: glyphsFor(options, guideStyle),
|
|
134
|
+
style: style.backgroundStyle.add(guideStyle).add(REMOVE_GUIDE_STYLES),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const width = Math.max(0, options.maxWidth - row.guides.length * GUIDE_CELLS);
|
|
113
138
|
// The guides are cropped by `cellFit`; the label is cropped too, so a label
|
|
114
139
|
// that ignores the width it is handed cannot push the row past the offer the
|
|
115
|
-
// guides were fitted into.
|
|
116
|
-
|
|
140
|
+
// guides were fitted into. No highlighter: a label is drawn plain, as
|
|
141
|
+
// Rich's `Tree` hands its labels `highlight=False`.
|
|
142
|
+
const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
|
|
143
|
+
const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(row.label.render(labelOptions), width), style));
|
|
117
144
|
if (lines.length === 0)
|
|
118
145
|
lines.push([]);
|
|
119
146
|
for (let i = 0; i < lines.length; i++) {
|
|
120
147
|
let left = options.maxWidth;
|
|
121
148
|
for (let g = 0; g < row.guides.length; g++) {
|
|
122
|
-
const
|
|
149
|
+
const { glyphs, style: guideStyle } = drawn[g];
|
|
150
|
+
const piece = cellFit(glyphs[i === 0 ? row.guides[g].first : row.guides[g].rest], asCellCol(left));
|
|
123
151
|
if (piece.length > 0)
|
|
124
|
-
yield new Segment(piece,
|
|
152
|
+
yield new Segment(piece, guideStyle);
|
|
125
153
|
left -= cellLen(piece);
|
|
126
154
|
}
|
|
127
155
|
yield* lines[i];
|
|
@@ -138,9 +166,8 @@ export class Tree {
|
|
|
138
166
|
// mode drew a 40-cell frame around nine cells of tree, and an unbounded
|
|
139
167
|
// offer came back unbounded.
|
|
140
168
|
let natural = 0;
|
|
141
|
-
for (const row of this._walk(
|
|
142
|
-
|
|
143
|
-
natural = Math.max(natural, guideWidth + labelWidth(parsed, row.label));
|
|
169
|
+
for (const row of this._walk()) {
|
|
170
|
+
natural = Math.max(natural, row.guides.length * GUIDE_CELLS + labelWidth(parsed, row.label));
|
|
144
171
|
}
|
|
145
172
|
const maximum = Math.min(natural, ceiling);
|
|
146
173
|
// A row is its guides plus at least one cell of label — but never more than
|
|
@@ -35,7 +35,7 @@ import { Segment } from "../core/segment.js";
|
|
|
35
35
|
import { Measurement } from "../core/measure.js";
|
|
36
36
|
import { cellCount, cellLen } from "../core/cells.js";
|
|
37
37
|
import { NULL_STYLE } from "../core/style.js";
|
|
38
|
-
import { drawable, fitHeight, getStyle,
|
|
38
|
+
import { drawable, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
|
|
39
39
|
/** A heavy line for the thumb on a light line for the track. */
|
|
40
40
|
export const SCROLLBAR = {
|
|
41
41
|
thumb: { glyph: "┃", style: "scrollbar.thumb" },
|
|
@@ -155,9 +155,7 @@ export class Viewport {
|
|
|
155
155
|
const options = withCellWidth(rawOptions);
|
|
156
156
|
const gutter = gutterWidth(this.scrollbar);
|
|
157
157
|
const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
|
|
158
|
-
const content =
|
|
159
|
-
? Measurement.get(inner, this.content)
|
|
160
|
-
: new Measurement(Math.min(1, inner.maxWidth), inner.maxWidth);
|
|
158
|
+
const content = Measurement.get(inner, this.content);
|
|
161
159
|
return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
|
|
162
160
|
}
|
|
163
161
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* ### The carrier is a hex string, deliberately
|
|
28
28
|
*
|
|
29
|
-
* A color crosses the template seam as `#RRGGBB
|
|
29
|
+
* A color crosses the template seam as `#RRGGBB` (or `#RRGGBBAA`), not as an opaque object,
|
|
30
30
|
* for three reasons:
|
|
31
31
|
*
|
|
32
32
|
* 1. **The type separates.** `"string"` is the strictest arg slot the template
|
|
@@ -56,12 +56,34 @@
|
|
|
56
56
|
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
57
|
*/
|
|
58
58
|
import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
|
|
59
|
-
import { ColorDepth } from "../core/color.js";
|
|
59
|
+
import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* A colour the colour functions can compute with. A fully transparent one — a
|
|
62
|
+
* theme's "no background", `#00000000` — has no colour of its own: what shows
|
|
63
|
+
* through it is whatever surface it lands on, which a template cannot know.
|
|
64
|
+
* Darkening, mixing or contrasting it would answer for a surface nobody named,
|
|
65
|
+
* so it is refused here, for every colour function and every `ramp` stop.
|
|
66
|
+
* [LAW:single-enforcer]
|
|
63
67
|
*/
|
|
64
|
-
export declare
|
|
68
|
+
export declare function computable(colour: ColorRgba, func: string, written: string): ColorRgba;
|
|
69
|
+
/**
|
|
70
|
+
* Where `readableOn`'s text will be drawn: the depth the output is encoded at,
|
|
71
|
+
* and the terminal that draws it when that is known. At sixteen colours a
|
|
72
|
+
* terminal paints each slot in its own theme's colour, so the floor is measured
|
|
73
|
+
* in `terminal`'s sixteen; without one, in the VGA defaults (the Windows
|
|
74
|
+
* console's at `WINDOWS`). Both are facts about the one place the text lands,
|
|
75
|
+
* so they arrive together.
|
|
76
|
+
*/
|
|
77
|
+
export interface DrawnAt {
|
|
78
|
+
readonly depth: ColorDepth;
|
|
79
|
+
readonly terminal?: TerminalTheme;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The `readableOn` binding, measuring where `drawnAt` says the text lands when
|
|
83
|
+
* it is evaluated — a consumer that renders to a per-call destination
|
|
84
|
+
* registers its own.
|
|
85
|
+
*/
|
|
86
|
+
export declare const readableOnFunc: (drawnAt: () => DrawnAt) => TemplateFunc;
|
|
65
87
|
/**
|
|
66
88
|
* The palette-free color vocabulary: every function takes colors and returns a
|
|
67
89
|
* color, so they compose by nesting.
|
|
@@ -80,8 +102,8 @@ export declare const readableOnFunc: (drawnAt: () => ColorDepth) => TemplateFunc
|
|
|
80
102
|
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
81
103
|
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
82
104
|
*
|
|
83
|
-
* @param drawnAt
|
|
84
|
-
* `readableOn` evaluation so a host that learns
|
|
85
|
-
* one getter; defaults to truecolor.
|
|
105
|
+
* @param drawnAt where the text will be drawn — the depth, and the terminal
|
|
106
|
+
* when known — read on every `readableOn` evaluation so a host that learns
|
|
107
|
+
* its destination per render passes one getter; defaults to truecolor.
|
|
86
108
|
*/
|
|
87
|
-
export declare function colorFuncs(drawnAt?: () =>
|
|
109
|
+
export declare function colorFuncs(drawnAt?: () => DrawnAt): FuncMap;
|