@promptctl/rich-js 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/dist/core/anchor.d.ts +45 -0
  2. package/dist/core/anchor.js +39 -0
  3. package/dist/core/ansi.d.ts +65 -0
  4. package/dist/core/ansi.js +231 -0
  5. package/dist/core/box.d.ts +143 -0
  6. package/dist/core/box.js +379 -0
  7. package/dist/core/cells.d.ts +159 -0
  8. package/dist/core/cells.js +287 -0
  9. package/dist/core/color.d.ts +246 -0
  10. package/dist/core/color.js +1038 -0
  11. package/dist/core/console.d.ts +165 -0
  12. package/dist/core/console.js +527 -0
  13. package/dist/core/emoji.d.ts +24 -0
  14. package/dist/core/emoji.js +391 -0
  15. package/dist/core/export-html.d.ts +59 -0
  16. package/dist/core/export-html.js +200 -0
  17. package/dist/core/export-lines.d.ts +121 -0
  18. package/dist/core/export-lines.js +141 -0
  19. package/dist/core/highlighter.d.ts +33 -0
  20. package/dist/core/highlighter.js +124 -0
  21. package/dist/core/json.d.ts +27 -0
  22. package/dist/core/json.js +53 -0
  23. package/dist/core/markup.d.ts +119 -0
  24. package/dist/core/markup.js +731 -0
  25. package/dist/core/measure.d.ts +27 -0
  26. package/dist/core/measure.js +62 -0
  27. package/dist/core/oklch.d.ts +161 -0
  28. package/dist/core/oklch.js +312 -0
  29. package/dist/core/osc8.d.ts +68 -0
  30. package/dist/core/osc8.js +90 -0
  31. package/dist/core/place.d.ts +30 -0
  32. package/dist/core/place.js +51 -0
  33. package/dist/core/pretty.d.ts +144 -0
  34. package/dist/core/pretty.js +476 -0
  35. package/dist/core/protocol.d.ts +267 -0
  36. package/dist/core/protocol.js +163 -0
  37. package/dist/core/render.d.ts +84 -0
  38. package/dist/core/render.js +145 -0
  39. package/dist/core/segment.d.ts +161 -0
  40. package/dist/core/segment.js +492 -0
  41. package/dist/core/spinnerData.d.ts +12 -0
  42. package/dist/core/spinnerData.js +117 -0
  43. package/dist/core/strip.d.ts +141 -0
  44. package/dist/core/strip.js +305 -0
  45. package/dist/core/style.d.ts +169 -0
  46. package/dist/core/style.js +695 -0
  47. package/dist/core/subscription.d.ts +12 -0
  48. package/dist/core/subscription.js +12 -0
  49. package/dist/core/text.d.ts +256 -0
  50. package/dist/core/text.js +976 -0
  51. package/dist/core/wrap.d.ts +41 -0
  52. package/dist/core/wrap.js +127 -0
  53. package/dist/host/app.d.ts +121 -0
  54. package/dist/host/app.js +255 -0
  55. package/dist/host/host-environment.d.ts +26 -0
  56. package/dist/host/host-environment.js +44 -0
  57. package/dist/host/index.d.ts +5 -0
  58. package/dist/host/index.js +3 -0
  59. package/dist/host/terminal-host.d.ts +179 -0
  60. package/dist/host/terminal-host.js +186 -0
  61. package/dist/index.d.ts +91 -0
  62. package/dist/index.js +107 -0
  63. package/dist/node/prompt.d.ts +21 -0
  64. package/dist/node/prompt.js +30 -0
  65. package/dist/node/save.d.ts +17 -0
  66. package/dist/node/save.js +21 -0
  67. package/dist/node/terminal-host.d.ts +113 -0
  68. package/dist/node/terminal-host.js +227 -0
  69. package/dist/node/traceback.d.ts +37 -0
  70. package/dist/node/traceback.js +134 -0
  71. package/dist/renderables/align.d.ts +16 -0
  72. package/dist/renderables/align.js +33 -0
  73. package/dist/renderables/columns.d.ts +61 -0
  74. package/dist/renderables/columns.js +152 -0
  75. package/dist/renderables/constrain.d.ts +15 -0
  76. package/dist/renderables/constrain.js +37 -0
  77. package/dist/renderables/embed.d.ts +50 -0
  78. package/dist/renderables/embed.js +71 -0
  79. package/dist/renderables/flexStrip.d.ts +41 -0
  80. package/dist/renderables/flexStrip.js +200 -0
  81. package/dist/renderables/group.d.ts +11 -0
  82. package/dist/renderables/group.js +17 -0
  83. package/dist/renderables/layout.d.ts +92 -0
  84. package/dist/renderables/layout.js +312 -0
  85. package/dist/renderables/live.d.ts +50 -0
  86. package/dist/renderables/live.js +175 -0
  87. package/dist/renderables/markdown.d.ts +24 -0
  88. package/dist/renderables/markdown.js +184 -0
  89. package/dist/renderables/padding.d.ts +38 -0
  90. package/dist/renderables/padding.js +141 -0
  91. package/dist/renderables/panel.d.ts +116 -0
  92. package/dist/renderables/panel.js +307 -0
  93. package/dist/renderables/progress.d.ts +90 -0
  94. package/dist/renderables/progress.js +238 -0
  95. package/dist/renderables/progressBar.d.ts +31 -0
  96. package/dist/renderables/progressBar.js +53 -0
  97. package/dist/renderables/prompt.d.ts +49 -0
  98. package/dist/renderables/prompt.js +125 -0
  99. package/dist/renderables/rule.d.ts +25 -0
  100. package/dist/renderables/rule.js +90 -0
  101. package/dist/renderables/spinner.d.ts +31 -0
  102. package/dist/renderables/spinner.js +70 -0
  103. package/dist/renderables/status.d.ts +23 -0
  104. package/dist/renderables/status.js +60 -0
  105. package/dist/renderables/syntax.d.ts +35 -0
  106. package/dist/renderables/syntax.js +115 -0
  107. package/dist/renderables/table.d.ts +194 -0
  108. package/dist/renderables/table.js +795 -0
  109. package/dist/renderables/traceback.d.ts +31 -0
  110. package/dist/renderables/traceback.js +108 -0
  111. package/dist/renderables/tree.d.ts +68 -0
  112. package/dist/renderables/tree.js +150 -0
  113. package/dist/renderables/viewport.d.ts +114 -0
  114. package/dist/renderables/viewport.js +198 -0
  115. package/dist/template-bindings/color-funcs.d.ts +87 -0
  116. package/dist/template-bindings/color-funcs.js +217 -0
  117. package/dist/template-bindings/helpers.d.ts +24 -0
  118. package/dist/template-bindings/helpers.js +48 -0
  119. package/dist/template-bindings/index.d.ts +107 -0
  120. package/dist/template-bindings/index.js +146 -0
  121. package/dist/template-bindings/palette-funcs.d.ts +94 -0
  122. package/dist/template-bindings/palette-funcs.js +141 -0
  123. package/dist/template-bindings/style-funcs.d.ts +28 -0
  124. package/dist/template-bindings/style-funcs.js +155 -0
  125. package/dist/themes/buildPalette.d.ts +36 -0
  126. package/dist/themes/buildPalette.js +56 -0
  127. package/dist/themes/colorMath.d.ts +82 -0
  128. package/dist/themes/colorMath.js +254 -0
  129. package/dist/themes/colorRef.d.ts +51 -0
  130. package/dist/themes/colorRef.js +96 -0
  131. package/dist/themes/data/ansi-tables.d.ts +14 -0
  132. package/dist/themes/data/ansi-tables.js +49 -0
  133. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  134. package/dist/themes/data/atom-one-dark.js +177 -0
  135. package/dist/themes/data/atom-one-light.d.ts +3 -0
  136. package/dist/themes/data/atom-one-light.js +177 -0
  137. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  138. package/dist/themes/data/catppuccin-frappe.js +177 -0
  139. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  140. package/dist/themes/data/catppuccin-latte.js +177 -0
  141. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  142. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  143. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  144. package/dist/themes/data/catppuccin-mocha.js +177 -0
  145. package/dist/themes/data/cyberpunk.d.ts +17 -0
  146. package/dist/themes/data/cyberpunk.js +193 -0
  147. package/dist/themes/data/default.d.ts +3 -0
  148. package/dist/themes/data/default.js +160 -0
  149. package/dist/themes/data/dracula.d.ts +3 -0
  150. package/dist/themes/data/dracula.js +177 -0
  151. package/dist/themes/data/flexoki.d.ts +3 -0
  152. package/dist/themes/data/flexoki.js +177 -0
  153. package/dist/themes/data/gruvbox.d.ts +3 -0
  154. package/dist/themes/data/gruvbox.js +177 -0
  155. package/dist/themes/data/index.d.ts +35 -0
  156. package/dist/themes/data/index.js +64 -0
  157. package/dist/themes/data/monokai.d.ts +3 -0
  158. package/dist/themes/data/monokai.js +177 -0
  159. package/dist/themes/data/nord.d.ts +3 -0
  160. package/dist/themes/data/nord.js +177 -0
  161. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  162. package/dist/themes/data/rose-pine-dawn.js +177 -0
  163. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  164. package/dist/themes/data/rose-pine-moon.js +177 -0
  165. package/dist/themes/data/rose-pine.d.ts +3 -0
  166. package/dist/themes/data/rose-pine.js +177 -0
  167. package/dist/themes/data/solarized-dark.d.ts +3 -0
  168. package/dist/themes/data/solarized-dark.js +177 -0
  169. package/dist/themes/data/solarized-light.d.ts +3 -0
  170. package/dist/themes/data/solarized-light.js +177 -0
  171. package/dist/themes/data/svg-export.d.ts +3 -0
  172. package/dist/themes/data/svg-export.js +177 -0
  173. package/dist/themes/data/textual-ansi.d.ts +3 -0
  174. package/dist/themes/data/textual-ansi.js +160 -0
  175. package/dist/themes/data/textual-dark.d.ts +3 -0
  176. package/dist/themes/data/textual-dark.js +160 -0
  177. package/dist/themes/data/textual-light.d.ts +3 -0
  178. package/dist/themes/data/textual-light.js +160 -0
  179. package/dist/themes/data/tokyo-night.d.ts +3 -0
  180. package/dist/themes/data/tokyo-night.js +177 -0
  181. package/dist/themes/data/types.d.ts +31 -0
  182. package/dist/themes/data/types.js +10 -0
  183. package/dist/themes/palette.d.ts +21 -0
  184. package/dist/themes/palette.js +28 -0
  185. package/dist/themes/ramp.d.ts +83 -0
  186. package/dist/themes/ramp.js +122 -0
  187. package/dist/themes/registry.d.ts +44 -0
  188. package/dist/themes/registry.js +94 -0
  189. package/dist/themes/terminalThemes.d.ts +49 -0
  190. package/dist/themes/terminalThemes.js +63 -0
  191. package/dist/themes/transpose.d.ts +87 -0
  192. package/dist/themes/transpose.js +138 -0
  193. package/dist/widgets/button.d.ts +49 -0
  194. package/dist/widgets/button.js +208 -0
  195. package/dist/widgets/checkbox.d.ts +41 -0
  196. package/dist/widgets/checkbox.js +157 -0
  197. package/dist/widgets/dropdown.d.ts +67 -0
  198. package/dist/widgets/dropdown.js +450 -0
  199. package/dist/widgets/event-router.d.ts +71 -0
  200. package/dist/widgets/event-router.js +629 -0
  201. package/dist/widgets/focus-manager.d.ts +45 -0
  202. package/dist/widgets/focus-manager.js +191 -0
  203. package/dist/widgets/hit.d.ts +64 -0
  204. package/dist/widgets/hit.js +113 -0
  205. package/dist/widgets/index.d.ts +20 -0
  206. package/dist/widgets/index.js +14 -0
  207. package/dist/widgets/slider.d.ts +57 -0
  208. package/dist/widgets/slider.js +270 -0
  209. package/dist/widgets/static-item.d.ts +34 -0
  210. package/dist/widgets/static-item.js +54 -0
  211. package/dist/widgets/text-input.d.ts +274 -0
  212. package/dist/widgets/text-input.js +1165 -0
  213. package/dist/widgets/toggle.d.ts +44 -0
  214. package/dist/widgets/toggle.js +180 -0
  215. package/dist/widgets/types.d.ts +87 -0
  216. package/dist/widgets/types.js +30 -0
  217. package/dist/widgets/widget-app.d.ts +63 -0
  218. package/dist/widgets/widget-app.js +199 -0
  219. package/dist/widgets/widget-base.d.ts +40 -0
  220. package/dist/widgets/widget-base.js +165 -0
  221. package/package.json +6 -2
