@promptctl/rich-js 0.18.0 → 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/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 +145 -0
- package/dist/core/box.js +372 -0
- package/dist/core/cells.d.ts +192 -0
- package/dist/core/cells.js +334 -0
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +326 -0
- package/dist/core/color.js +1225 -0
- package/dist/core/console.d.ts +196 -0
- package/dist/core/console.js +596 -0
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +197 -0
- package/dist/core/export-lines.d.ts +130 -0
- package/dist/core/export-lines.js +159 -0
- 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 +33 -0
- package/dist/core/highlighter.js +190 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +140 -0
- package/dist/core/markup.js +654 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +67 -0
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -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 +78 -0
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +49 -0
- package/dist/core/pretty.d.ts +247 -0
- package/dist/core/pretty.js +906 -0
- package/dist/core/protocol.d.ts +305 -0
- package/dist/core/protocol.js +183 -0
- package/dist/core/render.d.ts +87 -0
- package/dist/core/render.js +105 -0
- package/dist/core/segment.d.ts +162 -0
- package/dist/core/segment.js +488 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +152 -0
- package/dist/core/strip.js +326 -0
- package/dist/core/style.d.ts +189 -0
- package/dist/core/style.js +758 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +283 -0
- package/dist/core/text.js +1097 -0
- package/dist/core/wrap.d.ts +46 -0
- package/dist/core/wrap.js +140 -0
- package/dist/host/app.d.ts +133 -0
- package/dist/host/app.js +285 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +180 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +114 -0
- package/dist/node/prompt.d.ts +34 -0
- package/dist/node/prompt.js +51 -0
- package/dist/node/save.d.ts +18 -0
- package/dist/node/save.js +24 -0
- package/dist/node/terminal-host.d.ts +114 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +38 -0
- package/dist/node/traceback.js +98 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +26 -0
- package/dist/renderables/columns.d.ts +77 -0
- package/dist/renderables/columns.js +191 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +31 -0
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +107 -0
- package/dist/renderables/embed.js +160 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +14 -0
- package/dist/renderables/group.js +22 -0
- package/dist/renderables/layout.d.ts +75 -0
- package/dist/renderables/layout.js +223 -0
- package/dist/renderables/live.d.ts +74 -0
- package/dist/renderables/live.js +188 -0
- package/dist/renderables/markdown.d.ts +38 -0
- package/dist/renderables/markdown.js +587 -0
- package/dist/renderables/padding.d.ts +59 -0
- package/dist/renderables/padding.js +147 -0
- package/dist/renderables/panel.d.ts +114 -0
- package/dist/renderables/panel.js +303 -0
- package/dist/renderables/progress.d.ts +178 -0
- package/dist/renderables/progress.js +362 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +64 -0
- package/dist/renderables/prompt.d.ts +71 -0
- package/dist/renderables/prompt.js +165 -0
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +26 -0
- package/dist/renderables/rule.js +86 -0
- package/dist/renderables/spinner.d.ts +57 -0
- package/dist/renderables/spinner.js +103 -0
- package/dist/renderables/status.d.ts +29 -0
- package/dist/renderables/status.js +46 -0
- package/dist/renderables/syntax.d.ts +65 -0
- package/dist/renderables/syntax.js +166 -0
- package/dist/renderables/table.d.ts +232 -0
- package/dist/renderables/table.js +973 -0
- package/dist/renderables/traceback.d.ts +32 -0
- package/dist/renderables/traceback.js +279 -0
- package/dist/renderables/tree.d.ts +76 -0
- package/dist/renderables/tree.js +177 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +196 -0
- package/dist/template-bindings/color-funcs.d.ts +109 -0
- package/dist/template-bindings/color-funcs.js +243 -0
- package/dist/template-bindings/helpers.d.ts +26 -0
- package/dist/template-bindings/helpers.js +33 -0
- package/dist/template-bindings/index.d.ts +119 -0
- package/dist/template-bindings/index.js +162 -0
- package/dist/template-bindings/palette-funcs.d.ts +96 -0
- package/dist/template-bindings/palette-funcs.js +145 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +157 -0
- package/dist/themes/buildPalette.d.ts +57 -0
- package/dist/themes/buildPalette.js +100 -0
- package/dist/themes/colorMath.d.ts +94 -0
- package/dist/themes/colorMath.js +280 -0
- package/dist/themes/colorRef.d.ts +36 -0
- package/dist/themes/colorRef.js +78 -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 +45 -0
- package/dist/themes/palette.js +71 -0
- package/dist/themes/ramp.d.ts +70 -0
- package/dist/themes/ramp.js +103 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +105 -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 +45 -0
- package/dist/widgets/button.js +177 -0
- package/dist/widgets/checkbox.d.ts +38 -0
- package/dist/widgets/checkbox.js +135 -0
- package/dist/widgets/dropdown.d.ts +64 -0
- package/dist/widgets/dropdown.js +416 -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/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +54 -0
- package/dist/widgets/slider.js +247 -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 +283 -0
- package/dist/widgets/text-input.js +1174 -0
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +41 -0
- package/dist/widgets/toggle.js +157 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +65 -0
- package/dist/widgets/widget-app.js +206 -0
- package/dist/widgets/widget-base.d.ts +42 -0
- package/dist/widgets/widget-base.js +172 -0
- package/package.json +16 -2
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console — the central orchestrator for rendering styled output to the terminal.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "./segment.js";
|
|
5
|
+
import { NULL_STYLE, DEFAULT_THEME } from "./style.js";
|
|
6
|
+
import { isDumbTerminal, resolveDestination } from "./color.js";
|
|
7
|
+
import { encodeHtml } from "./export-html.js";
|
|
8
|
+
import { encodeSvg } from "./export-svg.js";
|
|
9
|
+
import { RichText } from "./text.js";
|
|
10
|
+
import { activeHighlighter, renderStr } from "./markup.js";
|
|
11
|
+
import { Pretty, UNSEEN_DATA_BOUNDS, isExpandable } from "./pretty.js";
|
|
12
|
+
import { JSONRenderable } from "./json.js";
|
|
13
|
+
import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
|
|
14
|
+
// [LAW:one-way-deps] exception: one of the two sanctioned upward edges out of
|
|
15
|
+
// `core/`, named in CLAUDE.md and pinned by `test/seam/layering.test.ts` — the
|
|
16
|
+
// orchestrator reaching down for `Rule`. `RuleOptions` rides the same edge
|
|
17
|
+
// rather than being restated here; see `rule()`.
|
|
18
|
+
import { Rule } from "../renderables/rule.js";
|
|
19
|
+
import { segmentsToString } from "./render.js";
|
|
20
|
+
import { placeBlock } from "./place.js";
|
|
21
|
+
import { isRenderable } from "./protocol.js";
|
|
22
|
+
// [LAW:dataflow-not-control-flow] With no live region claimed, stepping around
|
|
23
|
+
// it is the text itself: every write takes the same path.
|
|
24
|
+
const NO_LIVE_REGION = { around: (text) => text };
|
|
25
|
+
// Rows as the bytes that reach the terminal. Every row but the last is closed
|
|
26
|
+
// by a line break, and the last one when `closed` says so. Crop last, and crop
|
|
27
|
+
// the line-end with the rest: in the reference `end` is the tail of the printed
|
|
28
|
+
// line, so a line cut at the edge loses it too.
|
|
29
|
+
function emit({ rows, closed, cropWidth }) {
|
|
30
|
+
const output = rows.flatMap((row, index) => closed || index < rows.length - 1 ? [...row, Segment.line()] : row);
|
|
31
|
+
return [...Segment.cropLines(output, cropWidth)];
|
|
32
|
+
}
|
|
33
|
+
// The time `log` stamps: local 24-hour `HH:MM:SS`, what the reference's `%X`
|
|
34
|
+
// prints under the C locale a Python program runs in unless it asks for
|
|
35
|
+
// another. The machine's locale never reaches it, so neither does the width
|
|
36
|
+
// an AM/PM suffix would add, and one clock prints one stamp everywhere.
|
|
37
|
+
function logTime(time) {
|
|
38
|
+
// [LAW:no-silent-failure] The clock is the caller's, and an invalid `Date`
|
|
39
|
+
// would otherwise print as `[NaN:NaN:NaN]`.
|
|
40
|
+
if (Number.isNaN(time.getTime()))
|
|
41
|
+
throw new RangeError("Console getDatetime returned an invalid Date");
|
|
42
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
43
|
+
return `[${two(time.getHours())}:${two(time.getMinutes())}:${two(time.getSeconds())}] `;
|
|
44
|
+
}
|
|
45
|
+
// Three of the five justify methods place what a print draws as a block, the
|
|
46
|
+
// way Rich's `print` wraps each renderable in `Align`. The other two leave it
|
|
47
|
+
// where it was drawn: `"default"` asks for no placement, and `"full"` is a way
|
|
48
|
+
// of setting a paragraph's words, which only text can do.
|
|
49
|
+
const PLACED_BY = {
|
|
50
|
+
default: undefined,
|
|
51
|
+
full: undefined,
|
|
52
|
+
left: "left",
|
|
53
|
+
center: "center",
|
|
54
|
+
right: "right",
|
|
55
|
+
};
|
|
56
|
+
// A text run as one block: its items joined into one text, as the reference's
|
|
57
|
+
// `Text.join` joins them, drawn under the print's options and ended by the
|
|
58
|
+
// print's `end`. Measure and render both read that one text, so the width
|
|
59
|
+
// `justify` places the run at is the width it is drawn at. Each item's own
|
|
60
|
+
// `justify`, `overflow`, `noWrap` and `tabSize` stay behind, as they do in the
|
|
61
|
+
// reference: the print's options set the whole run.
|
|
62
|
+
class TextRun {
|
|
63
|
+
end;
|
|
64
|
+
text;
|
|
65
|
+
constructor(items, end) {
|
|
66
|
+
this.end = end;
|
|
67
|
+
this.text = items.reduce((joined, item) => joined.append(item), new RichText("", { end: "" }));
|
|
68
|
+
}
|
|
69
|
+
*render(options) {
|
|
70
|
+
yield* this.text.render(options);
|
|
71
|
+
yield this.end;
|
|
72
|
+
}
|
|
73
|
+
measure(options) {
|
|
74
|
+
return this.text.measure(options);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// A host with an environment but no streams — what a browser looks like from
|
|
78
|
+
// here. Frozen so the shared instance cannot be mutated into a fake terminal.
|
|
79
|
+
const DETACHED_ENVIRONMENT = Object.freeze({
|
|
80
|
+
env: Object.freeze({}),
|
|
81
|
+
});
|
|
82
|
+
// [LAW:single-enforcer] The only read of the ambient `process` global in this
|
|
83
|
+
// module, and the reason every other function below takes its environment as
|
|
84
|
+
// an argument. `process` satisfies `ConsoleEnvironment` structurally, so the
|
|
85
|
+
// node path needs no adapter; the browser path degrades to a host that answers
|
|
86
|
+
// "no streams" rather than throwing at the reference.
|
|
87
|
+
function ambientEnvironment() {
|
|
88
|
+
return typeof process === "undefined" ? DETACHED_ENVIRONMENT : process;
|
|
89
|
+
}
|
|
90
|
+
// [LAW:one-source-of-truth] A console talks to exactly one stream, and every
|
|
91
|
+
// question it asks the host — are you a terminal, how wide are you, where do
|
|
92
|
+
// bytes go — is a question about *that* stream. Selecting it once, here, is
|
|
93
|
+
// what makes the three answers agree. When each site re-selected for itself,
|
|
94
|
+
// one of them (size) silently didn't, and a `Console({ stderr: true })` keyed
|
|
95
|
+
// its colour to stderr and its width to stdout.
|
|
96
|
+
function boundStream(environment, useStderr) {
|
|
97
|
+
return useStderr ? environment.stderr : environment.stdout;
|
|
98
|
+
}
|
|
99
|
+
// [LAW:single-enforcer] Effective TTY status of the console's output target is
|
|
100
|
+
// computed once, at construction, and cached — an explicit `file` is not a
|
|
101
|
+
// terminal, `forceTerminal` says it is regardless, and otherwise the bound
|
|
102
|
+
// stream answers. Every input is fixed at construction, so recomputing per
|
|
103
|
+
// read could only ever return the same answer twice.
|
|
104
|
+
function effectiveIsTTY(options, stream) {
|
|
105
|
+
if (options?.forceTerminal)
|
|
106
|
+
return true;
|
|
107
|
+
if (options?.file)
|
|
108
|
+
return false;
|
|
109
|
+
return stream?.isTTY ?? false;
|
|
110
|
+
}
|
|
111
|
+
function terminalSize(environment, stream) {
|
|
112
|
+
const cols = environment.env["COLUMNS"];
|
|
113
|
+
const lines = environment.env["LINES"];
|
|
114
|
+
const w = cols ? parseInt(cols, 10) : (stream?.columns ?? 80);
|
|
115
|
+
const h = lines ? parseInt(lines, 10) : (stream?.rows ?? 24);
|
|
116
|
+
return { width: w || 80, height: h || 24 };
|
|
117
|
+
}
|
|
118
|
+
// [LAW:single-enforcer] One trust-boundary check: when no `file:` was
|
|
119
|
+
// provided, the console writes to the stream it is bound to — which a browser
|
|
120
|
+
// host does not have. Without this, the absence would surface as a `TypeError`
|
|
121
|
+
// deep inside `_write`. Centralizing it here keeps both consumers (`file`
|
|
122
|
+
// getter, `_write`) behaving identically — no second copy of the check to
|
|
123
|
+
// drift.
|
|
124
|
+
function defaultSink(stream) {
|
|
125
|
+
if (stream === undefined) {
|
|
126
|
+
throw new Error("Console: no `file` provided and the environment has no stream to " +
|
|
127
|
+
"write to (e.g. running in a browser). Pass " +
|
|
128
|
+
"`environment: hostEnvironment(host)` to write through a terminal " +
|
|
129
|
+
"host, or a `file` so output has somewhere to go.");
|
|
130
|
+
}
|
|
131
|
+
return stream;
|
|
132
|
+
}
|
|
133
|
+
// [LAW:dataflow-not-control-flow] Build one size-reading function at
|
|
134
|
+
// construction time from whichever options the caller supplied. The width
|
|
135
|
+
// getter is then unconditional — it always calls `_getSize()`. Variability
|
|
136
|
+
// lives in the captured closure (which source it consults), not in branches
|
|
137
|
+
// the getter has to evaluate on every read.
|
|
138
|
+
//
|
|
139
|
+
// The all-static and mixed cases get *different* closures: when both
|
|
140
|
+
// dimensions are fixed, the returned function is a constant — no
|
|
141
|
+
// `terminalSize` call on every read, since neither value can change.
|
|
142
|
+
// When at least one dimension is dynamic, the closure consults
|
|
143
|
+
// `terminalSize` to fill in the missing side.
|
|
144
|
+
function resolveGetSize(options, environment, stream) {
|
|
145
|
+
if (options?.getSize)
|
|
146
|
+
return options.getSize;
|
|
147
|
+
const staticWidth = options?.width;
|
|
148
|
+
const staticHeight = options?.height;
|
|
149
|
+
if (staticWidth !== undefined && staticHeight !== undefined) {
|
|
150
|
+
// [LAW:one-source-of-truth] Freeze the captured object so consumers can't
|
|
151
|
+
// mutate `console.size` and silently change every subsequent read.
|
|
152
|
+
// Matches the dynamic-closure path (which builds a fresh object per call
|
|
153
|
+
// and is therefore implicitly immutable from a sharing standpoint).
|
|
154
|
+
const fixed = Object.freeze({ width: staticWidth, height: staticHeight });
|
|
155
|
+
return () => fixed;
|
|
156
|
+
}
|
|
157
|
+
return () => {
|
|
158
|
+
const term = terminalSize(environment, stream);
|
|
159
|
+
return {
|
|
160
|
+
width: staticWidth ?? term.width,
|
|
161
|
+
height: staticHeight ?? term.height,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// --- Console ---
|
|
166
|
+
/** Shared because it is stateless: highlighting nothing has nothing to own. */
|
|
167
|
+
const NO_HIGHLIGHT = new NullHighlighter();
|
|
168
|
+
export class Console {
|
|
169
|
+
_destination;
|
|
170
|
+
// [LAW:one-source-of-truth] Size flows through a single function. Static
|
|
171
|
+
// `width`/`height` options collapse into a closure that returns them; a
|
|
172
|
+
// caller-supplied `getSize` overrides. Every size read in this class goes
|
|
173
|
+
// through `_getSize()` — no second path, no second source to drift.
|
|
174
|
+
_getSize;
|
|
175
|
+
_getDatetime;
|
|
176
|
+
// The stamp the last `log` row was given, which the next row's stamp is
|
|
177
|
+
// compared with: the reference's `LogRender._last_time`, one per console.
|
|
178
|
+
// [LAW:no-ambient-temporal-coupling] "same as the previous row" is a fact
|
|
179
|
+
// about an earlier call, so it is state this console owns, not order luck.
|
|
180
|
+
_lastLogTime = null;
|
|
181
|
+
_style;
|
|
182
|
+
_isTerminal;
|
|
183
|
+
_interactive;
|
|
184
|
+
_file;
|
|
185
|
+
// [LAW:one-source-of-truth] The resolved output stream, not the `stderr`
|
|
186
|
+
// flag that selected it. Storing the answer instead of the question is what
|
|
187
|
+
// stops `isTerminal`, `size`, and `file` from re-deriving it three ways.
|
|
188
|
+
_stream;
|
|
189
|
+
_record;
|
|
190
|
+
_markup;
|
|
191
|
+
_highlight;
|
|
192
|
+
_theme;
|
|
193
|
+
_onStyleError;
|
|
194
|
+
_asciiOnly;
|
|
195
|
+
_highlighter;
|
|
196
|
+
_recorded;
|
|
197
|
+
// [LAW:types-are-the-program] The capture state is carried in the *type*
|
|
198
|
+
// of this field, not in a sentinel value. `null` = not capturing; any
|
|
199
|
+
// string (including "") = capturing, this is the buffer. A paired
|
|
200
|
+
// boolean+string would admit the impossible state "not capturing, buffer
|
|
201
|
+
// non-empty" — `string | null` makes that unrepresentable.
|
|
202
|
+
_capture;
|
|
203
|
+
_liveRegion;
|
|
204
|
+
constructor(options) {
|
|
205
|
+
const environment = options?.environment ?? ambientEnvironment();
|
|
206
|
+
const stream = boundStream(environment, options?.stderr ?? false);
|
|
207
|
+
this._stream = stream;
|
|
208
|
+
this._isTerminal = effectiveIsTTY(options, stream);
|
|
209
|
+
// [LAW:dataflow-not-control-flow] `isTTY` and `env` are forwarded
|
|
210
|
+
// unconditionally, so detection stays inside the console's injected
|
|
211
|
+
// environment rather than the ambient process's.
|
|
212
|
+
const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env });
|
|
213
|
+
// [LAW:single-enforcer] The console's one override, an explicit
|
|
214
|
+
// `hyperlinks`, is applied here and nowhere else; every write encodes for
|
|
215
|
+
// this value whole.
|
|
216
|
+
this._destination = {
|
|
217
|
+
colorSystem: resolved.colorSystem,
|
|
218
|
+
hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
|
|
219
|
+
};
|
|
220
|
+
this._getSize = resolveGetSize(options, environment, stream);
|
|
221
|
+
this._getDatetime = options?.getDatetime ?? (() => new Date());
|
|
222
|
+
// [LAW:no-ambient-temporal-coupling] The theme is assigned before the
|
|
223
|
+
// style, because the console's own style may be one of the theme's names.
|
|
224
|
+
this._theme = options?.theme ?? DEFAULT_THEME;
|
|
225
|
+
this._style = this._theme.resolve(options?.style ?? NULL_STYLE);
|
|
226
|
+
// Rich's rule: a terminal is interactive unless it is a dumb one.
|
|
227
|
+
this._interactive = options?.forceInteractive ?? (this._isTerminal && !isDumbTerminal(environment.env));
|
|
228
|
+
this._file = options?.file;
|
|
229
|
+
this._record = options?.record ?? false;
|
|
230
|
+
this._markup = options?.markup !== false;
|
|
231
|
+
this._highlight = options?.highlight !== false;
|
|
232
|
+
this._highlighter = options?.highlighter ?? new ReprHighlighter();
|
|
233
|
+
this._onStyleError = options?.onStyleError;
|
|
234
|
+
this._asciiOnly = options?.asciiOnly ?? false;
|
|
235
|
+
this._recorded = [];
|
|
236
|
+
this._capture = null;
|
|
237
|
+
this._liveRegion = NO_LIVE_REGION;
|
|
238
|
+
}
|
|
239
|
+
// --- Properties ---
|
|
240
|
+
get size() {
|
|
241
|
+
return this._getSize();
|
|
242
|
+
}
|
|
243
|
+
get width() {
|
|
244
|
+
return this._getSize().width;
|
|
245
|
+
}
|
|
246
|
+
get height() {
|
|
247
|
+
return this._getSize().height;
|
|
248
|
+
}
|
|
249
|
+
get isTerminal() {
|
|
250
|
+
return this._isTerminal;
|
|
251
|
+
}
|
|
252
|
+
get isInteractive() {
|
|
253
|
+
return this._interactive;
|
|
254
|
+
}
|
|
255
|
+
get colorSystem() {
|
|
256
|
+
return this._destination.colorSystem;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Where this console writes: the colour depth it draws at and whether it
|
|
260
|
+
* emits OSC 8 hyperlinks, overrides applied — the value its output is
|
|
261
|
+
* encoded for, for anything that encodes on its behalf (`Live`).
|
|
262
|
+
*/
|
|
263
|
+
get destination() {
|
|
264
|
+
return this._destination;
|
|
265
|
+
}
|
|
266
|
+
// [LAW:one-source-of-truth] Output target lookup matches `_write`'s:
|
|
267
|
+
// explicit `file` wins, otherwise `defaultSink` returns the bound stream
|
|
268
|
+
// (or throws clearly when the environment has none). Exposed
|
|
269
|
+
// so renderables that bypass the segment pipeline (e.g. Live's raw control
|
|
270
|
+
// sequences) still write to the caller's configured stream. Return type is
|
|
271
|
+
// the narrow `ConsoleSink` — Live (and any external consumer) only needs
|
|
272
|
+
// `.write()`, so the type tells the truth about what the surface guarantees.
|
|
273
|
+
get file() {
|
|
274
|
+
return this._file ?? defaultSink(this._stream);
|
|
275
|
+
}
|
|
276
|
+
get theme() {
|
|
277
|
+
return this._theme;
|
|
278
|
+
}
|
|
279
|
+
get options() {
|
|
280
|
+
// One read of the size, so a resize cannot pair one frame's width with
|
|
281
|
+
// another's height.
|
|
282
|
+
const { width, height } = this.size;
|
|
283
|
+
return {
|
|
284
|
+
maxWidth: width,
|
|
285
|
+
// The terminal an inline print lands on: a ceiling, never a region —
|
|
286
|
+
// content keeps its natural height beneath it.
|
|
287
|
+
height: { rows: height, exact: false },
|
|
288
|
+
isTerminal: this.isTerminal,
|
|
289
|
+
asciiOnly: this._asciiOnly,
|
|
290
|
+
theme: this._theme,
|
|
291
|
+
onStyleError: this._onStyleError,
|
|
292
|
+
colorSystem: this._destination.colorSystem,
|
|
293
|
+
markup: this._markup,
|
|
294
|
+
highlight: this._highlight,
|
|
295
|
+
highlighter: this._highlighter,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// --- Print ---
|
|
299
|
+
print(...args) {
|
|
300
|
+
this._writeSegments(emit(this._draw(args, this.options)));
|
|
301
|
+
}
|
|
302
|
+
// What `print` draws for `args` at `options.maxWidth`.
|
|
303
|
+
_draw(args, options) {
|
|
304
|
+
// Extract options from last arg if it's a PrintOptions
|
|
305
|
+
let opts = {};
|
|
306
|
+
let items;
|
|
307
|
+
// The trailing options object is only recognised when something precedes
|
|
308
|
+
// it. A lone object is data — `print({ style: "..." })` reads as a value to
|
|
309
|
+
// format, and treating it as options would emit an empty line, which is the
|
|
310
|
+
// one outcome this method must never produce. Nothing is given up: options
|
|
311
|
+
// configure the rendering of content, so a call carrying options and no
|
|
312
|
+
// content had nothing to render either way. [LAW:no-silent-failure]
|
|
313
|
+
const lastArg = args[args.length - 1];
|
|
314
|
+
if (args.length > 1 &&
|
|
315
|
+
typeof lastArg === "object" &&
|
|
316
|
+
lastArg !== null &&
|
|
317
|
+
!isRenderable(lastArg) &&
|
|
318
|
+
("style" in lastArg || "justify" in lastArg || "markup" in lastArg ||
|
|
319
|
+
"highlight" in lastArg || "overflow" in lastArg || "end" in lastArg ||
|
|
320
|
+
"softWrap" in lastArg || "crop" in lastArg || "sep" in lastArg)) {
|
|
321
|
+
opts = lastArg;
|
|
322
|
+
items = args.slice(0, -1);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
items = args;
|
|
326
|
+
}
|
|
327
|
+
// What a string is read as, wherever in this print it lands: an argument,
|
|
328
|
+
// or a cell or label inside a renderable argument. `print`'s own flags
|
|
329
|
+
// outrank the console's, as Rich's `options.update` has them.
|
|
330
|
+
const strings = {
|
|
331
|
+
markup: opts.markup ?? this._markup,
|
|
332
|
+
highlight: opts.highlight ?? this._highlight,
|
|
333
|
+
highlighter: this._highlighter,
|
|
334
|
+
};
|
|
335
|
+
const sep = opts.sep ?? " ";
|
|
336
|
+
const end = opts.end ?? "\n";
|
|
337
|
+
const softWrap = opts.softWrap ?? false;
|
|
338
|
+
const printStyle = this._theme.resolve(opts.style ?? NULL_STYLE);
|
|
339
|
+
// A print is a column of blocks, and every argument joins one of two kinds.
|
|
340
|
+
// Text — a string, a `RichText`, or a scalar — runs together: adjacent text
|
|
341
|
+
// items are one block, joined by `sep` and ended by `end`. Any other
|
|
342
|
+
// renderable is a block of its own that occupies whole lines, so neither
|
|
343
|
+
// `sep` nor `end` ever touches it. That is the reference's split — `end`
|
|
344
|
+
// belongs to text, not to the print — and it is what lets two printed panels
|
|
345
|
+
// stack with no blank line between them. Data is cut where the reference
|
|
346
|
+
// cuts it: a container (`isExpandable`) is a block, a scalar is text. A
|
|
347
|
+
// container is laid out by `Pretty` across as many lines as it needs, and
|
|
348
|
+
// joined into a run it would start partway along a line and leave its
|
|
349
|
+
// closing bracket where the next item carries on. The line is not closed by
|
|
350
|
+
// asking where the cursor sits: `print("a\n")` is a line and an empty one,
|
|
351
|
+
// here as in the reference and in every other `print`, and only the kind of
|
|
352
|
+
// the item can tell that trailing break from a `Panel`'s. A call with
|
|
353
|
+
// nothing to print is one empty text run, so it still ends the line.
|
|
354
|
+
const blocks = items.length === 0 ? [{ kind: "text", items: [] }] : [];
|
|
355
|
+
for (const item of items) {
|
|
356
|
+
// Four arms, and they are the whole domain. A `RichText` is already text.
|
|
357
|
+
// No arm's own `end` reaches the line: `TextRun` joins the run with
|
|
358
|
+
// `append`, which leaves each part's behind, as Rich's `Text(sep).join`
|
|
359
|
+
// does, because the line end of a run is the print's. Any other renderable draws itself,
|
|
360
|
+
// as a block. A string is the only kind of argument that can *contain*
|
|
361
|
+
// markup, so it is the only kind the markup dialect is applied to.
|
|
362
|
+
// Everything else is data — a block when it is a container, text when it
|
|
363
|
+
// is not — and `Pretty` is the single authority on how a
|
|
364
|
+
// JavaScript value displays — `String(value)` was a second, weaker one
|
|
365
|
+
// that answered `[object Object]` for every object and let the markup
|
|
366
|
+
// parser eat it. [LAW:one-source-of-truth]
|
|
367
|
+
let text;
|
|
368
|
+
if (item instanceof RichText) {
|
|
369
|
+
text = item;
|
|
370
|
+
}
|
|
371
|
+
else if (isRenderable(item)) {
|
|
372
|
+
blocks.push({ kind: "lines", renderable: item });
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
else if (typeof item === "string") {
|
|
376
|
+
text = renderStr(item, strings);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// The highlighter travels with the value. `highlight` and a custom
|
|
380
|
+
// `highlighter` are console-wide settings, so they have to reach a
|
|
381
|
+
// formatted object the same way they reach a printed string; a `Pretty`
|
|
382
|
+
// left to its own default would outrank both. [LAW:dataflow-not-control-flow]
|
|
383
|
+
// the disabled case is an identity highlighter, not a skipped call.
|
|
384
|
+
const pretty = new Pretty(item, {
|
|
385
|
+
...UNSEEN_DATA_BOUNDS,
|
|
386
|
+
highlighter: activeHighlighter(strings) ?? NO_HIGHLIGHT,
|
|
387
|
+
});
|
|
388
|
+
// A scalar spells the same at every width, so it joins its run as the
|
|
389
|
+
// one text it is.
|
|
390
|
+
if (isExpandable(item)) {
|
|
391
|
+
blocks.push({ kind: "lines", renderable: pretty });
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
text = pretty.toText(options);
|
|
395
|
+
}
|
|
396
|
+
const run = blocks.at(-1);
|
|
397
|
+
if (run?.kind === "text")
|
|
398
|
+
run.items.push(new RichText(sep), text);
|
|
399
|
+
else
|
|
400
|
+
blocks.push({ kind: "text", items: [text] });
|
|
401
|
+
}
|
|
402
|
+
// Soft wrap is the reference's pair of defaults, not a mode of its own: it
|
|
403
|
+
// stops wrapping, and when no overflow was named it asks for `"ignore"`, so
|
|
404
|
+
// the line leaves unbounded. A named overflow still cuts the unwrapped line
|
|
405
|
+
// at the width, as Rich's does. What `"ignore"` does to a line — no edge,
|
|
406
|
+
// no justify — is `RichText`'s to apply, so it crosses as it was asked for.
|
|
407
|
+
const renderOpts = {
|
|
408
|
+
...options,
|
|
409
|
+
...strings,
|
|
410
|
+
justify: opts.justify === "default" ? undefined : opts.justify,
|
|
411
|
+
overflow: opts.overflow ?? (softWrap ? "ignore" : undefined),
|
|
412
|
+
noWrap: softWrap,
|
|
413
|
+
};
|
|
414
|
+
// The print style, then the console's base style, over each line a block
|
|
415
|
+
// drew and never over the break that ends it: a styled break carries SGR
|
|
416
|
+
// codes across the newline.
|
|
417
|
+
// [LAW:dataflow-not-control-flow] no style to apply is an empty list, not
|
|
418
|
+
// a skipped step.
|
|
419
|
+
const styles = [printStyle, this._style].filter((style) => !style.isNull);
|
|
420
|
+
const styleContent = (segments) => styles.reduce((styled, style) => Segment.applyStyle(styled, style), segments);
|
|
421
|
+
// The common default end is "\n" — reuse Segment's cached newline rather
|
|
422
|
+
// than allocating one per print; only a non-default end needs a fresh one.
|
|
423
|
+
const terminator = end === "\n" ? Segment.line() : new Segment(end);
|
|
424
|
+
const align = PLACED_BY[opts.justify ?? "default"];
|
|
425
|
+
// Each block as its lines, and whether its last line is closed. A placed
|
|
426
|
+
// block is closed line by line, as `Align` closes it. Unplaced, a text run
|
|
427
|
+
// is closed exactly where its content — its `end` included — breaks; any
|
|
428
|
+
// other block's lines are closed whether or not it closed them itself: a
|
|
429
|
+
// `Panel` ends in a line break and a bare `ProgressBar` does not (a line
|
|
430
|
+
// fragment — test/seam/line-ends.ts), and both leave the next print at the
|
|
431
|
+
// start of a line.
|
|
432
|
+
const drawBlock = (block) => {
|
|
433
|
+
const renderable = block.kind === "text" ? new TextRun(block.items, terminator) : block.renderable;
|
|
434
|
+
if (align)
|
|
435
|
+
return { lines: placeBlock(renderable, align, renderOpts), closed: true };
|
|
436
|
+
const drawn = [...renderable.render(renderOpts)];
|
|
437
|
+
// A text run's last segment is always its `end`, yielded even when
|
|
438
|
+
// empty, and `splitLines` drops the empty line after a final break only
|
|
439
|
+
// when nothing follows it — so the run is closed exactly when `end` is.
|
|
440
|
+
const closed = block.kind === "lines" || end.endsWith("\n");
|
|
441
|
+
return { lines: Segment.splitLines(drawn), closed };
|
|
442
|
+
};
|
|
443
|
+
// Every line end goes through the same writeSegments funnel as the text it
|
|
444
|
+
// ends, so it survives recording — otherwise `exportText` and `exportHtml`
|
|
445
|
+
// would join consecutive prints onto a single line. [LAW:single-enforcer]
|
|
446
|
+
// A block left open carries on along the next block's first line, so the
|
|
447
|
+
// rows are read off the joined output rather than counted per block.
|
|
448
|
+
const output = [];
|
|
449
|
+
let closed = true;
|
|
450
|
+
for (const block of blocks) {
|
|
451
|
+
const drawn = drawBlock(block);
|
|
452
|
+
drawn.lines.forEach((line, index) => {
|
|
453
|
+
output.push(...styleContent(line));
|
|
454
|
+
if (drawn.closed || index < drawn.lines.length - 1)
|
|
455
|
+
output.push(Segment.line());
|
|
456
|
+
});
|
|
457
|
+
closed = drawn.closed;
|
|
458
|
+
}
|
|
459
|
+
// Soft wrap turns cropping off whatever `crop` says, because a line it left
|
|
460
|
+
// whole is meant to reach the terminal whole. [LAW:dataflow-not-control-flow]
|
|
461
|
+
// Not cropping is an unbounded width rather than a skipped step — the same
|
|
462
|
+
// spelling `RichText` uses for `"ignore"`.
|
|
463
|
+
const cropWidth = !softWrap && (opts.crop ?? true) ? options.maxWidth : Infinity;
|
|
464
|
+
return { rows: Segment.splitLines(output), closed, cropWidth };
|
|
465
|
+
}
|
|
466
|
+
// The reference's `LogRender`: a grid row of two cells, the time and beside it
|
|
467
|
+
// whatever `print` would draw for `args` in the width that is left. A grid row
|
|
468
|
+
// is at least one line tall and always closed, whatever `end` left open
|
|
469
|
+
// inside its cell. The time gives up cells before the content does, so a
|
|
470
|
+
// console narrower than the time still shows every argument. The time takes
|
|
471
|
+
// the console's style alone, as the reference styles only the renderables.
|
|
472
|
+
// A time that reads as the previous row's is left blank at its width, the
|
|
473
|
+
// reference's `omit_repeated_times`, so a burst of rows carries one stamp.
|
|
474
|
+
log(...args) {
|
|
475
|
+
const options = this.options;
|
|
476
|
+
const stamped = logTime(this._getDatetime());
|
|
477
|
+
const repeated = stamped === this._lastLogTime;
|
|
478
|
+
const time = new RichText(stamped, { end: "" });
|
|
479
|
+
time.stylize("log.time");
|
|
480
|
+
const width = Math.min(time.cellLength, options.maxWidth - 1);
|
|
481
|
+
const { rows, cropWidth } = this._draw(args, { ...options, maxWidth: options.maxWidth - width });
|
|
482
|
+
const [stampLine = []] = Segment.splitLines(time.render({ ...options, maxWidth: time.cellLength }));
|
|
483
|
+
const styled = (cell) => [...Segment.applyStyle(cell, this._style)];
|
|
484
|
+
const stamp = styled(Segment.adjustLineLength(stampLine, width));
|
|
485
|
+
const blank = styled([new Segment(" ".repeat(width))]);
|
|
486
|
+
const head = repeated ? blank : stamp;
|
|
487
|
+
const grid = Array.from({ length: Math.max(1, rows.length) }, (_, index) => [
|
|
488
|
+
...(index === 0 ? head : blank),
|
|
489
|
+
...(rows[index] ?? []),
|
|
490
|
+
]);
|
|
491
|
+
// The row is the column wider than its cell; an uncropped cell stays so.
|
|
492
|
+
this._writeSegments(emit({ rows: grid, closed: true, cropWidth: cropWidth + width }));
|
|
493
|
+
// Remembered only once the row is written: a log that threw printed no
|
|
494
|
+
// stamp, so the next row's must show.
|
|
495
|
+
this._lastLogTime = stamped;
|
|
496
|
+
}
|
|
497
|
+
// [LAW:one-source-of-truth] `RuleOptions` is `Rule`'s, not a restatement of
|
|
498
|
+
// it. `console.ts` used to declare its own copy and field-by-field forward
|
|
499
|
+
// into this constructor, which made a second map of one shape — identical
|
|
500
|
+
// then, free to drift the moment `Rule` grew an option.
|
|
501
|
+
rule(title, options) {
|
|
502
|
+
const segments = [...new Rule(title, options).render(this.options)];
|
|
503
|
+
this._writeSegments(segments);
|
|
504
|
+
}
|
|
505
|
+
// [LAW:one-source-of-truth] `JSONRenderable` is the one formatter for JSON and
|
|
506
|
+
// `JSONOptions` is its option set; this method only picks the constructor. It
|
|
507
|
+
// prints with soft wrap, as the reference does, so a line wider than the
|
|
508
|
+
// console reaches the terminal whole and the output stays valid JSON.
|
|
509
|
+
printJson(json, options) {
|
|
510
|
+
const renderable = typeof json === "string"
|
|
511
|
+
? JSONRenderable.fromString(json, options)
|
|
512
|
+
: JSONRenderable.fromData(json, options);
|
|
513
|
+
this.print(renderable, { softWrap: true });
|
|
514
|
+
}
|
|
515
|
+
// --- Live region ---
|
|
516
|
+
/**
|
|
517
|
+
* Hold `region` under everything this console writes until the returned
|
|
518
|
+
* call lets it go. One region at a time: a second would be erased and
|
|
519
|
+
* painted over by the first, so claiming one while another is held throws.
|
|
520
|
+
*/
|
|
521
|
+
claimLiveRegion(region) {
|
|
522
|
+
if (this._liveRegion !== NO_LIVE_REGION) {
|
|
523
|
+
throw new Error("A live display is already running on this console; stop it before starting another.");
|
|
524
|
+
}
|
|
525
|
+
this._liveRegion = region;
|
|
526
|
+
// Letting go of a region no longer held — a second call, or after another
|
|
527
|
+
// was claimed — leaves the one that is.
|
|
528
|
+
return () => {
|
|
529
|
+
if (this._liveRegion === region)
|
|
530
|
+
this._liveRegion = NO_LIVE_REGION;
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// --- Capture ---
|
|
534
|
+
// [LAW:one-source-of-truth] Redirect semantics match Python Rich's
|
|
535
|
+
// `Console.capture()`: while a capture is active the underlying target
|
|
536
|
+
// (file / stdout / stderr) receives nothing, and `endCapture()` returns
|
|
537
|
+
// the full text that would otherwise have been written. This is not a
|
|
538
|
+
// tee — callers wanting both must compose explicitly.
|
|
539
|
+
beginCapture() {
|
|
540
|
+
this._capture = "";
|
|
541
|
+
}
|
|
542
|
+
endCapture() {
|
|
543
|
+
const result = this._capture ?? "";
|
|
544
|
+
this._capture = null;
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
// --- Export (when record:true) ---
|
|
548
|
+
exportText({ clear = true } = {}) {
|
|
549
|
+
const text = this._recorded.map((s) => s.text).join("");
|
|
550
|
+
if (clear)
|
|
551
|
+
this._recorded = [];
|
|
552
|
+
return text;
|
|
553
|
+
}
|
|
554
|
+
// [LAW:single-enforcer] `encodeHtml` draws the picture `exportLines`
|
|
555
|
+
// resolves; nothing here interprets a `Style`.
|
|
556
|
+
exportHtml({ theme, clear = true } = {}) {
|
|
557
|
+
const html = encodeHtml(this._recorded, theme);
|
|
558
|
+
if (clear)
|
|
559
|
+
this._recorded = [];
|
|
560
|
+
return html;
|
|
561
|
+
}
|
|
562
|
+
// [LAW:single-enforcer] As `exportHtml`: `encodeSvg` draws, this clears.
|
|
563
|
+
exportSvg({ theme, title = "Rich", clear = true } = {}) {
|
|
564
|
+
const svg = encodeSvg(this._recorded, { theme, title, width: this.width });
|
|
565
|
+
if (clear)
|
|
566
|
+
this._recorded = [];
|
|
567
|
+
return svg;
|
|
568
|
+
}
|
|
569
|
+
// --- Internal ---
|
|
570
|
+
// [LAW:single-enforcer] One encode call per write batch routes through the
|
|
571
|
+
// same `segmentsToString` used by `renderToString`. Recording captures every
|
|
572
|
+
// non-control segment for replay.
|
|
573
|
+
_writeSegments(segments) {
|
|
574
|
+
if (this._record) {
|
|
575
|
+
for (const segment of segments) {
|
|
576
|
+
if (!segment.isControl)
|
|
577
|
+
this._recorded.push(segment);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const encoded = segmentsToString(segments, this._destination);
|
|
581
|
+
if (encoded.length > 0)
|
|
582
|
+
this._write(encoded);
|
|
583
|
+
}
|
|
584
|
+
_write(text) {
|
|
585
|
+
// [LAW:types-are-the-program] `_capture !== null` is a discriminator
|
|
586
|
+
// narrow on `string | null`, not a sentinel comparison — the type
|
|
587
|
+
// carries the active/inactive state. The early return is what makes
|
|
588
|
+
// this a redirect (not a tee); see beginCapture for semantics.
|
|
589
|
+
if (this._capture !== null) {
|
|
590
|
+
this._capture += text;
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const target = this._file ?? defaultSink(this._stream);
|
|
594
|
+
target.write(this._liveRegion.around(text));
|
|
595
|
+
}
|
|
596
|
+
}
|