@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,527 @@
1
+ /**
2
+ * Console — the central orchestrator for rendering styled output to the terminal.
3
+ */
4
+ import { Segment } from "./segment.js";
5
+ import { NULL_STYLE, DEFAULT_THEME } from "./style.js";
6
+ import { resolveDestination } from "./color.js";
7
+ import { encodeHtml } from "./export-html.js";
8
+ import { RichText } from "./text.js";
9
+ import { renderMarkup } from "./markup.js";
10
+ import { Pretty } from "./pretty.js";
11
+ import { JSONRenderable } from "./json.js";
12
+ import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
13
+ // [LAW:one-way-deps] exception: one of the two sanctioned upward edges out of
14
+ // `core/`, named in CLAUDE.md and pinned by `test/seam/layering.test.ts` — the
15
+ // orchestrator reaching down for `Rule`. `RuleOptions` rides the same edge
16
+ // rather than being restated here; see `rule()`.
17
+ import { Rule } from "../renderables/rule.js";
18
+ import { segmentsToString } from "./render.js";
19
+ import { placeBlock } from "./place.js";
20
+ import { isRenderable } from "./protocol.js";
21
+ // Three of the five justify methods place what a print draws as a block, the
22
+ // way Rich's `print` wraps each renderable in `Align`. The other two leave it
23
+ // where it was drawn: `"default"` asks for no placement, and `"full"` is a way
24
+ // of setting a paragraph's words, which only text can do.
25
+ const PLACED_BY = {
26
+ default: undefined,
27
+ full: undefined,
28
+ left: "left",
29
+ center: "center",
30
+ right: "right",
31
+ };
32
+ // A text run as one block: its items joined into one text, as the reference's
33
+ // `Text.join` joins them, drawn under the print's options and ended by the
34
+ // print's `end`. Measure and render both read that one text, so the width
35
+ // `justify` places the run at is the width it is drawn at. Each item's own
36
+ // `justify`, `overflow`, `noWrap` and `tabSize` stay behind, as they do in the
37
+ // reference: the print's options set the whole run.
38
+ class TextRun {
39
+ items;
40
+ end;
41
+ constructor(items, end) {
42
+ this.items = items;
43
+ this.end = end;
44
+ }
45
+ *render(options) {
46
+ yield* this.text(options).render(options);
47
+ yield this.end;
48
+ }
49
+ measure(options) {
50
+ return this.text(options).measure(options);
51
+ }
52
+ // A `Pretty` lays itself out for the width it is offered, so the run is
53
+ // joined per call rather than once.
54
+ text(options) {
55
+ return this.items.reduce((joined, item) => joined.append(item instanceof Pretty ? item.toText(options) : item), new RichText("", { end: "" }));
56
+ }
57
+ }
58
+ // A host with an environment but no streams — what a browser looks like from
59
+ // here. Frozen so the shared instance cannot be mutated into a fake terminal.
60
+ const DETACHED_ENVIRONMENT = Object.freeze({
61
+ env: Object.freeze({}),
62
+ });
63
+ // [LAW:single-enforcer] The only read of the ambient `process` global in this
64
+ // module, and the reason every other function below takes its environment as
65
+ // an argument. `process` satisfies `ConsoleEnvironment` structurally, so the
66
+ // node path needs no adapter; the browser path degrades to a host that answers
67
+ // "no streams" rather than throwing at the reference.
68
+ function ambientEnvironment() {
69
+ return typeof process === "undefined" ? DETACHED_ENVIRONMENT : process;
70
+ }
71
+ // [LAW:one-source-of-truth] A console talks to exactly one stream, and every
72
+ // question it asks the host — are you a terminal, how wide are you, where do
73
+ // bytes go — is a question about *that* stream. Selecting it once, here, is
74
+ // what makes the three answers agree. When each site re-selected for itself,
75
+ // one of them (size) silently didn't, and a `Console({ stderr: true })` keyed
76
+ // its colour to stderr and its width to stdout.
77
+ function boundStream(environment, useStderr) {
78
+ return useStderr ? environment.stderr : environment.stdout;
79
+ }
80
+ // [LAW:single-enforcer] Effective TTY status of the console's output target is
81
+ // computed once, at construction, and cached — an explicit `file` is not a
82
+ // terminal, `forceTerminal` says it is regardless, and otherwise the bound
83
+ // stream answers. Every input is fixed at construction, so recomputing per
84
+ // read could only ever return the same answer twice.
85
+ function effectiveIsTTY(options, stream) {
86
+ if (options?.forceTerminal)
87
+ return true;
88
+ if (options?.file)
89
+ return false;
90
+ return stream?.isTTY ?? false;
91
+ }
92
+ function terminalSize(environment, stream) {
93
+ const cols = environment.env["COLUMNS"];
94
+ const lines = environment.env["LINES"];
95
+ const w = cols ? parseInt(cols, 10) : (stream?.columns ?? 80);
96
+ const h = lines ? parseInt(lines, 10) : (stream?.rows ?? 24);
97
+ return { width: w || 80, height: h || 24 };
98
+ }
99
+ // [LAW:single-enforcer] One trust-boundary check: when no `file:` was
100
+ // provided, the console writes to the stream it is bound to — which a browser
101
+ // host does not have. Without this, the absence would surface as a `TypeError`
102
+ // deep inside `_write`. Centralizing it here keeps both consumers (`file`
103
+ // getter, `_write`) behaving identically — no second copy of the check to
104
+ // drift.
105
+ function defaultSink(stream) {
106
+ if (stream === undefined) {
107
+ throw new Error("Console: no `file` provided and the environment has no stream to " +
108
+ "write to (e.g. running in a browser). Pass " +
109
+ "`environment: hostEnvironment(host)` to write through a terminal " +
110
+ "host, or a `file` so output has somewhere to go.");
111
+ }
112
+ return stream;
113
+ }
114
+ // [LAW:dataflow-not-control-flow] Build one size-reading function at
115
+ // construction time from whichever options the caller supplied. The width
116
+ // getter is then unconditional — it always calls `_getSize()`. Variability
117
+ // lives in the captured closure (which source it consults), not in branches
118
+ // the getter has to evaluate on every read.
119
+ //
120
+ // The all-static and mixed cases get *different* closures: when both
121
+ // dimensions are fixed, the returned function is a constant — no
122
+ // `terminalSize` call on every read, since neither value can change.
123
+ // When at least one dimension is dynamic, the closure consults
124
+ // `terminalSize` to fill in the missing side.
125
+ function resolveGetSize(options, environment, stream) {
126
+ if (options?.getSize)
127
+ return options.getSize;
128
+ const staticWidth = options?.width;
129
+ const staticHeight = options?.height;
130
+ if (staticWidth !== undefined && staticHeight !== undefined) {
131
+ // [LAW:one-source-of-truth] Freeze the captured object so consumers can't
132
+ // mutate `console.size` and silently change every subsequent read.
133
+ // Matches the dynamic-closure path (which builds a fresh object per call
134
+ // and is therefore implicitly immutable from a sharing standpoint).
135
+ const fixed = Object.freeze({ width: staticWidth, height: staticHeight });
136
+ return () => fixed;
137
+ }
138
+ return () => {
139
+ const term = terminalSize(environment, stream);
140
+ return {
141
+ width: staticWidth ?? term.width,
142
+ height: staticHeight ?? term.height,
143
+ };
144
+ };
145
+ }
146
+ // --- Console ---
147
+ /** Shared because it is stateless: highlighting nothing has nothing to own. */
148
+ const NO_HIGHLIGHT = new NullHighlighter();
149
+ /**
150
+ * What `print` bounds a data argument by when the caller said nothing.
151
+ *
152
+ * `print` formats whatever it is handed, so it is the one place that has to
153
+ * assume nothing about the value's size, in any of the three ways a value can
154
+ * be large. Unbounded, `print(buffer)` emits a line per byte,
155
+ * `print(deeplyNested)` descends until the stack gives out, and a single
156
+ * response body assigned to a field arrives in full — each of them a debug line
157
+ * that costs megabytes or hangs the terminal. All three bounds announce
158
+ * themselves in the output (`... +N`, `{...}`, `+N` after the closing quote),
159
+ * so this truncates visibly and never silently. [LAW:no-silent-failure]
160
+ *
161
+ * `maxString` reaches only strings nested inside data; a string argument is
162
+ * printed by the arm above, where the caller asked for that string by name.
163
+ *
164
+ * A caller constructing a `Pretty` has seen their data and gets no defaults;
165
+ * these belong to the convenience path, not to the formatter.
166
+ */
167
+ const PRINT_DATA_BOUNDS = { maxLength: 100, maxDepth: 16, maxString: 1000 };
168
+ export class Console {
169
+ _destination;
170
+ // [LAW:one-source-of-truth] Size flows through a single function. Static
171
+ // `width`/`height` options collapse into a closure that returns them; a
172
+ // caller-supplied `getSize` overrides. Every size read in this class goes
173
+ // through `_getSize()` — no second path, no second source to drift.
174
+ _getSize;
175
+ _style;
176
+ _isTerminal;
177
+ _forceInteractive;
178
+ _file;
179
+ // [LAW:one-source-of-truth] The resolved output stream, not the `stderr`
180
+ // flag that selected it. Storing the answer instead of the question is what
181
+ // stops `isTerminal`, `size`, and `file` from re-deriving it three ways.
182
+ _stream;
183
+ _record;
184
+ _markup;
185
+ _highlight;
186
+ _theme;
187
+ _onStyleError;
188
+ _asciiOnly;
189
+ _highlighter;
190
+ _recorded;
191
+ // [LAW:types-are-the-program] The capture state is carried in the *type*
192
+ // of this field, not in a sentinel value. `null` = not capturing; any
193
+ // string (including "") = capturing, this is the buffer. A paired
194
+ // boolean+string would admit the impossible state "not capturing, buffer
195
+ // non-empty" — `string | null` makes that unrepresentable.
196
+ _capture;
197
+ constructor(options) {
198
+ const environment = options?.environment ?? ambientEnvironment();
199
+ const stream = boundStream(environment, options?.stderr ?? false);
200
+ this._stream = stream;
201
+ this._isTerminal = effectiveIsTTY(options, stream);
202
+ // [LAW:dataflow-not-control-flow] `isTTY` and `env` are forwarded
203
+ // unconditionally, so detection stays inside the console's injected
204
+ // environment rather than the ambient process's.
205
+ const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env });
206
+ // [LAW:single-enforcer] The console's one override, an explicit
207
+ // `hyperlinks`, is applied here and nowhere else; every write encodes for
208
+ // this value whole.
209
+ this._destination = {
210
+ colorSystem: resolved.colorSystem,
211
+ hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
212
+ };
213
+ this._getSize = resolveGetSize(options, environment, stream);
214
+ // [LAW:no-ambient-temporal-coupling] The theme is assigned before the
215
+ // style, because the console's own style may be one of the theme's names.
216
+ this._theme = options?.theme ?? DEFAULT_THEME;
217
+ this._style = this._theme.resolve(options?.style ?? NULL_STYLE);
218
+ this._forceInteractive = options?.forceInteractive;
219
+ this._file = options?.file;
220
+ this._record = options?.record ?? false;
221
+ this._markup = options?.markup !== false;
222
+ this._highlight = options?.highlight !== false;
223
+ this._highlighter = options?.highlighter ?? new ReprHighlighter();
224
+ this._onStyleError = options?.onStyleError;
225
+ this._asciiOnly = options?.asciiOnly ?? false;
226
+ this._recorded = [];
227
+ this._capture = null;
228
+ }
229
+ // --- Properties ---
230
+ get size() {
231
+ return this._getSize();
232
+ }
233
+ get width() {
234
+ return this._getSize().width;
235
+ }
236
+ get height() {
237
+ return this._getSize().height;
238
+ }
239
+ get isTerminal() {
240
+ return this._isTerminal;
241
+ }
242
+ get isInteractive() {
243
+ if (this._forceInteractive !== undefined)
244
+ return this._forceInteractive;
245
+ return this.isTerminal;
246
+ }
247
+ get colorSystem() {
248
+ return this._destination.colorSystem;
249
+ }
250
+ /**
251
+ * Where this console writes: the colour depth it draws at and whether it
252
+ * emits OSC 8 hyperlinks, overrides applied — the value its output is
253
+ * encoded for, for anything that encodes on its behalf (`Live`).
254
+ */
255
+ get destination() {
256
+ return this._destination;
257
+ }
258
+ // [LAW:one-source-of-truth] Output target lookup matches `_write`'s:
259
+ // explicit `file` wins, otherwise `defaultSink` returns the bound stream
260
+ // (or throws clearly when the environment has none). Exposed
261
+ // so renderables that bypass the segment pipeline (e.g. Live's raw control
262
+ // sequences) still write to the caller's configured stream. Return type is
263
+ // the narrow `ConsoleSink` — Live (and any external consumer) only needs
264
+ // `.write()`, so the type tells the truth about what the surface guarantees.
265
+ get file() {
266
+ return this._file ?? defaultSink(this._stream);
267
+ }
268
+ get theme() {
269
+ return this._theme;
270
+ }
271
+ get options() {
272
+ // One read of the size, so a resize cannot pair one frame's width with
273
+ // another's height.
274
+ const { width, height } = this.size;
275
+ return {
276
+ maxWidth: width,
277
+ // The terminal an inline print lands on: a ceiling, never a region —
278
+ // content keeps its natural height beneath it.
279
+ height: { rows: height, exact: false },
280
+ isTerminal: this.isTerminal,
281
+ asciiOnly: this._asciiOnly,
282
+ theme: this._theme,
283
+ onStyleError: this._onStyleError,
284
+ colorSystem: this._destination.colorSystem,
285
+ };
286
+ }
287
+ // --- Print ---
288
+ print(...args) {
289
+ // Extract options from last arg if it's a PrintOptions
290
+ let opts = {};
291
+ let items;
292
+ // The trailing options object is only recognised when something precedes
293
+ // it. A lone object is data — `print({ style: "..." })` reads as a value to
294
+ // format, and treating it as options would emit an empty line, which is the
295
+ // one outcome this method must never produce. Nothing is given up: options
296
+ // configure the rendering of content, so a call carrying options and no
297
+ // content had nothing to render either way. [LAW:no-silent-failure]
298
+ const lastArg = args[args.length - 1];
299
+ if (args.length > 1 &&
300
+ typeof lastArg === "object" &&
301
+ lastArg !== null &&
302
+ !isRenderable(lastArg) &&
303
+ ("style" in lastArg || "justify" in lastArg || "markup" in lastArg ||
304
+ "highlight" in lastArg || "overflow" in lastArg || "end" in lastArg ||
305
+ "softWrap" in lastArg || "crop" in lastArg || "sep" in lastArg)) {
306
+ opts = lastArg;
307
+ items = args.slice(0, -1);
308
+ }
309
+ else {
310
+ items = args;
311
+ }
312
+ const doMarkup = opts.markup ?? this._markup;
313
+ const doHighlight = opts.highlight ?? this._highlight;
314
+ const sep = opts.sep ?? " ";
315
+ const end = opts.end ?? "\n";
316
+ const softWrap = opts.softWrap ?? false;
317
+ const printStyle = this._theme.resolve(opts.style ?? NULL_STYLE);
318
+ // A print is a column of blocks, and every argument joins one of two kinds.
319
+ // Text — a string, a `RichText`, or data — runs together: adjacent text
320
+ // items are one block, joined by `sep` and ended by `end`. Any other
321
+ // renderable is a block of its own that occupies whole lines, so neither
322
+ // `sep` nor `end` ever touches it. That is the reference's split — `end`
323
+ // belongs to text, not to the print — and it is what lets two printed panels
324
+ // stack with no blank line between them. The one place the reference cuts
325
+ // differently is data: it gives a container lines of its own, and here all
326
+ // data is text. The line is not closed by asking where the cursor sits:
327
+ // `print("a\n")` is a line and an empty one, here as in the reference and
328
+ // in every other `print`, and only the kind of the item can tell that
329
+ // trailing break from a `Panel`'s. A call with nothing to print is one
330
+ // empty text run, so it still ends the line.
331
+ const blocks = items.length === 0 ? [{ kind: "text", items: [] }] : [];
332
+ for (const item of items) {
333
+ // Four arms, and they are the whole domain. A `RichText` is already text;
334
+ // it runs as a copy with its own `end` cleared, as a string's is, because
335
+ // the line end of a run is the print's. Any other renderable draws itself,
336
+ // as a block. A string is the only kind of argument that can *contain*
337
+ // markup, so it is the only kind the markup dialect is applied to.
338
+ // Everything else is data, and `Pretty` is the single authority on how a
339
+ // JavaScript value displays — `String(value)` was a second, weaker one
340
+ // that answered `[object Object]` for every object and let the markup
341
+ // parser eat it. [LAW:one-source-of-truth]
342
+ let text;
343
+ if (item instanceof RichText) {
344
+ const richText = item.copy();
345
+ richText.end = "";
346
+ text = richText;
347
+ }
348
+ else if (isRenderable(item)) {
349
+ blocks.push({ kind: "lines", renderable: item });
350
+ continue;
351
+ }
352
+ else if (typeof item === "string") {
353
+ const richText = doMarkup ? renderMarkup(item) : new RichText(item);
354
+ richText.end = "";
355
+ if (doHighlight) {
356
+ this._highlighter.highlight(richText);
357
+ }
358
+ text = richText;
359
+ }
360
+ else {
361
+ // The highlighter travels with the value. `highlight` and a custom
362
+ // `highlighter` are console-wide settings, so they have to reach a
363
+ // formatted object the same way they reach a printed string; a `Pretty`
364
+ // left to its own default would outrank both. [LAW:dataflow-not-control-flow]
365
+ // the disabled case is an identity highlighter, not a skipped call.
366
+ // Indent guides are styling too, and travel with the same decision —
367
+ // the console owns what `highlight` means for everything it emits,
368
+ // rather than `Pretty` inferring it back out of the highlighter.
369
+ text = new Pretty(item, {
370
+ ...PRINT_DATA_BOUNDS,
371
+ highlighter: doHighlight ? this._highlighter : NO_HIGHLIGHT,
372
+ indentGuides: doHighlight,
373
+ });
374
+ }
375
+ const run = blocks.at(-1);
376
+ if (run?.kind === "text")
377
+ run.items.push(new RichText(sep, { end: "" }), text);
378
+ else
379
+ blocks.push({ kind: "text", items: [text] });
380
+ }
381
+ // Soft wrap is the reference's pair of defaults, not a mode of its own: it
382
+ // stops wrapping, and when no overflow was named it asks for `"ignore"`, so
383
+ // the line leaves unbounded. A named overflow still cuts the unwrapped line
384
+ // at the width, as Rich's does. What `"ignore"` does to a line — no edge,
385
+ // no justify — is `RichText`'s to apply, so it crosses as it was asked for.
386
+ const renderOpts = {
387
+ ...this.options,
388
+ justify: opts.justify === "default" ? undefined : opts.justify,
389
+ overflow: opts.overflow ?? (softWrap ? "ignore" : undefined),
390
+ noWrap: softWrap,
391
+ };
392
+ // The print style, then the console's base style, over each line a block
393
+ // drew and never over the break that ends it: a styled break carries SGR
394
+ // codes across the newline.
395
+ // [LAW:dataflow-not-control-flow] no style to apply is an empty list, not
396
+ // a skipped step.
397
+ const styles = [printStyle, this._style].filter((style) => !style.isNull);
398
+ const styleContent = (segments) => styles.reduce((styled, style) => Segment.applyStyle(styled, style), segments);
399
+ // The common default end is "\n" — reuse Segment's cached newline rather
400
+ // than allocating one per print; only a non-default end needs a fresh one.
401
+ const terminator = end === "\n" ? Segment.line() : new Segment(end);
402
+ const align = PLACED_BY[opts.justify ?? "default"];
403
+ // Each block as its lines, and whether its last line is closed. A placed
404
+ // block is closed line by line, as `Align` closes it. Unplaced, a text run
405
+ // is closed exactly where its content — its `end` included — breaks; any
406
+ // other block's lines are closed whether or not it closed them itself: a
407
+ // `Panel` ends in a line break and a bare `Spinner` or `ProgressBar` does
408
+ // not (they're line fragments, meant to be composed within a line —
409
+ // rich-flexstrip-5kf.4cq), and both leave the next print at the start of a
410
+ // line.
411
+ const drawBlock = (block) => {
412
+ const renderable = block.kind === "text" ? new TextRun(block.items, terminator) : block.renderable;
413
+ if (align)
414
+ return { lines: placeBlock(renderable, align, renderOpts), closed: true };
415
+ const drawn = [...renderable.render(renderOpts)];
416
+ // A text run's last segment is always its `end`, yielded even when
417
+ // empty, and `splitLines` drops the empty line after a final break only
418
+ // when nothing follows it — so the run is closed exactly when `end` is.
419
+ const closed = block.kind === "lines" || end.endsWith("\n");
420
+ return { lines: Segment.splitLines(drawn), closed };
421
+ };
422
+ // Every line end goes through the same writeSegments funnel as the text it
423
+ // ends, so it survives recording — otherwise `exportText` and `exportHtml`
424
+ // would join consecutive prints onto a single line. [LAW:single-enforcer]
425
+ const output = [];
426
+ for (const block of blocks) {
427
+ const { lines, closed } = drawBlock(block);
428
+ lines.forEach((line, index) => {
429
+ output.push(...styleContent(line));
430
+ if (closed || index < lines.length - 1)
431
+ output.push(Segment.line());
432
+ });
433
+ }
434
+ // Crop last, and crop the line-end with the rest: in the reference `end` is
435
+ // the tail of the printed line, so a line cut at the edge loses it too.
436
+ // Soft wrap turns cropping off whatever `crop` says, because a line it left
437
+ // whole is meant to reach the terminal whole. [LAW:dataflow-not-control-flow]
438
+ // Not cropping is an unbounded width rather than a skipped step — the same
439
+ // spelling `RichText` uses for `"ignore"`.
440
+ const cropWidth = !softWrap && (opts.crop ?? true) ? this.width : Infinity;
441
+ this._writeSegments([...Segment.cropLines(output, cropWidth)]);
442
+ }
443
+ log(...args) {
444
+ // Simple log implementation — adds timestamp
445
+ const now = new Date();
446
+ const time = now.toLocaleTimeString();
447
+ const timeText = new RichText(`[${time}] `, { end: "" });
448
+ timeText.stylize("log.time");
449
+ this.print(timeText, ...args);
450
+ }
451
+ // [LAW:one-source-of-truth] `RuleOptions` is `Rule`'s, not a restatement of
452
+ // it. `console.ts` used to declare its own copy and field-by-field forward
453
+ // into this constructor, which made a second map of one shape — identical
454
+ // then, free to drift the moment `Rule` grew an option.
455
+ rule(title, options) {
456
+ const segments = [...new Rule(title, options).render(this.options)];
457
+ this._writeSegments(segments);
458
+ }
459
+ // [LAW:one-source-of-truth] `JSONRenderable` is the one formatter for JSON and
460
+ // `JSONOptions` is its option set; this method only picks the constructor. It
461
+ // prints with soft wrap, as the reference does, so a line wider than the
462
+ // console reaches the terminal whole and the output stays valid JSON.
463
+ printJson(json, options) {
464
+ const renderable = typeof json === "string"
465
+ ? JSONRenderable.fromString(json, options)
466
+ : JSONRenderable.fromData(json, options);
467
+ this.print(renderable, { softWrap: true });
468
+ }
469
+ // --- Capture ---
470
+ // [LAW:one-source-of-truth] Redirect semantics match Python Rich's
471
+ // `Console.capture()`: while a capture is active the underlying target
472
+ // (file / stdout / stderr) receives nothing, and `endCapture()` returns
473
+ // the full text that would otherwise have been written. This is not a
474
+ // tee — callers wanting both must compose explicitly.
475
+ beginCapture() {
476
+ this._capture = "";
477
+ }
478
+ endCapture() {
479
+ const result = this._capture ?? "";
480
+ this._capture = null;
481
+ return result;
482
+ }
483
+ // --- Export (when record:true) ---
484
+ exportText({ clear = true } = {}) {
485
+ const text = this._recorded.map((s) => s.text).join("");
486
+ if (clear)
487
+ this._recorded = [];
488
+ return text;
489
+ }
490
+ // [LAW:single-enforcer] `encodeHtml` draws the picture `exportLines`
491
+ // resolves; nothing here interprets a `Style`.
492
+ exportHtml({ theme, clear = true } = {}) {
493
+ const html = encodeHtml(this._recorded, theme);
494
+ if (clear)
495
+ this._recorded = [];
496
+ return html;
497
+ }
498
+ // --- Internal ---
499
+ // [LAW:single-enforcer] One encode call per write batch routes through the
500
+ // same `segmentsToString` tree-coalescer used by `renderToString`. Adjacent
501
+ // same-style segments share one SGR open/close pair on the wire instead of
502
+ // one pair per segment. Recording captures every non-control segment for
503
+ // replay regardless of how the bytes coalesce.
504
+ _writeSegments(segments) {
505
+ if (this._record) {
506
+ for (const segment of segments) {
507
+ if (!segment.isControl)
508
+ this._recorded.push(segment);
509
+ }
510
+ }
511
+ const encoded = segmentsToString(segments, this._destination);
512
+ if (encoded.length > 0)
513
+ this._write(encoded);
514
+ }
515
+ _write(text) {
516
+ // [LAW:types-are-the-program] `_capture !== null` is a discriminator
517
+ // narrow on `string | null`, not a sentinel comparison — the type
518
+ // carries the active/inactive state. The early return is what makes
519
+ // this a redirect (not a tee); see beginCapture for semantics.
520
+ if (this._capture !== null) {
521
+ this._capture += text;
522
+ return;
523
+ }
524
+ const target = this._file ?? defaultSink(this._stream);
525
+ target.write(text);
526
+ }
527
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Emoji shortcode support — dictionary, replacement, and renderable.
3
+ */
4
+ import { Segment } from "./segment.js";
5
+ import type { Style } from "./style.js";
6
+ import type { Renderable, RenderOptions } from "./protocol.js";
7
+ export declare const EMOJI: Record<string, string>;
8
+ /**
9
+ * Replaces :shortcode: patterns in text with emoji characters.
10
+ */
11
+ export declare function emojiReplace(text: string, defaultVariant?: "emoji" | "text"): string;
12
+ export declare class NoEmoji extends Error {
13
+ constructor(name: string);
14
+ }
15
+ export declare class Emoji implements Renderable {
16
+ readonly name: string;
17
+ private readonly _char;
18
+ private readonly _style;
19
+ private readonly _variant;
20
+ constructor(name: string, style?: Style, variant?: "emoji" | "text");
21
+ toString(): string;
22
+ render(_options: RenderOptions): Iterable<Segment>;
23
+ static replace(text: string): string;
24
+ }