@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:prompt — readline-backed `PromptInput` for use with the prompt
|
|
3
|
+
* renderable in a Node runtime.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:locality-or-seam] `node:readline` lives only here, on the node side
|
|
6
|
+
* of the API boundary. The main barrel stays browser-safe; consumers that
|
|
7
|
+
* want interactive prompts in Node opt in by importing `nodeAsk` and
|
|
8
|
+
* passing it as the input capability:
|
|
9
|
+
*
|
|
10
|
+
* import { Prompt } from "@promptctl/rich-js";
|
|
11
|
+
* import { nodeAsk } from "@promptctl/rich-js/node/prompt";
|
|
12
|
+
* const answer = await Prompt.ask("What's your name?", nodeAsk);
|
|
13
|
+
* // or with options:
|
|
14
|
+
* const choice = await Prompt.ask("Pick one", nodeAsk, { choices: ["a", "b"] });
|
|
15
|
+
*
|
|
16
|
+
* The prompt is printed by the `Console` it was asked on — the app's, when it
|
|
17
|
+
* passed one as `console`, a new default one otherwise — and then a line is
|
|
18
|
+
* read, as Rich's `Console.input` prints its prompt and calls `input()`. So
|
|
19
|
+
* the prompt is coloured exactly as that console's own output would be and
|
|
20
|
+
* goes to its target: a console on stderr asks on stderr, beside the
|
|
21
|
+
* invalid-answer messages it prints. It goes as one logical line, never broken
|
|
22
|
+
* at the console's width: the terminal wraps it at whatever width it has.
|
|
23
|
+
*
|
|
24
|
+
* readline reads in non-terminal mode, the terminal's own cooked line
|
|
25
|
+
* discipline, as Python's `input()` does without its readline module: in
|
|
26
|
+
* terminal mode readline redraws the line itself, prompt included, and needs a
|
|
27
|
+
* whole `tty.WriteStream` to draw on, which a console's target need not be.
|
|
28
|
+
*
|
|
29
|
+
* [LAW:single-enforcer] One readline interface per `nodeAsk` call —
|
|
30
|
+
* created, asked, closed. No shared `rl` across prompts, no listener-leak
|
|
31
|
+
* pitfalls when callers stack prompts in a loop.
|
|
32
|
+
*/
|
|
33
|
+
import * as readline from "node:readline";
|
|
34
|
+
export const nodeAsk = (prompt, console) => {
|
|
35
|
+
console.print(prompt, { end: "", softWrap: true });
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const rl = readline.createInterface({
|
|
38
|
+
input: process.stdin,
|
|
39
|
+
// [LAW:types-are-the-program] exception: readline types its output as a
|
|
40
|
+
// whole `NodeJS.WritableStream`, and a console's target is only a
|
|
41
|
+
// `ConsoleSink`. With `terminal: false` readline only ever calls its
|
|
42
|
+
// `write`, whatever its `isTTY` says.
|
|
43
|
+
output: console.file,
|
|
44
|
+
terminal: false,
|
|
45
|
+
});
|
|
46
|
+
rl.question("", (answer) => {
|
|
47
|
+
rl.close();
|
|
48
|
+
resolve(answer);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:save — node-only file helpers that persist recorded console output.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:locality-or-seam] These functions live outside the main barrel so
|
|
5
|
+
* Console can stop importing `fs`. Importing `Console` itself stays
|
|
6
|
+
* browser-safe; importing this module is the explicit opt-in for the
|
|
7
|
+
* fs-backed surface, and bundlers targeting the browser will (correctly)
|
|
8
|
+
* refuse to resolve `fs` if a consumer ever pulls it in by accident.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:single-enforcer] One funnel per format: `saveText` consumes
|
|
11
|
+
* `Console.exportText()`, `saveHtml` consumes `Console.exportHtml()`, and
|
|
12
|
+
* `saveSvg` consumes `Console.exportSvg()`. The encoding and recording logic
|
|
13
|
+
* stays on Console; this file is purely the IO sink.
|
|
14
|
+
*/
|
|
15
|
+
import type { Console } from "../core/console.js";
|
|
16
|
+
export declare function saveText(out: Console, path: string, options?: Parameters<Console["exportText"]>[0]): void;
|
|
17
|
+
export declare function saveHtml(out: Console, path: string, options?: Parameters<Console["exportHtml"]>[0]): void;
|
|
18
|
+
export declare function saveSvg(out: Console, path: string, options?: Parameters<Console["exportSvg"]>[0]): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:save — node-only file helpers that persist recorded console output.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:locality-or-seam] These functions live outside the main barrel so
|
|
5
|
+
* Console can stop importing `fs`. Importing `Console` itself stays
|
|
6
|
+
* browser-safe; importing this module is the explicit opt-in for the
|
|
7
|
+
* fs-backed surface, and bundlers targeting the browser will (correctly)
|
|
8
|
+
* refuse to resolve `fs` if a consumer ever pulls it in by accident.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:single-enforcer] One funnel per format: `saveText` consumes
|
|
11
|
+
* `Console.exportText()`, `saveHtml` consumes `Console.exportHtml()`, and
|
|
12
|
+
* `saveSvg` consumes `Console.exportSvg()`. The encoding and recording logic
|
|
13
|
+
* stays on Console; this file is purely the IO sink.
|
|
14
|
+
*/
|
|
15
|
+
import { writeFileSync } from "node:fs";
|
|
16
|
+
export function saveText(out, path, options) {
|
|
17
|
+
writeFileSync(path, out.exportText(options), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
export function saveHtml(out, path, options) {
|
|
20
|
+
writeFileSync(path, out.exportHtml(options), "utf-8");
|
|
21
|
+
}
|
|
22
|
+
export function saveSvg(out, path, options) {
|
|
23
|
+
writeFileSync(path, out.exportSvg(options), "utf-8");
|
|
24
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:terminal-host — the `TerminalHost` implementation backed by node's
|
|
3
|
+
* process streams.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:locality-or-seam] The `TerminalHost` interface is the seam and lives
|
|
6
|
+
* in `host/terminal-host.ts`, importable anywhere. *This* file is one
|
|
7
|
+
* value satisfying that interface, and it reads `process.stdin` /
|
|
8
|
+
* `process.stdout`, so it is a node subpath:
|
|
9
|
+
*
|
|
10
|
+
* import { NodeTerminalHost } from "@promptctl/rich-js/node/terminal-host";
|
|
11
|
+
* const screen = new Screen(new NodeTerminalHost());
|
|
12
|
+
*
|
|
13
|
+
* The ambient `process` global is not an import, so it slips past the
|
|
14
|
+
* module-graph reasoning that keeps the main barrel browser-safe: reading
|
|
15
|
+
* `process.stdin` inside a constructor throws in a browser only when someone
|
|
16
|
+
* constructs it. Moving the class here makes the boundary structural — a
|
|
17
|
+
* browser bundle that never reaches for this subpath cannot reach the global
|
|
18
|
+
* either.
|
|
19
|
+
*
|
|
20
|
+
* [LAW:no-shared-mutable-globals] Which files may read the host, and what
|
|
21
|
+
* each takes off it, is `HOST_ACCESS` in `test/seam/ambient-process.ts` —
|
|
22
|
+
* this file's entry carries its `why`, and a test fails when anything joins
|
|
23
|
+
* the list. Deliberately not restated here: the sole-ownership sentence that
|
|
24
|
+
* used to stand in this spot is what that list replaced.
|
|
25
|
+
*
|
|
26
|
+
* So if you find yourself reaching for `process.stdin`, `process.stdout`, or
|
|
27
|
+
* `setRawMode` in the runtime or in a demo, take a `TerminalHost` parameter
|
|
28
|
+
* instead. The seam is already here.
|
|
29
|
+
*/
|
|
30
|
+
import type { DataHandler, ExitHandler, ResizeHandler, TerminalHost, TerminalSize } from "../host/terminal-host.js";
|
|
31
|
+
import type { Unsubscribe } from "../core/subscription.js";
|
|
32
|
+
import type { Env } from "../core/env.js";
|
|
33
|
+
/**
|
|
34
|
+
* Subset of NodeJS stream API the node host actually depends on. Tests
|
|
35
|
+
* construct a host over PassThrough / Writable mocks; production wires it
|
|
36
|
+
* to `process.stdin` / `process.stdout`.
|
|
37
|
+
*/
|
|
38
|
+
interface NodeReadable {
|
|
39
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
40
|
+
off(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
41
|
+
setRawMode?(raw: boolean): unknown;
|
|
42
|
+
resume?(): unknown;
|
|
43
|
+
pause?(): unknown;
|
|
44
|
+
readonly isTTY?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface NodeWritable {
|
|
47
|
+
write(chunk: string | Uint8Array): unknown;
|
|
48
|
+
on(event: "resize", listener: () => void): unknown;
|
|
49
|
+
off(event: "resize", listener: () => void): unknown;
|
|
50
|
+
readonly columns?: number;
|
|
51
|
+
readonly rows?: number;
|
|
52
|
+
readonly isTTY?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Subset of node's `process` the host hears the program end through and
|
|
56
|
+
* suspends it with. Tests pass an `EventEmitter` that records `kill`, because
|
|
57
|
+
* the real one would stop the test runner.
|
|
58
|
+
*/
|
|
59
|
+
interface NodeProcess {
|
|
60
|
+
readonly pid: number;
|
|
61
|
+
on(event: string, listener: () => void): unknown;
|
|
62
|
+
prependListener(event: string, listener: () => void): unknown;
|
|
63
|
+
off(event: string, listener: () => void): unknown;
|
|
64
|
+
listenerCount(event: string): number;
|
|
65
|
+
kill(pid: number, signal: NodeJS.Signals): unknown;
|
|
66
|
+
}
|
|
67
|
+
export interface NodeTerminalHostOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Input stream. Defaults to `process.stdin` so production code does not
|
|
70
|
+
* have to construct one explicitly. Tests pass a `PassThrough`.
|
|
71
|
+
*/
|
|
72
|
+
stdin?: NodeReadable;
|
|
73
|
+
/**
|
|
74
|
+
* Output stream. Defaults to `process.stdout` for the same reason.
|
|
75
|
+
* Tests pass a `Writable` mock.
|
|
76
|
+
*/
|
|
77
|
+
stdout?: NodeWritable;
|
|
78
|
+
/**
|
|
79
|
+
* The environment colour detection reads. Defaults to `process.env`; tests
|
|
80
|
+
* pass a fixed map so what the host reports does not depend on the shell
|
|
81
|
+
* that ran them.
|
|
82
|
+
*/
|
|
83
|
+
env?: Env;
|
|
84
|
+
/**
|
|
85
|
+
* What the host hears the program end through and suspends it with.
|
|
86
|
+
* Defaults to `process`; tests pass an emitter that records `kill`.
|
|
87
|
+
*/
|
|
88
|
+
process?: NodeProcess;
|
|
89
|
+
}
|
|
90
|
+
export declare class NodeTerminalHost implements TerminalHost {
|
|
91
|
+
private readonly stdin;
|
|
92
|
+
private readonly stdout;
|
|
93
|
+
private readonly process;
|
|
94
|
+
readonly env: Env;
|
|
95
|
+
private readonly dataHandlers;
|
|
96
|
+
private readonly resizeHandlers;
|
|
97
|
+
private dataListener;
|
|
98
|
+
private resizeListener;
|
|
99
|
+
private rawModeRequested;
|
|
100
|
+
constructor(options?: NodeTerminalHostOptions);
|
|
101
|
+
get isTTY(): boolean;
|
|
102
|
+
get writesToTerminal(): boolean;
|
|
103
|
+
write(data: Uint8Array | string): void;
|
|
104
|
+
size(): TerminalSize;
|
|
105
|
+
setRawMode(raw: boolean): void;
|
|
106
|
+
private applyRawMode;
|
|
107
|
+
onData(handler: DataHandler): Unsubscribe;
|
|
108
|
+
onResize(handler: ResizeHandler): Unsubscribe;
|
|
109
|
+
onExit(handler: ExitHandler): Unsubscribe;
|
|
110
|
+
suspend(): Promise<void>;
|
|
111
|
+
start(): void;
|
|
112
|
+
stop(): void;
|
|
113
|
+
}
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:terminal-host — the `TerminalHost` implementation backed by node's
|
|
3
|
+
* process streams.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:locality-or-seam] The `TerminalHost` interface is the seam and lives
|
|
6
|
+
* in `host/terminal-host.ts`, importable anywhere. *This* file is one
|
|
7
|
+
* value satisfying that interface, and it reads `process.stdin` /
|
|
8
|
+
* `process.stdout`, so it is a node subpath:
|
|
9
|
+
*
|
|
10
|
+
* import { NodeTerminalHost } from "@promptctl/rich-js/node/terminal-host";
|
|
11
|
+
* const screen = new Screen(new NodeTerminalHost());
|
|
12
|
+
*
|
|
13
|
+
* The ambient `process` global is not an import, so it slips past the
|
|
14
|
+
* module-graph reasoning that keeps the main barrel browser-safe: reading
|
|
15
|
+
* `process.stdin` inside a constructor throws in a browser only when someone
|
|
16
|
+
* constructs it. Moving the class here makes the boundary structural — a
|
|
17
|
+
* browser bundle that never reaches for this subpath cannot reach the global
|
|
18
|
+
* either.
|
|
19
|
+
*
|
|
20
|
+
* [LAW:no-shared-mutable-globals] Which files may read the host, and what
|
|
21
|
+
* each takes off it, is `HOST_ACCESS` in `test/seam/ambient-process.ts` —
|
|
22
|
+
* this file's entry carries its `why`, and a test fails when anything joins
|
|
23
|
+
* the list. Deliberately not restated here: the sole-ownership sentence that
|
|
24
|
+
* used to stand in this spot is what that list replaced.
|
|
25
|
+
*
|
|
26
|
+
* So if you find yourself reaching for `process.stdin`, `process.stdout`, or
|
|
27
|
+
* `setRawMode` in the runtime or in a demo, take a `TerminalHost` parameter
|
|
28
|
+
* instead. The seam is already here.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The events a program ends on without anyone having signalled it. `exit`
|
|
32
|
+
* is every orderly end, `process.exit` included. `uncaughtExceptionMonitor`
|
|
33
|
+
* is a crash, and node emits it ahead of every `uncaughtException` listener
|
|
34
|
+
* and ahead of its own report — so a terminal handed back here is handed back
|
|
35
|
+
* before any crash report prints, `installTraceback`'s included, whatever
|
|
36
|
+
* order the listeners were added in.
|
|
37
|
+
*/
|
|
38
|
+
const ENDINGS = ["exit", "uncaughtExceptionMonitor"];
|
|
39
|
+
/** The signals that end a program from outside while it holds the terminal. */
|
|
40
|
+
const TERMINATIONS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
41
|
+
const DEFAULT_COLS = 80;
|
|
42
|
+
const DEFAULT_ROWS = 24;
|
|
43
|
+
export class NodeTerminalHost {
|
|
44
|
+
stdin;
|
|
45
|
+
stdout;
|
|
46
|
+
process;
|
|
47
|
+
env;
|
|
48
|
+
dataHandlers = new Set();
|
|
49
|
+
resizeHandlers = new Set();
|
|
50
|
+
dataListener;
|
|
51
|
+
resizeListener;
|
|
52
|
+
rawModeRequested = false;
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.stdin = options.stdin ?? process.stdin;
|
|
55
|
+
this.stdout = options.stdout ?? process.stdout;
|
|
56
|
+
this.env = options.env ?? process.env;
|
|
57
|
+
this.process = options.process ?? process;
|
|
58
|
+
}
|
|
59
|
+
get isTTY() {
|
|
60
|
+
return !!this.stdin.isTTY && this.writesToTerminal;
|
|
61
|
+
}
|
|
62
|
+
get writesToTerminal() {
|
|
63
|
+
return !!this.stdout.isTTY;
|
|
64
|
+
}
|
|
65
|
+
write(data) {
|
|
66
|
+
this.stdout.write(data);
|
|
67
|
+
}
|
|
68
|
+
size() {
|
|
69
|
+
return {
|
|
70
|
+
cols: this.stdout.columns ?? DEFAULT_COLS,
|
|
71
|
+
rows: this.stdout.rows ?? DEFAULT_ROWS,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
setRawMode(raw) {
|
|
75
|
+
this.rawModeRequested = raw;
|
|
76
|
+
this.applyRawMode(raw);
|
|
77
|
+
}
|
|
78
|
+
// [LAW:single-enforcer] One site decides what to do when toggling raw
|
|
79
|
+
// mode on the underlying transport fails. Both `setRawMode` (call from
|
|
80
|
+
// outside) and `stop` (cleanup) go through this helper so neither
|
|
81
|
+
// caller has to branch on capability or exception-handling.
|
|
82
|
+
//
|
|
83
|
+
// [LAW:dataflow-not-control-flow] Two shapes of "the transport can't do
|
|
84
|
+
// this" are absorbed identically: the optional chain handles "method
|
|
85
|
+
// missing" (non-TTY streams, PassThrough in tests); the try/catch
|
|
86
|
+
// handles "method exists but threw" (node's `setRawMode` throws on
|
|
87
|
+
// closed or non-TTY streams). Callers see neither shape — they always
|
|
88
|
+
// call `setRawMode(bool)`, and the host decides what's possible.
|
|
89
|
+
//
|
|
90
|
+
// The catch is silent because the only call sites are lifecycle
|
|
91
|
+
// transitions: if the stream is already gone, there is nothing
|
|
92
|
+
// semantically meaningful for the host to do, and surfacing the
|
|
93
|
+
// exception would only make teardown brittle without giving the caller
|
|
94
|
+
// a recovery path. The demos this replaces did `try { ... } catch {}`
|
|
95
|
+
// at every call site for exactly this reason — absorbing it once here
|
|
96
|
+
// means the pattern stops spreading.
|
|
97
|
+
applyRawMode(raw) {
|
|
98
|
+
try {
|
|
99
|
+
this.stdin.setRawMode?.(raw);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Underlying stream couldn't honor the request (closed, no longer
|
|
103
|
+
// a TTY, etc.). Lifecycle continues.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
onData(handler) {
|
|
107
|
+
this.dataHandlers.add(handler);
|
|
108
|
+
// [LAW:single-enforcer] One underlying `data` listener fans out to all
|
|
109
|
+
// subscribers — keeps stdin's flowing/paused state derivable from
|
|
110
|
+
// "any subscribers?" instead of from per-call ref-counts scattered
|
|
111
|
+
// through the runtime.
|
|
112
|
+
if (!this.dataListener) {
|
|
113
|
+
this.dataListener = (chunk) => {
|
|
114
|
+
for (const h of this.dataHandlers)
|
|
115
|
+
h(chunk);
|
|
116
|
+
};
|
|
117
|
+
this.stdin.on("data", this.dataListener);
|
|
118
|
+
this.stdin.resume?.();
|
|
119
|
+
}
|
|
120
|
+
return () => {
|
|
121
|
+
this.dataHandlers.delete(handler);
|
|
122
|
+
if (this.dataHandlers.size === 0 && this.dataListener) {
|
|
123
|
+
this.stdin.off("data", this.dataListener);
|
|
124
|
+
this.dataListener = undefined;
|
|
125
|
+
// Returning stdin to paused state mirrors the symmetric "no
|
|
126
|
+
// subscribers → no flow" invariant. Without this, removing the
|
|
127
|
+
// last subscriber leaves stdin in flowing mode and the process
|
|
128
|
+
// hangs forever waiting for an event nobody is listening for.
|
|
129
|
+
this.stdin.pause?.();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
onResize(handler) {
|
|
134
|
+
this.resizeHandlers.add(handler);
|
|
135
|
+
if (!this.resizeListener) {
|
|
136
|
+
this.resizeListener = () => {
|
|
137
|
+
const size = this.size();
|
|
138
|
+
for (const h of this.resizeHandlers)
|
|
139
|
+
h(size);
|
|
140
|
+
};
|
|
141
|
+
this.stdout.on("resize", this.resizeListener);
|
|
142
|
+
}
|
|
143
|
+
return () => {
|
|
144
|
+
this.resizeHandlers.delete(handler);
|
|
145
|
+
if (this.resizeHandlers.size === 0 && this.resizeListener) {
|
|
146
|
+
this.stdout.off("resize", this.resizeListener);
|
|
147
|
+
this.resizeListener = undefined;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
onExit(handler) {
|
|
152
|
+
const proc = this.process;
|
|
153
|
+
// [LAW:single-enforcer] Every path detaches every listener before the
|
|
154
|
+
// handler runs, which is what makes it run once: the second event of a
|
|
155
|
+
// crash — the monitor, then `exit` — finds nothing left to call.
|
|
156
|
+
const end = () => {
|
|
157
|
+
detach();
|
|
158
|
+
handler();
|
|
159
|
+
};
|
|
160
|
+
// A signal a program listens for no longer terminates it, so the
|
|
161
|
+
// listener owes the signal its outcome. Raised again once ours is gone,
|
|
162
|
+
// it terminates — unless the program listens for it too, in which case
|
|
163
|
+
// it was never going to, and that listener hears it next. Ours goes
|
|
164
|
+
// first and leaves before the rest are called: a listener keeping the
|
|
165
|
+
// same rule (`signal-exit`'s) then finds itself alone and raises it, where
|
|
166
|
+
// two that each counted the other would both have stood down.
|
|
167
|
+
const terminations = TERMINATIONS.map((signal) => {
|
|
168
|
+
const listener = () => {
|
|
169
|
+
end();
|
|
170
|
+
if (proc.listenerCount(signal) === 0)
|
|
171
|
+
proc.kill(proc.pid, signal);
|
|
172
|
+
};
|
|
173
|
+
return { signal, listener };
|
|
174
|
+
});
|
|
175
|
+
const detach = () => {
|
|
176
|
+
for (const event of ENDINGS)
|
|
177
|
+
proc.off(event, end);
|
|
178
|
+
for (const { signal, listener } of terminations)
|
|
179
|
+
proc.off(signal, listener);
|
|
180
|
+
};
|
|
181
|
+
for (const event of ENDINGS)
|
|
182
|
+
proc.on(event, end);
|
|
183
|
+
for (const { signal, listener } of terminations)
|
|
184
|
+
proc.prependListener(signal, listener);
|
|
185
|
+
return detach;
|
|
186
|
+
}
|
|
187
|
+
// The shell's own job control: the signal Ctrl+Z would have sent outside
|
|
188
|
+
// raw mode, to the whole foreground job as the terminal sends it — a
|
|
189
|
+
// launcher (`npm run`, `sh -c`) waiting on this process stops too, so the
|
|
190
|
+
// shell sees the job stop and takes the terminal. A signal a process sends
|
|
191
|
+
// its own group is delivered before `kill` returns, so the process is
|
|
192
|
+
// stopped inside the call and running again when it returns: `fg` has
|
|
193
|
+
// continued it, or nothing was stopped at all — the kernel discards the
|
|
194
|
+
// signal in a job no shell controls, as it does Ctrl+Z's. Either way the
|
|
195
|
+
// terminal is the program's again.
|
|
196
|
+
suspend() {
|
|
197
|
+
this.process.kill(0, "SIGTSTP");
|
|
198
|
+
return Promise.resolve();
|
|
199
|
+
}
|
|
200
|
+
start() {
|
|
201
|
+
// No-op on node: stdin/stdout are always available. Lifecycle exists
|
|
202
|
+
// so `BrowserTerminalHost` can attach to the DOM here without the
|
|
203
|
+
// runtime branching on "do I need to start?"
|
|
204
|
+
}
|
|
205
|
+
stop() {
|
|
206
|
+
// [LAW:one-source-of-truth] Symmetric teardown: drop every subscriber
|
|
207
|
+
// and detach the underlying stream listeners. After stop(), the host
|
|
208
|
+
// is in the same shape a freshly-constructed one would be in. Calling
|
|
209
|
+
// stop() repeatedly is safe — the dataListener/resizeListener guards
|
|
210
|
+
// make later calls no-ops.
|
|
211
|
+
if (this.dataListener) {
|
|
212
|
+
this.stdin.off("data", this.dataListener);
|
|
213
|
+
this.dataListener = undefined;
|
|
214
|
+
this.stdin.pause?.();
|
|
215
|
+
}
|
|
216
|
+
if (this.resizeListener) {
|
|
217
|
+
this.stdout.off("resize", this.resizeListener);
|
|
218
|
+
this.resizeListener = undefined;
|
|
219
|
+
}
|
|
220
|
+
this.dataHandlers.clear();
|
|
221
|
+
this.resizeHandlers.clear();
|
|
222
|
+
if (this.rawModeRequested) {
|
|
223
|
+
this.applyRawMode(false);
|
|
224
|
+
this.rawModeRequested = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:traceback — install `Traceback` as the process-wide crash renderer.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:locality-or-seam] `process.on` / `process.exit` live only here, on the
|
|
5
|
+
* node side of the API boundary. The `Traceback` renderable itself is pure
|
|
6
|
+
* rendering and stays in the browser-safe barrel; installing it as a global
|
|
7
|
+
* handler is a node capability, so it is a node subpath:
|
|
8
|
+
*
|
|
9
|
+
* import { installTraceback } from "@promptctl/rich-js/node/traceback";
|
|
10
|
+
* installTraceback({ suppress: ["node_modules"] });
|
|
11
|
+
*
|
|
12
|
+
* [LAW:dataflow-not-control-flow] Every crash is reported from one channel,
|
|
13
|
+
* `uncaughtException`. An unhandled rejection is thrown into it rather than
|
|
14
|
+
* reported beside it, so both kinds of crash are literally one code path. No
|
|
15
|
+
* payload is trusted to be an `Error`: `@types/node` annotates
|
|
16
|
+
* `uncaughtException` with `Error`, but `throw "boom"` delivers the raw
|
|
17
|
+
* string, so the annotation is a claim about the common case rather than a
|
|
18
|
+
* guarantee. `report` takes `unknown` and hands it to `Traceback` as it came,
|
|
19
|
+
* which reads a caught value of any shape.
|
|
20
|
+
*
|
|
21
|
+
* [LAW:no-ambient-temporal-coupling] The one channel is also what puts the
|
|
22
|
+
* report after the terminal is handed back. A program holding the alternate
|
|
23
|
+
* screen restores it on `uncaughtExceptionMonitor` (`NodeTerminalHost.onExit`),
|
|
24
|
+
* which node emits ahead of every `uncaughtException` listener. Reported
|
|
25
|
+
* straight from `unhandledRejection`, a crash skips that phase: the report
|
|
26
|
+
* draws inside the alternate screen and vanishes with it when `exit` restores
|
|
27
|
+
* the terminal. Node's crash sequence owns the order, not the order in which
|
|
28
|
+
* `installTraceback` and a host happened to add their listeners.
|
|
29
|
+
*/
|
|
30
|
+
import type { TracebackOptions } from "../renderables/traceback.js";
|
|
31
|
+
/**
|
|
32
|
+
* Register rich tracebacks for uncaught exceptions and unhandled rejections.
|
|
33
|
+
*
|
|
34
|
+
* Calling this more than once replaces the previous handler — the last call
|
|
35
|
+
* wins, options included — so a process always has exactly one rich crash
|
|
36
|
+
* renderer no matter how many entry points reach for it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function installTraceback(options?: TracebackOptions): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:traceback — install `Traceback` as the process-wide crash renderer.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:locality-or-seam] `process.on` / `process.exit` live only here, on the
|
|
5
|
+
* node side of the API boundary. The `Traceback` renderable itself is pure
|
|
6
|
+
* rendering and stays in the browser-safe barrel; installing it as a global
|
|
7
|
+
* handler is a node capability, so it is a node subpath:
|
|
8
|
+
*
|
|
9
|
+
* import { installTraceback } from "@promptctl/rich-js/node/traceback";
|
|
10
|
+
* installTraceback({ suppress: ["node_modules"] });
|
|
11
|
+
*
|
|
12
|
+
* [LAW:dataflow-not-control-flow] Every crash is reported from one channel,
|
|
13
|
+
* `uncaughtException`. An unhandled rejection is thrown into it rather than
|
|
14
|
+
* reported beside it, so both kinds of crash are literally one code path. No
|
|
15
|
+
* payload is trusted to be an `Error`: `@types/node` annotates
|
|
16
|
+
* `uncaughtException` with `Error`, but `throw "boom"` delivers the raw
|
|
17
|
+
* string, so the annotation is a claim about the common case rather than a
|
|
18
|
+
* guarantee. `report` takes `unknown` and hands it to `Traceback` as it came,
|
|
19
|
+
* which reads a caught value of any shape.
|
|
20
|
+
*
|
|
21
|
+
* [LAW:no-ambient-temporal-coupling] The one channel is also what puts the
|
|
22
|
+
* report after the terminal is handed back. A program holding the alternate
|
|
23
|
+
* screen restores it on `uncaughtExceptionMonitor` (`NodeTerminalHost.onExit`),
|
|
24
|
+
* which node emits ahead of every `uncaughtException` listener. Reported
|
|
25
|
+
* straight from `unhandledRejection`, a crash skips that phase: the report
|
|
26
|
+
* draws inside the alternate screen and vanishes with it when `exit` restores
|
|
27
|
+
* the terminal. Node's crash sequence owns the order, not the order in which
|
|
28
|
+
* `installTraceback` and a host happened to add their listeners.
|
|
29
|
+
*/
|
|
30
|
+
import { Console } from "../core/console.js";
|
|
31
|
+
import { Traceback } from "../renderables/traceback.js";
|
|
32
|
+
/**
|
|
33
|
+
* The handler currently registered, or a no-op standing in for "none yet".
|
|
34
|
+
*
|
|
35
|
+
* [LAW:no-shared-mutable-globals] The process listener registry is a shared
|
|
36
|
+
* global, so the handler installed into it gets a single owner here rather
|
|
37
|
+
* than a caller appending to it once per call. Holding the reference is what
|
|
38
|
+
* lets `installTraceback` replace rather than accumulate.
|
|
39
|
+
*
|
|
40
|
+
* [LAW:types-are-the-program] A no-op rather than `null`: `process.off` on an
|
|
41
|
+
* unregistered listener does nothing, so the first call needs no null check
|
|
42
|
+
* and no "have we installed yet" branch. There is no absent case to handle.
|
|
43
|
+
*/
|
|
44
|
+
let installed = () => { };
|
|
45
|
+
/**
|
|
46
|
+
* An unhandled rejection, raised as the uncaught exception it becomes when
|
|
47
|
+
* nobody listens — its reason unwrapped, which node's own conversion would
|
|
48
|
+
* bury in a message for any reason that is not an `Error`. Raised once the
|
|
49
|
+
* rejection's other listeners have heard it: a throw inside the emit would
|
|
50
|
+
* end it before them.
|
|
51
|
+
*/
|
|
52
|
+
function escalate(reason) {
|
|
53
|
+
queueMicrotask(() => {
|
|
54
|
+
throw reason;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Register rich tracebacks for uncaught exceptions and unhandled rejections.
|
|
59
|
+
*
|
|
60
|
+
* Calling this more than once replaces the previous handler — the last call
|
|
61
|
+
* wins, options included — so a process always has exactly one rich crash
|
|
62
|
+
* renderer no matter how many entry points reach for it.
|
|
63
|
+
*/
|
|
64
|
+
export function installTraceback(options) {
|
|
65
|
+
// [LAW:no-ambient-temporal-coupling] "This process is already crashing" is
|
|
66
|
+
// lifecycle state with one owner, not a fact left to the interleaving of two
|
|
67
|
+
// writes. A cascade — cleanup code rejecting a promise while the first
|
|
68
|
+
// report is still draining — would otherwise start a second write and a
|
|
69
|
+
// second `process.exit(1)`, and whichever drained first would cut the other
|
|
70
|
+
// off mid-frame. The first crash is the fault and the rest is its wake, so
|
|
71
|
+
// one complete report of the fault beats two corrupt ones.
|
|
72
|
+
let crashing = false;
|
|
73
|
+
const report = (reason) => {
|
|
74
|
+
if (crashing)
|
|
75
|
+
return;
|
|
76
|
+
crashing = true;
|
|
77
|
+
// [LAW:effects-at-boundaries] Render to a string first, then perform one
|
|
78
|
+
// write. `beginCapture` redirects the console's sink without setting
|
|
79
|
+
// `file:`, so stderr's TTY status still drives colour depth — a `file:`
|
|
80
|
+
// sink would report `isTTY === false` and silently strip colour.
|
|
81
|
+
const out = new Console({ stderr: true });
|
|
82
|
+
out.beginCapture();
|
|
83
|
+
out.print(new Traceback(reason, options));
|
|
84
|
+
// [LAW:no-silent-failure] Exit only once the bytes have drained. When
|
|
85
|
+
// stderr is a pipe (CI logs, `2>&1 | tee`), writes are asynchronous and
|
|
86
|
+
// an immediate `process.exit(1)` truncates the traceback at the pipe
|
|
87
|
+
// buffer — the longer the traceback, the more of it is lost, which is
|
|
88
|
+
// the reverse of what a crash report should do.
|
|
89
|
+
process.stderr.write(out.endCapture(), () => {
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
process.off("uncaughtException", installed);
|
|
94
|
+
process.off("unhandledRejection", escalate);
|
|
95
|
+
process.on("uncaughtException", report);
|
|
96
|
+
process.on("unhandledRejection", escalate);
|
|
97
|
+
installed = report;
|
|
98
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Align — wraps a renderable and places its output horizontally, as one block.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "../core/segment.js";
|
|
5
|
+
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
6
|
+
import { type Alignment } from "../core/place.js";
|
|
7
|
+
export declare class Align implements Renderable, Measurable {
|
|
8
|
+
readonly renderable: Renderable;
|
|
9
|
+
readonly align: Alignment;
|
|
10
|
+
constructor(renderable: Renderable, align?: Alignment);
|
|
11
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
12
|
+
measure(options: RenderOptions): {
|
|
13
|
+
minimum: number;
|
|
14
|
+
maximum: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Align — wraps a renderable and places its output horizontally, as one block.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "../core/segment.js";
|
|
5
|
+
import { Measurement } from "../core/measure.js";
|
|
6
|
+
import { withBoundedWidth } from "../core/protocol.js";
|
|
7
|
+
import { placeBlock } from "../core/place.js";
|
|
8
|
+
export class Align {
|
|
9
|
+
renderable;
|
|
10
|
+
align;
|
|
11
|
+
constructor(renderable, align = "center") {
|
|
12
|
+
this.renderable = renderable;
|
|
13
|
+
this.align = align;
|
|
14
|
+
}
|
|
15
|
+
// [LAW:one-source-of-truth] `placeBlock` is the placement; `Console.print`'s
|
|
16
|
+
// `justify` uses the same one.
|
|
17
|
+
*render(options) {
|
|
18
|
+
for (const line of placeBlock(this.renderable, this.align, withBoundedWidth(options, this))) {
|
|
19
|
+
yield* line;
|
|
20
|
+
yield Segment.line();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
measure(options) {
|
|
24
|
+
return Measurement.get(options, this.renderable);
|
|
25
|
+
}
|
|
26
|
+
}
|