@@ -0,0 +1,113 @@
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
+ /**
33
+ * Subset of NodeJS stream API the node host actually depends on. Tests
34
+ * construct a host over PassThrough / Writable mocks; production wires it
35
+ * to `process.stdin` / `process.stdout`.
36
+ */
37
+ interface NodeReadable {
38
+ on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
39
+ off(event: "data", listener: (chunk: Buffer | string) => void): unknown;
40
+ setRawMode?(raw: boolean): unknown;
41
+ resume?(): unknown;
42
+ pause?(): unknown;
43
+ readonly isTTY?: boolean;
44
+ }
45
+ interface NodeWritable {
46
+ write(chunk: string | Uint8Array): unknown;
47
+ on(event: "resize", listener: () => void): unknown;
48
+ off(event: "resize", listener: () => void): unknown;
49
+ readonly columns?: number;
50
+ readonly rows?: number;
51
+ readonly isTTY?: boolean;
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
+ }
66
+ export interface NodeTerminalHostOptions {
67
+ /**
68
+ * Input stream. Defaults to `process.stdin` so production code does not
69
+ * have to construct one explicitly. Tests pass a `PassThrough`.
70
+ */
71
+ stdin?: NodeReadable;
72
+ /**
73
+ * Output stream. Defaults to `process.stdout` for the same reason.
74
+ * Tests pass a `Writable` mock.
75
+ */
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;
88
+ }
89
+ export declare class NodeTerminalHost implements TerminalHost {
90
+ private readonly stdin;
91
+ private readonly stdout;
92
+ private readonly process;
93
+ readonly env: NodeJS.ProcessEnv;
94
+ private readonly dataHandlers;
95
+ private readonly resizeHandlers;
96
+ private dataListener;
97
+ private resizeListener;
98
+ private rawModeRequested;
99
+ constructor(options?: NodeTerminalHostOptions);
100
+ get isTTY(): boolean;
101
+ get writesToTerminal(): boolean;
102
+ write(data: Uint8Array | string): void;
103
+ size(): TerminalSize;
104
+ setRawMode(raw: boolean): void;
105
+ private applyRawMode;
106
+ onData(handler: DataHandler): Unsubscribe;
107
+ onResize(handler: ResizeHandler): Unsubscribe;
108
+ onExit(handler: ExitHandler): Unsubscribe;
109
+ suspend(): Promise<void>;
110
+ start(): void;
111
+ stop(): void;
112
+ }
113
+ 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,37 @@
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`.
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.
28
+ */
29
+ import type { TracebackOptions } from "../renderables/traceback.js";
30
+ /**
31
+ * Register rich tracebacks for uncaught exceptions and unhandled rejections.
32
+ *
33
+ * Calling this more than once replaces the previous handler — the last call
34
+ * wins, options included — so a process always has exactly one rich crash
35
+ * renderer no matter how many entry points reach for it.
36
+ */
37
+ export declare function installTraceback(options?: TracebackOptions): void;
@@ -0,0 +1,134 @@
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`.
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.
28
+ */
29
+ import { inspect, types } from "node:util";
30
+ import { Console } from "../core/console.js";
31
+ import { Traceback } from "../renderables/traceback.js";
32
+ /** A payload's own stack, when it carries one, or `""` when it does not. */
33
+ function stackOf(reason) {
34
+ if (typeof reason !== "object" || reason === null)
35
+ return "";
36
+ if (!("stack" in reason))
37
+ return "";
38
+ return typeof reason.stack === "string" ? reason.stack : "";
39
+ }
40
+ /**
41
+ * [LAW:parse-dont-validate] The checkpoint between a crash payload, which node
42
+ * allows to be any value at all, and `Traceback`, which renders an `Error`.
43
+ * The return type is the proof: nothing downstream re-asks whether the payload
44
+ * was throwable, and no raw value can reach the renderable to have `.name` and
45
+ * `.message` read off it as `undefined`.
46
+ *
47
+ * The gate is `isNativeError`, not `instanceof Error`. An error thrown inside a
48
+ * `node:vm` context is a real error with real frames, but its prototype comes
49
+ * from that realm, so `instanceof` answers false and the frames would be
50
+ * discarded by the one component that exists to show them. `isNativeError`
51
+ * reads V8's internal error slot and is realm-independent.
52
+ *
53
+ * Whatever survives that gate is not an error, so it has no call site of its
54
+ * own; `stackOf` salvages a stack from a duck-typed thrower and otherwise
55
+ * leaves it empty rather than reporting the frames of this function, which
56
+ * point into rich-js and describe nothing about the fault. `inspect` renders
57
+ * every payload shape, including circular objects, where `String(reason)`
58
+ * would collapse them to `[object Object]`.
59
+ */
60
+ function toError(reason) {
61
+ if (types.isNativeError(reason))
62
+ return reason;
63
+ const error = new Error(inspect(reason));
64
+ error.name = "NonError";
65
+ error.stack = stackOf(reason);
66
+ return error;
67
+ }
68
+ /**
69
+ * The handler currently registered, or a no-op standing in for "none yet".
70
+ *
71
+ * [LAW:no-shared-mutable-globals] The process listener registry is a shared
72
+ * global, so the handler installed into it gets a single owner here rather
73
+ * than a caller appending to it once per call. Holding the reference is what
74
+ * lets `installTraceback` replace rather than accumulate.
75
+ *
76
+ * [LAW:types-are-the-program] A no-op rather than `null`: `process.off` on an
77
+ * unregistered listener does nothing, so the first call needs no null check
78
+ * and no "have we installed yet" branch. There is no absent case to handle.
79
+ */
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
+ }
93
+ /**
94
+ * Register rich tracebacks for uncaught exceptions and unhandled rejections.
95
+ *
96
+ * Calling this more than once replaces the previous handler — the last call
97
+ * wins, options included — so a process always has exactly one rich crash
98
+ * renderer no matter how many entry points reach for it.
99
+ */
100
+ export function installTraceback(options) {
101
+ // [LAW:no-ambient-temporal-coupling] "This process is already crashing" is
102
+ // lifecycle state with one owner, not a fact left to the interleaving of two
103
+ // writes. A cascade — cleanup code rejecting a promise while the first
104
+ // report is still draining — would otherwise start a second write and a
105
+ // second `process.exit(1)`, and whichever drained first would cut the other
106
+ // off mid-frame. The first crash is the fault and the rest is its wake, so
107
+ // one complete report of the fault beats two corrupt ones.
108
+ let crashing = false;
109
+ const report = (reason) => {
110
+ if (crashing)
111
+ return;
112
+ crashing = true;
113
+ // [LAW:effects-at-boundaries] Render to a string first, then perform one
114
+ // write. `beginCapture` redirects the console's sink without setting
115
+ // `file:`, so stderr's TTY status still drives colour depth — a `file:`
116
+ // sink would report `isTTY === false` and silently strip colour.
117
+ const out = new Console({ stderr: true });
118
+ out.beginCapture();
119
+ out.print(new Traceback(toError(reason), options));
120
+ // [LAW:no-silent-failure] Exit only once the bytes have drained. When
121
+ // stderr is a pipe (CI logs, `2>&1 | tee`), writes are asynchronous and
122
+ // an immediate `process.exit(1)` truncates the traceback at the pipe
123
+ // buffer — the longer the traceback, the more of it is lost, which is
124
+ // the reverse of what a crash report should do.
125
+ process.stderr.write(out.endCapture(), () => {
126
+ process.exit(1);
127
+ });
128
+ };
129
+ process.off("uncaughtException", installed);
130
+ process.off("unhandledRejection", escalate);
131
+ process.on("uncaughtException", report);
132
+ process.on("unhandledRejection", escalate);
133
+ installed = report;
134
+ }
@@ -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,33 @@
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 { isMeasurable, 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
+ if (isMeasurable(this.renderable)) {
25
+ const measurement = Measurement.get(options, this.renderable);
26
+ return {
27
+ minimum: Math.max(1, measurement.minimum),
28
+ maximum: measurement.maximum,
29
+ };
30
+ }
31
+ return { minimum: 1, maximum: options.maxWidth };
32
+ }
33
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Columns — arranges renderables in a multi-column layout.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import type { PaddingDimensions } from "./padding.js";
6
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
+ export interface ColumnsOptions {
8
+ expand?: boolean;
9
+ equal?: boolean;
10
+ width?: number;
11
+ padding?: PaddingDimensions;
12
+ columnFirst?: boolean;
13
+ }
14
+ export declare class Columns implements Renderable, Measurable {
15
+ renderables: (Renderable & Partial<Measurable>)[];
16
+ readonly expand: boolean;
17
+ readonly equal: boolean;
18
+ readonly colWidth: number | undefined;
19
+ readonly columnFirst: boolean;
20
+ readonly gutterWidth: number;
21
+ constructor(items?: Iterable<unknown>, options?: ColumnsOptions);
22
+ render(rawOptions: RenderOptions): Iterable<Segment>;
23
+ /**
24
+ * The requested width divided into columns, once.
25
+ *
26
+ * [LAW:single-enforcer] The three modes disagree only about how many columns
27
+ * there are and how wide each one is, so they disagree in one place. `render`
28
+ * lays out against this division and `_naturalWidth` is the width at which it
29
+ * comes out as one row of every item.
30
+ */
31
+ private _divide;
32
+ /**
33
+ * The widest any one item wants to be.
34
+ *
35
+ * An item that cannot measure itself wants the offer, which is what `Panel`,
36
+ * `Padding`, `Layout` and `Tree` all answer for the same case — and under an
37
+ * unbounded offer that is `Infinity`, so `withBoundedWidth` throws and says
38
+ * the request was unanswerable. Counting it as one cell instead reported a
39
+ * natural width of 1, which resolved an unbounded offer to a single column and
40
+ * cropped forty cells of content down to `"x"` with no error at all.
41
+ */
42
+ private _itemWidth;
43
+ /**
44
+ * The declared column width, bounded by the width offered.
45
+ *
46
+ * [LAW:single-enforcer] A declared width is what a column asks for, not what
47
+ * it takes — the contract `Table._outerWidth` keeps for a declared table
48
+ * width. Read by `_divide` for render and by `_naturalWidth` for measure, so
49
+ * the two answer from one number: laid out at the raw declared width, a
50
+ * six-cell column offered three emitted six-cell lines while `measure`
51
+ * reported three, and the terminal's soft wrap took the frame of everything
52
+ * printed after it.
53
+ */
54
+ private _declaredWidth;
55
+ /** The width at which every item sits on one row: n columns and the gutters between them. */
56
+ private _naturalWidth;
57
+ measure(rawOptions: RenderOptions): {
58
+ minimum: number;
59
+ maximum: number;
60
+ };
61
+ }