@promptctl/rich-js 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -26
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +0 -1
- package/dist/core/box.js +6 -18
- package/dist/core/cells.d.ts +16 -5
- package/dist/core/cells.js +31 -10
- package/dist/core/color.d.ts +4 -8
- package/dist/core/color.js +28 -19
- package/dist/core/console.d.ts +13 -4
- package/dist/core/console.js +91 -36
- package/dist/core/export-html.d.ts +40 -5
- package/dist/core/export-html.js +111 -17
- package/dist/core/markup.d.ts +27 -5
- package/dist/core/markup.js +282 -170
- package/dist/core/measure.d.ts +1 -1
- package/dist/core/measure.js +2 -1
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +3 -0
- package/dist/core/pretty.js +5 -1
- package/dist/core/protocol.d.ts +136 -6
- package/dist/core/protocol.js +64 -2
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +2 -2
- package/dist/core/segment.d.ts +23 -4
- package/dist/core/segment.js +75 -16
- package/dist/core/spinnerData.d.ts +2 -0
- package/dist/core/spinnerData.js +3 -1
- package/dist/core/strip.d.ts +3 -7
- package/dist/core/strip.js +32 -35
- package/dist/core/style.d.ts +25 -0
- package/dist/core/style.js +61 -89
- package/dist/core/text.d.ts +16 -15
- package/dist/core/text.js +62 -76
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +4 -2
- package/dist/host/index.js +2 -1
- package/dist/host/terminal-host.d.ts +43 -7
- package/dist/host/terminal-host.js +57 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/node/terminal-host.d.ts +31 -2
- package/dist/node/terminal-host.js +69 -2
- package/dist/node/traceback.d.ts +16 -6
- package/dist/node/traceback.js +30 -8
- package/dist/renderables/align.d.ts +2 -2
- package/dist/renderables/align.js +6 -19
- package/dist/renderables/columns.js +4 -20
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/group.js +3 -1
- package/dist/renderables/layout.js +31 -34
- package/dist/renderables/live.d.ts +6 -3
- package/dist/renderables/live.js +89 -45
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.js +6 -4
- package/dist/renderables/padding.js +3 -2
- package/dist/renderables/panel.js +26 -41
- package/dist/renderables/progress.js +5 -8
- package/dist/renderables/progressBar.js +6 -5
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +9 -9
- package/dist/renderables/spinner.d.ts +5 -3
- package/dist/renderables/spinner.js +14 -8
- package/dist/renderables/syntax.d.ts +1 -1
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +26 -14
- package/dist/renderables/table.js +239 -141
- package/dist/renderables/traceback.js +8 -9
- package/dist/renderables/tree.d.ts +17 -3
- package/dist/renderables/tree.js +62 -71
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.js +11 -13
- package/dist/template-bindings/index.js +4 -2
- package/dist/template-bindings/palette-funcs.js +6 -16
- package/dist/template-bindings/style-funcs.js +4 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.js +19 -0
- package/dist/themes/data/atom-one-light.js +19 -0
- package/dist/themes/data/catppuccin-frappe.js +19 -0
- package/dist/themes/data/catppuccin-latte.js +19 -0
- package/dist/themes/data/catppuccin-macchiato.js +19 -0
- package/dist/themes/data/catppuccin-mocha.js +19 -0
- package/dist/themes/data/cyberpunk.js +2 -0
- package/dist/themes/data/default.js +2 -0
- package/dist/themes/data/dracula.js +19 -0
- package/dist/themes/data/flexoki.js +19 -0
- package/dist/themes/data/gruvbox.js +19 -0
- package/dist/themes/data/monokai.js +19 -0
- package/dist/themes/data/nord.js +19 -0
- package/dist/themes/data/rose-pine-dawn.js +19 -0
- package/dist/themes/data/rose-pine-moon.js +19 -0
- package/dist/themes/data/rose-pine.js +19 -0
- package/dist/themes/data/solarized-dark.js +19 -0
- package/dist/themes/data/solarized-light.js +19 -0
- package/dist/themes/data/svg-export.js +19 -0
- package/dist/themes/data/textual-ansi.js +2 -0
- package/dist/themes/data/textual-dark.js +2 -0
- package/dist/themes/data/textual-light.js +2 -0
- package/dist/themes/data/tokyo-night.js +19 -0
- package/dist/themes/data/types.d.ts +16 -0
- package/dist/themes/data/types.js +10 -1
- package/dist/themes/registry.d.ts +3 -2
- package/dist/themes/registry.js +13 -1
- package/dist/themes/terminalThemes.d.ts +2 -7
- package/dist/themes/terminalThemes.js +4 -9
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +4 -2
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +5 -3
- package/dist/widgets/dropdown.d.ts +2 -2
- package/dist/widgets/dropdown.js +13 -16
- package/dist/widgets/event-router.d.ts +21 -28
- package/dist/widgets/event-router.js +80 -73
- package/dist/widgets/focus-manager.d.ts +25 -6
- package/dist/widgets/focus-manager.js +63 -65
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +5 -7
- package/dist/widgets/index.js +3 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/slider.js +7 -7
- package/dist/widgets/static-item.d.ts +9 -15
- package/dist/widgets/static-item.js +9 -15
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +17 -14
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +3 -2
- package/dist/widgets/types.d.ts +14 -35
- package/dist/widgets/types.js +0 -1
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +4 -6
- package/dist/widgets/widget-base.js +22 -30
- package/package.json +12 -4
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hostEnvironment — a `TerminalHost` as the `ConsoleEnvironment` a `Console`
|
|
3
|
+
* consults, so a console writing through a host asks that host every question
|
|
4
|
+
* it has about its destination.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:one-source-of-truth] A console asks its destination four things: where
|
|
7
|
+
* bytes go, is it a terminal, how big is it, and — through the environment —
|
|
8
|
+
* what colours it draws. A bare sink answers only the first, and the other
|
|
9
|
+
* three fall to the ambient `process`: some other stream's answer in node,
|
|
10
|
+
* none in a browser, where a console on xterm.js saw an unnamed TTY and drew
|
|
11
|
+
* 16 colours. Handing the console the whole environment keeps all four
|
|
12
|
+
* answers the host's.
|
|
13
|
+
*
|
|
14
|
+
* The host's size is `size()`, so the environment handed on carries no
|
|
15
|
+
* `COLUMNS` or `LINES`: a console reads those ahead of the stream, and an
|
|
16
|
+
* exported `COLUMNS` in the shell that launched a node host would pin the
|
|
17
|
+
* console's width while the terminal it draws on is resized. The stream's
|
|
18
|
+
* `columns` and `rows` are getters, so a console that was not given a fixed
|
|
19
|
+
* size follows the host through resizes.
|
|
20
|
+
*/
|
|
21
|
+
// [LAW:types-are-the-program] A host always has a stream, so the return type
|
|
22
|
+
// says so rather than inheriting `ConsoleEnvironment`'s optional ones. It is
|
|
23
|
+
// one terminal, so stdout and stderr are the same stream — which is where a
|
|
24
|
+
// program's two streams land when both are attached to a terminal.
|
|
25
|
+
export function hostEnvironment(host) {
|
|
26
|
+
const { COLUMNS: _columns, LINES: _lines, ...env } = host.env;
|
|
27
|
+
const stream = {
|
|
28
|
+
write(chunk) {
|
|
29
|
+
host.write(chunk);
|
|
30
|
+
},
|
|
31
|
+
// [LAW:one-source-of-truth] A console asks whether its output is a
|
|
32
|
+
// terminal, which is the host's `writesToTerminal`, not its `isTTY`.
|
|
33
|
+
get isTTY() {
|
|
34
|
+
return host.writesToTerminal;
|
|
35
|
+
},
|
|
36
|
+
get columns() {
|
|
37
|
+
return host.size().cols;
|
|
38
|
+
},
|
|
39
|
+
get rows() {
|
|
40
|
+
return host.size().rows;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return { env, stdout: stream, stderr: stream };
|
|
44
|
+
}
|
package/dist/host/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { BrowserTerminalHost } from "./terminal-host.js";
|
|
2
|
-
export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
3
|
-
export {
|
|
2
|
+
export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
3
|
+
export { hostEnvironment } from "./host-environment.js";
|
|
4
|
+
export { App } from "./app.js";
|
|
5
|
+
export type { AppOptions, AppPhase, Surface } from "./app.js";
|
package/dist/host/index.js
CHANGED
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
*
|
|
33
33
|
* [boundaries: capabilities over context] A `TerminalHost` grants exactly
|
|
34
34
|
* the I/O capabilities the runtime needs: write bytes, read input, query
|
|
35
|
-
* size, observe resize, switch raw mode,
|
|
36
|
-
*
|
|
35
|
+
* size, observe resize, switch raw mode, read the terminal's environment,
|
|
36
|
+
* hear the program end, hand the terminal back for a while, lifecycle. It is
|
|
37
|
+
* not an omniscient handle to "the process."
|
|
37
38
|
*/
|
|
38
39
|
import type { Unsubscribe } from "../core/subscription.js";
|
|
39
40
|
export interface TerminalSize {
|
|
@@ -42,10 +43,14 @@ export interface TerminalSize {
|
|
|
42
43
|
}
|
|
43
44
|
export type DataHandler = (chunk: Uint8Array | string) => void;
|
|
44
45
|
export type ResizeHandler = (size: TerminalSize) => void;
|
|
46
|
+
export type ExitHandler = () => void;
|
|
45
47
|
export interface TerminalHost {
|
|
46
48
|
/**
|
|
47
|
-
* Write raw bytes (or UTF-8 text) to the terminal
|
|
48
|
-
*
|
|
49
|
+
* Write raw bytes (or UTF-8 text) to the terminal as a program's output
|
|
50
|
+
* reaches it: no escape interpretation, no framing, and a terminal on a
|
|
51
|
+
* tty receives each newline as carriage return + newline, the tty's own
|
|
52
|
+
* output translation, which node's raw mode leaves on. A host with no tty
|
|
53
|
+
* in front of its terminal does that translation itself.
|
|
49
54
|
*/
|
|
50
55
|
write(data: Uint8Array | string): void;
|
|
51
56
|
/**
|
|
@@ -68,11 +73,38 @@ export interface TerminalHost {
|
|
|
68
73
|
*/
|
|
69
74
|
setRawMode(raw: boolean): void;
|
|
70
75
|
/**
|
|
71
|
-
* Whether the host is connected to a real interactive terminal
|
|
72
|
-
*
|
|
73
|
-
* runtime; non-TTY hosts default those features off.
|
|
76
|
+
* Whether the host is connected to a real interactive terminal — input and
|
|
77
|
+
* output both: what an interactive program checks before it starts.
|
|
74
78
|
*/
|
|
75
79
|
readonly isTTY: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the bytes `write` sends land on a terminal, whatever the input
|
|
82
|
+
* is. Colour detection asks this: a program with piped stdin and a terminal
|
|
83
|
+
* stdout still draws on a terminal, and `isTTY` would call it plain.
|
|
84
|
+
*/
|
|
85
|
+
readonly writesToTerminal: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The environment a program running on this terminal sees — `TERM`,
|
|
88
|
+
* `COLORTERM`, `NO_COLOR` and the rest. Colour detection reads it: what a
|
|
89
|
+
* terminal can draw is a fact about the host, so it travels with the host
|
|
90
|
+
* rather than being read off whatever `process` the renderer happens to
|
|
91
|
+
* share a runtime with, which in a browser is none.
|
|
92
|
+
*/
|
|
93
|
+
readonly env: NodeJS.ProcessEnv;
|
|
94
|
+
/**
|
|
95
|
+
* Run `handler` once, when the program ends by any path this host can
|
|
96
|
+
* observe, so whoever changed the terminal can hand it back. The program
|
|
97
|
+
* still ends as it would have without the handler: a signal still
|
|
98
|
+
* terminates and a crash is still reported.
|
|
99
|
+
*/
|
|
100
|
+
onExit(handler: ExitHandler): Unsubscribe;
|
|
101
|
+
/**
|
|
102
|
+
* Hand the terminal to whatever launched the program, and resolve once it
|
|
103
|
+
* hands it back. Raw mode delivers Ctrl+Z as a key rather than a signal, so
|
|
104
|
+
* suspending is something a program asks for, and it restores the terminal
|
|
105
|
+
* before asking.
|
|
106
|
+
*/
|
|
107
|
+
suspend(): Promise<void>;
|
|
76
108
|
/**
|
|
77
109
|
* Begin the host's lifecycle. Implementations attach whatever resources
|
|
78
110
|
* they own — or no-op when there is nothing to attach. Idempotent. See
|
|
@@ -133,9 +165,13 @@ export declare class BrowserTerminalHost implements TerminalHost {
|
|
|
133
165
|
private resizeSubscription;
|
|
134
166
|
constructor(options: BrowserTerminalHostOptions);
|
|
135
167
|
get isTTY(): boolean;
|
|
168
|
+
get writesToTerminal(): boolean;
|
|
169
|
+
get env(): NodeJS.ProcessEnv;
|
|
136
170
|
write(data: Uint8Array | string): void;
|
|
137
171
|
size(): TerminalSize;
|
|
138
172
|
setRawMode(_raw: boolean): void;
|
|
173
|
+
onExit(_handler: ExitHandler): Unsubscribe;
|
|
174
|
+
suspend(): Promise<void>;
|
|
139
175
|
onData(handler: DataHandler): Unsubscribe;
|
|
140
176
|
onResize(handler: ResizeHandler): Unsubscribe;
|
|
141
177
|
start(): void;
|
|
@@ -32,9 +32,14 @@
|
|
|
32
32
|
*
|
|
33
33
|
* [boundaries: capabilities over context] A `TerminalHost` grants exactly
|
|
34
34
|
* the I/O capabilities the runtime needs: write bytes, read input, query
|
|
35
|
-
* size, observe resize, switch raw mode,
|
|
36
|
-
*
|
|
35
|
+
* size, observe resize, switch raw mode, read the terminal's environment,
|
|
36
|
+
* hear the program end, hand the terminal back for a while, lifecycle. It is
|
|
37
|
+
* not an omniscient handle to "the process."
|
|
37
38
|
*/
|
|
39
|
+
const XTERM_ENV = Object.freeze({
|
|
40
|
+
TERM: "xterm-256color",
|
|
41
|
+
COLORTERM: "truecolor",
|
|
42
|
+
});
|
|
38
43
|
export class BrowserTerminalHost {
|
|
39
44
|
terminal;
|
|
40
45
|
dataHandlers = new Set();
|
|
@@ -51,8 +56,23 @@ export class BrowserTerminalHost {
|
|
|
51
56
|
get isTTY() {
|
|
52
57
|
return true;
|
|
53
58
|
}
|
|
59
|
+
get writesToTerminal() {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// What xterm.js is, stated the way a terminal states it: it identifies as
|
|
63
|
+
// `xterm-256color` and draws 24-bit SGR. With no env at all, detection saw
|
|
64
|
+
// an unnamed TTY and drew every colour at 16-colour depth.
|
|
65
|
+
get env() {
|
|
66
|
+
return XTERM_ENV;
|
|
67
|
+
}
|
|
68
|
+
// [LAW:single-enforcer] xterm.js is a terminal with no tty in front of
|
|
69
|
+
// it, so this host is where the tty's newline translation happens. Left to
|
|
70
|
+
// the terminal, a bare newline moves down without returning, and every row
|
|
71
|
+
// a Console writes starts where the last one ended — every demo drew as a
|
|
72
|
+
// staircase. Here and not in each page's xterm options, so every writer
|
|
73
|
+
// through this host gets it.
|
|
54
74
|
write(data) {
|
|
55
|
-
this.terminal.write(data);
|
|
75
|
+
this.terminal.write(returnBeforeNewline(data));
|
|
56
76
|
}
|
|
57
77
|
size() {
|
|
58
78
|
return { cols: this.terminal.cols, rows: this.terminal.rows };
|
|
@@ -64,6 +84,16 @@ export class BrowserTerminalHost {
|
|
|
64
84
|
// hosts. (Mirror: NodeTerminalHost on a non-TTY stream takes the same
|
|
65
85
|
// no-op path via the optional-chain in `applyRawMode`.)
|
|
66
86
|
setRawMode(_raw) { }
|
|
87
|
+
// The page owns the terminal's life: a program here ends when the page
|
|
88
|
+
// does, and nothing is left to restore.
|
|
89
|
+
onExit(_handler) {
|
|
90
|
+
return () => { };
|
|
91
|
+
}
|
|
92
|
+
// Nothing launched the program that could take the terminal back, so the
|
|
93
|
+
// hand-back is immediate.
|
|
94
|
+
suspend() {
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
67
97
|
// [LAW:single-enforcer] One underlying `terminal.onData` subscription
|
|
68
98
|
// fans out to every host-level handler — the same lazy-attach,
|
|
69
99
|
// last-unsubscribe-detaches shape as NodeTerminalHost.onData. The
|
|
@@ -130,3 +160,27 @@ export class BrowserTerminalHost {
|
|
|
130
160
|
this.resizeHandlers.clear();
|
|
131
161
|
}
|
|
132
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* `data` with a carriage return before every newline — the tty's ONLCR. A
|
|
165
|
+
* newline byte never occurs inside a multi-byte UTF-8 character, so the bytes
|
|
166
|
+
* are translated as bytes. A newline already preceded by a carriage return
|
|
167
|
+
* gains a second one, which returns to a column the cursor is already in, as
|
|
168
|
+
* the kernel's translation does.
|
|
169
|
+
*/
|
|
170
|
+
function returnBeforeNewline(data) {
|
|
171
|
+
if (typeof data === "string")
|
|
172
|
+
return data.replaceAll("\n", "\r\n");
|
|
173
|
+
let newlines = 0;
|
|
174
|
+
for (let i = 0; i < data.length; i++)
|
|
175
|
+
newlines += Number(data[i] === 0x0a);
|
|
176
|
+
if (newlines === 0)
|
|
177
|
+
return data;
|
|
178
|
+
const out = new Uint8Array(data.length + newlines);
|
|
179
|
+
let at = 0;
|
|
180
|
+
for (let i = 0; i < data.length; i++) {
|
|
181
|
+
if (data[i] === 0x0a)
|
|
182
|
+
out[at++] = 0x0d;
|
|
183
|
+
out[at++] = data[i];
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,11 +19,12 @@ export { Style, StyleSyntaxError, StyleStack, Theme, NULL_STYLE, DEFAULT_STYLES,
|
|
|
19
19
|
export type { StyleOptions } from "./core/style.js";
|
|
20
20
|
export { Segment, ControlType, } from "./core/segment.js";
|
|
21
21
|
export type { ControlCode } from "./core/segment.js";
|
|
22
|
+
export type { Anchor } from "./core/anchor.js";
|
|
22
23
|
export { Box } from "./core/box.js";
|
|
23
24
|
export type { EdgeChars, ContentChars, RowLevel, SubstituteOptions } from "./core/box.js";
|
|
24
25
|
export { ASCII, ASCII2, ASCII_DOUBLE_HEAD, SQUARE, SQUARE_DOUBLE_HEAD, MINIMAL, MINIMAL_HEAVY_HEAD, MINIMAL_DOUBLE_HEAD, SIMPLE, SIMPLE_HEAD, SIMPLE_HEAVY, HORIZONTALS, ROUNDED, HEAVY, HEAVY_EDGE, HEAVY_HEAD, DOUBLE, DOUBLE_EDGE, MARKDOWN, } from "./core/box.js";
|
|
25
|
-
export { isRenderable, isMeasurable, withCellWidth, withBoundedWidth, } from "./core/protocol.js";
|
|
26
|
-
export type { RenderOptions, Renderable, Measurable, StyleErrorHandler, } from "./core/protocol.js";
|
|
26
|
+
export { isRenderable, isMeasurable, withCellWidth, withBoundedWidth, insetHeight, stackedHeight, regionRows, fitHeight, getStyle, } from "./core/protocol.js";
|
|
27
|
+
export type { RenderOptions, OverflowMethod, Height, Renderable, Measurable, StyleErrorHandler, } from "./core/protocol.js";
|
|
27
28
|
export type { Unsubscribe } from "./core/subscription.js";
|
|
28
29
|
export { Measurement, measureRenderables } from "./core/measure.js";
|
|
29
30
|
export { Span, RichText } from "./core/text.js";
|
|
@@ -34,6 +35,7 @@ export { renderToString, segmentToString, segmentsToString, } from "./core/rende
|
|
|
34
35
|
export type { RenderToStringOptions } from "./core/render.js";
|
|
35
36
|
export { OSC8, osc8Sequences } from "./core/osc8.js";
|
|
36
37
|
export type { Osc8Sequence } from "./core/osc8.js";
|
|
38
|
+
export { AnsiDecoder, decodeAnsi } from "./core/ansi.js";
|
|
37
39
|
export { MarkupRegistry, globalMarkupRegistry, renderMarkup, } from "./core/markup.js";
|
|
38
40
|
export type { MarkupTagContext, MarkupTagHandler, RenderMarkupOptions, } from "./core/markup.js";
|
|
39
41
|
export { EMOJI, emojiReplace, Emoji, NoEmoji } from "./core/emoji.js";
|
|
@@ -49,9 +51,11 @@ export { Console } from "./core/console.js";
|
|
|
49
51
|
export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, PrintOptions, } from "./core/console.js";
|
|
50
52
|
export { Constrain } from "./renderables/constrain.js";
|
|
51
53
|
export { Align } from "./renderables/align.js";
|
|
52
|
-
export type { Alignment } from "./
|
|
54
|
+
export type { Alignment } from "./core/place.js";
|
|
53
55
|
export { Padding } from "./renderables/padding.js";
|
|
54
56
|
export type { PaddingDimensions } from "./renderables/padding.js";
|
|
57
|
+
export { Viewport, SCROLLBAR } from "./renderables/viewport.js";
|
|
58
|
+
export type { Scrollbar, ScrollbarPart, ViewportOptions } from "./renderables/viewport.js";
|
|
55
59
|
export { Rule } from "./renderables/rule.js";
|
|
56
60
|
export type { RuleAlign, RuleOptions } from "./renderables/rule.js";
|
|
57
61
|
export { Panel } from "./renderables/panel.js";
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ export { Segment, ControlType, } from "./core/segment.js";
|
|
|
32
32
|
export { Box } from "./core/box.js";
|
|
33
33
|
export { ASCII, ASCII2, ASCII_DOUBLE_HEAD, SQUARE, SQUARE_DOUBLE_HEAD, MINIMAL, MINIMAL_HEAVY_HEAD, MINIMAL_DOUBLE_HEAD, SIMPLE, SIMPLE_HEAD, SIMPLE_HEAVY, HORIZONTALS, ROUNDED, HEAVY, HEAVY_EDGE, HEAVY_HEAD, DOUBLE, DOUBLE_EDGE, MARKDOWN, } from "./core/box.js";
|
|
34
34
|
// Protocol
|
|
35
|
-
export { isRenderable, isMeasurable, withCellWidth, withBoundedWidth, } from "./core/protocol.js";
|
|
35
|
+
export { isRenderable, isMeasurable, withCellWidth, withBoundedWidth, insetHeight, stackedHeight, regionRows, fitHeight, getStyle, } from "./core/protocol.js";
|
|
36
36
|
// Measurement
|
|
37
37
|
export { Measurement, measureRenderables } from "./core/measure.js";
|
|
38
38
|
// Text
|
|
@@ -43,6 +43,8 @@ export { Strip, PowerlineJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER_GLYPHS, Caps
|
|
|
43
43
|
export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
|
|
44
44
|
// OSC 8 hyperlink wire grammar — for consumers that read rendered bytes back
|
|
45
45
|
export { OSC8, osc8Sequences } from "./core/osc8.js";
|
|
46
|
+
// ANSI bytes back into styled text
|
|
47
|
+
export { AnsiDecoder, decodeAnsi } from "./core/ansi.js";
|
|
46
48
|
// Markup plugin tags
|
|
47
49
|
export { MarkupRegistry, globalMarkupRegistry, renderMarkup, } from "./core/markup.js";
|
|
48
50
|
// Emoji
|
|
@@ -63,6 +65,7 @@ export { Console } from "./core/console.js";
|
|
|
63
65
|
export { Constrain } from "./renderables/constrain.js";
|
|
64
66
|
export { Align } from "./renderables/align.js";
|
|
65
67
|
export { Padding } from "./renderables/padding.js";
|
|
68
|
+
export { Viewport, SCROLLBAR } from "./renderables/viewport.js";
|
|
66
69
|
export { Rule } from "./renderables/rule.js";
|
|
67
70
|
export { Panel } from "./renderables/panel.js";
|
|
68
71
|
export { Group } from "./renderables/group.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* process streams.
|
|
4
4
|
*
|
|
5
5
|
* [LAW:locality-or-seam] The `TerminalHost` interface is the seam and lives
|
|
6
|
-
* in `
|
|
6
|
+
* in `host/terminal-host.ts`, importable anywhere. *This* file is one
|
|
7
7
|
* value satisfying that interface, and it reads `process.stdin` /
|
|
8
8
|
* `process.stdout`, so it is a node subpath:
|
|
9
9
|
*
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* `setRawMode` in the runtime or in a demo, take a `TerminalHost` parameter
|
|
28
28
|
* instead. The seam is already here.
|
|
29
29
|
*/
|
|
30
|
-
import type { DataHandler, ResizeHandler, TerminalHost, TerminalSize } from "../host/terminal-host.js";
|
|
30
|
+
import type { DataHandler, ExitHandler, ResizeHandler, TerminalHost, TerminalSize } from "../host/terminal-host.js";
|
|
31
31
|
import type { Unsubscribe } from "../core/subscription.js";
|
|
32
32
|
/**
|
|
33
33
|
* Subset of NodeJS stream API the node host actually depends on. Tests
|
|
@@ -50,6 +50,19 @@ interface NodeWritable {
|
|
|
50
50
|
readonly rows?: number;
|
|
51
51
|
readonly isTTY?: boolean;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Subset of node's `process` the host hears the program end through and
|
|
55
|
+
* suspends it with. Tests pass an `EventEmitter` that records `kill`, because
|
|
56
|
+
* the real one would stop the test runner.
|
|
57
|
+
*/
|
|
58
|
+
interface NodeProcess {
|
|
59
|
+
readonly pid: number;
|
|
60
|
+
on(event: string, listener: () => void): unknown;
|
|
61
|
+
prependListener(event: string, listener: () => void): unknown;
|
|
62
|
+
off(event: string, listener: () => void): unknown;
|
|
63
|
+
listenerCount(event: string): number;
|
|
64
|
+
kill(pid: number, signal: NodeJS.Signals): unknown;
|
|
65
|
+
}
|
|
53
66
|
export interface NodeTerminalHostOptions {
|
|
54
67
|
/**
|
|
55
68
|
* Input stream. Defaults to `process.stdin` so production code does not
|
|
@@ -61,10 +74,23 @@ export interface NodeTerminalHostOptions {
|
|
|
61
74
|
* Tests pass a `Writable` mock.
|
|
62
75
|
*/
|
|
63
76
|
stdout?: NodeWritable;
|
|
77
|
+
/**
|
|
78
|
+
* The environment colour detection reads. Defaults to `process.env`; tests
|
|
79
|
+
* pass a fixed map so what the host reports does not depend on the shell
|
|
80
|
+
* that ran them.
|
|
81
|
+
*/
|
|
82
|
+
env?: NodeJS.ProcessEnv;
|
|
83
|
+
/**
|
|
84
|
+
* What the host hears the program end through and suspends it with.
|
|
85
|
+
* Defaults to `process`; tests pass an emitter that records `kill`.
|
|
86
|
+
*/
|
|
87
|
+
process?: NodeProcess;
|
|
64
88
|
}
|
|
65
89
|
export declare class NodeTerminalHost implements TerminalHost {
|
|
66
90
|
private readonly stdin;
|
|
67
91
|
private readonly stdout;
|
|
92
|
+
private readonly process;
|
|
93
|
+
readonly env: NodeJS.ProcessEnv;
|
|
68
94
|
private readonly dataHandlers;
|
|
69
95
|
private readonly resizeHandlers;
|
|
70
96
|
private dataListener;
|
|
@@ -72,12 +98,15 @@ export declare class NodeTerminalHost implements TerminalHost {
|
|
|
72
98
|
private rawModeRequested;
|
|
73
99
|
constructor(options?: NodeTerminalHostOptions);
|
|
74
100
|
get isTTY(): boolean;
|
|
101
|
+
get writesToTerminal(): boolean;
|
|
75
102
|
write(data: Uint8Array | string): void;
|
|
76
103
|
size(): TerminalSize;
|
|
77
104
|
setRawMode(raw: boolean): void;
|
|
78
105
|
private applyRawMode;
|
|
79
106
|
onData(handler: DataHandler): Unsubscribe;
|
|
80
107
|
onResize(handler: ResizeHandler): Unsubscribe;
|
|
108
|
+
onExit(handler: ExitHandler): Unsubscribe;
|
|
109
|
+
suspend(): Promise<void>;
|
|
81
110
|
start(): void;
|
|
82
111
|
stop(): void;
|
|
83
112
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* process streams.
|
|
4
4
|
*
|
|
5
5
|
* [LAW:locality-or-seam] The `TerminalHost` interface is the seam and lives
|
|
6
|
-
* in `
|
|
6
|
+
* in `host/terminal-host.ts`, importable anywhere. *This* file is one
|
|
7
7
|
* value satisfying that interface, and it reads `process.stdin` /
|
|
8
8
|
* `process.stdout`, so it is a node subpath:
|
|
9
9
|
*
|
|
@@ -27,11 +27,24 @@
|
|
|
27
27
|
* `setRawMode` in the runtime or in a demo, take a `TerminalHost` parameter
|
|
28
28
|
* instead. The seam is already here.
|
|
29
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"];
|
|
30
41
|
const DEFAULT_COLS = 80;
|
|
31
42
|
const DEFAULT_ROWS = 24;
|
|
32
43
|
export class NodeTerminalHost {
|
|
33
44
|
stdin;
|
|
34
45
|
stdout;
|
|
46
|
+
process;
|
|
47
|
+
env;
|
|
35
48
|
dataHandlers = new Set();
|
|
36
49
|
resizeHandlers = new Set();
|
|
37
50
|
dataListener;
|
|
@@ -40,9 +53,14 @@ export class NodeTerminalHost {
|
|
|
40
53
|
constructor(options = {}) {
|
|
41
54
|
this.stdin = options.stdin ?? process.stdin;
|
|
42
55
|
this.stdout = options.stdout ?? process.stdout;
|
|
56
|
+
this.env = options.env ?? process.env;
|
|
57
|
+
this.process = options.process ?? process;
|
|
43
58
|
}
|
|
44
59
|
get isTTY() {
|
|
45
|
-
return !!this.stdin.isTTY &&
|
|
60
|
+
return !!this.stdin.isTTY && this.writesToTerminal;
|
|
61
|
+
}
|
|
62
|
+
get writesToTerminal() {
|
|
63
|
+
return !!this.stdout.isTTY;
|
|
46
64
|
}
|
|
47
65
|
write(data) {
|
|
48
66
|
this.stdout.write(data);
|
|
@@ -130,6 +148,55 @@ export class NodeTerminalHost {
|
|
|
130
148
|
}
|
|
131
149
|
};
|
|
132
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
|
+
}
|
|
133
200
|
start() {
|
|
134
201
|
// No-op on node: stdin/stdout are always available. Lifecycle exists
|
|
135
202
|
// so `BrowserTerminalHost` can attach to the DOM here without the
|
package/dist/node/traceback.d.ts
CHANGED
|
@@ -9,12 +9,22 @@
|
|
|
9
9
|
* import { installTraceback } from "@promptctl/rich-js/node/traceback";
|
|
10
10
|
* installTraceback({ suppress: ["node_modules"] });
|
|
11
11
|
*
|
|
12
|
-
* [LAW:dataflow-not-control-flow]
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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`.
|
|
19
|
+
*
|
|
20
|
+
* [LAW:no-ambient-temporal-coupling] The one channel is also what puts the
|
|
21
|
+
* report after the terminal is handed back. A program holding the alternate
|
|
22
|
+
* screen restores it on `uncaughtExceptionMonitor` (`NodeTerminalHost.onExit`),
|
|
23
|
+
* which node emits ahead of every `uncaughtException` listener. Reported
|
|
24
|
+
* straight from `unhandledRejection`, a crash skips that phase: the report
|
|
25
|
+
* draws inside the alternate screen and vanishes with it when `exit` restores
|
|
26
|
+
* the terminal. Node's crash sequence owns the order, not the order in which
|
|
27
|
+
* `installTraceback` and a host happened to add their listeners.
|
|
18
28
|
*/
|
|
19
29
|
import type { TracebackOptions } from "../renderables/traceback.js";
|
|
20
30
|
/**
|
package/dist/node/traceback.js
CHANGED
|
@@ -9,12 +9,22 @@
|
|
|
9
9
|
* import { installTraceback } from "@promptctl/rich-js/node/traceback";
|
|
10
10
|
* installTraceback({ suppress: ["node_modules"] });
|
|
11
11
|
*
|
|
12
|
-
* [LAW:dataflow-not-control-flow]
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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`.
|
|
19
|
+
*
|
|
20
|
+
* [LAW:no-ambient-temporal-coupling] The one channel is also what puts the
|
|
21
|
+
* report after the terminal is handed back. A program holding the alternate
|
|
22
|
+
* screen restores it on `uncaughtExceptionMonitor` (`NodeTerminalHost.onExit`),
|
|
23
|
+
* which node emits ahead of every `uncaughtException` listener. Reported
|
|
24
|
+
* straight from `unhandledRejection`, a crash skips that phase: the report
|
|
25
|
+
* draws inside the alternate screen and vanishes with it when `exit` restores
|
|
26
|
+
* the terminal. Node's crash sequence owns the order, not the order in which
|
|
27
|
+
* `installTraceback` and a host happened to add their listeners.
|
|
18
28
|
*/
|
|
19
29
|
import { inspect, types } from "node:util";
|
|
20
30
|
import { Console } from "../core/console.js";
|
|
@@ -68,6 +78,18 @@ function toError(reason) {
|
|
|
68
78
|
* and no "have we installed yet" branch. There is no absent case to handle.
|
|
69
79
|
*/
|
|
70
80
|
let installed = () => { };
|
|
81
|
+
/**
|
|
82
|
+
* An unhandled rejection, raised as the uncaught exception it becomes when
|
|
83
|
+
* nobody listens — its reason unwrapped, which node's own conversion would
|
|
84
|
+
* bury in a message for any reason that is not an `Error`. Raised once the
|
|
85
|
+
* rejection's other listeners have heard it: a throw inside the emit would
|
|
86
|
+
* end it before them.
|
|
87
|
+
*/
|
|
88
|
+
function escalate(reason) {
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
throw reason;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
71
93
|
/**
|
|
72
94
|
* Register rich tracebacks for uncaught exceptions and unhandled rejections.
|
|
73
95
|
*
|
|
@@ -105,8 +127,8 @@ export function installTraceback(options) {
|
|
|
105
127
|
});
|
|
106
128
|
};
|
|
107
129
|
process.off("uncaughtException", installed);
|
|
108
|
-
process.off("unhandledRejection",
|
|
130
|
+
process.off("unhandledRejection", escalate);
|
|
109
131
|
process.on("uncaughtException", report);
|
|
110
|
-
process.on("unhandledRejection",
|
|
132
|
+
process.on("unhandledRejection", escalate);
|
|
111
133
|
installed = report;
|
|
112
134
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Align — wraps a renderable and
|
|
2
|
+
* Align — wraps a renderable and places its output horizontally, as one block.
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
6
|
-
|
|
6
|
+
import { type Alignment } from "../core/place.js";
|
|
7
7
|
export declare class Align implements Renderable, Measurable {
|
|
8
8
|
readonly renderable: Renderable;
|
|
9
9
|
readonly align: Alignment;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Align — wraps a renderable and
|
|
2
|
+
* Align — wraps a renderable and places its output horizontally, as one block.
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import { Measurement } from "../core/measure.js";
|
|
6
|
-
import { isMeasurable } from "../core/protocol.js";
|
|
6
|
+
import { isMeasurable, withBoundedWidth } from "../core/protocol.js";
|
|
7
|
+
import { placeBlock } from "../core/place.js";
|
|
7
8
|
export class Align {
|
|
8
9
|
renderable;
|
|
9
10
|
align;
|
|
@@ -11,25 +12,11 @@ export class Align {
|
|
|
11
12
|
this.renderable = renderable;
|
|
12
13
|
this.align = align;
|
|
13
14
|
}
|
|
15
|
+
// [LAW:one-source-of-truth] `placeBlock` is the placement; `Console.print`'s
|
|
16
|
+
// `justify` uses the same one.
|
|
14
17
|
*render(options) {
|
|
15
|
-
const
|
|
16
|
-
const segments = [...this.renderable.render(options)];
|
|
17
|
-
const lines = Segment.splitLines(segments);
|
|
18
|
-
for (const line of lines) {
|
|
19
|
-
const lineWidth = Segment.getLineLength(line);
|
|
20
|
-
const gap = maxWidth - lineWidth;
|
|
21
|
-
// [LAW:dataflow-not-control-flow] Always compute padding; gap <= 0 produces empty strings
|
|
22
|
-
const leftPad = this.align === "right"
|
|
23
|
-
? gap
|
|
24
|
-
: this.align === "center"
|
|
25
|
-
? Math.floor(gap / 2)
|
|
26
|
-
: 0;
|
|
27
|
-
if (leftPad > 0)
|
|
28
|
-
yield new Segment(" ".repeat(leftPad));
|
|
18
|
+
for (const line of placeBlock(this.renderable, this.align, withBoundedWidth(options, this))) {
|
|
29
19
|
yield* line;
|
|
30
|
-
const rightPad = gap - leftPad;
|
|
31
|
-
if (rightPad > 0)
|
|
32
|
-
yield new Segment(" ".repeat(rightPad));
|
|
33
20
|
yield Segment.line();
|
|
34
21
|
}
|
|
35
22
|
}
|