@promptctl/rich-js 0.18.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +11 -8
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +99 -18
  9. package/dist/core/color.js +334 -146
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/noise.d.ts +31 -0
  33. package/dist/core/noise.js +94 -0
  34. package/dist/core/osc8.d.ts +6 -6
  35. package/dist/core/osc8.js +9 -21
  36. package/dist/core/paint.d.ts +107 -0
  37. package/dist/core/paint.js +161 -0
  38. package/dist/core/place.d.ts +1 -1
  39. package/dist/core/place.js +4 -6
  40. package/dist/core/pretty.d.ts +126 -2
  41. package/dist/core/pretty.js +571 -105
  42. package/dist/core/protocol.d.ts +43 -3
  43. package/dist/core/protocol.js +23 -2
  44. package/dist/core/render.d.ts +33 -30
  45. package/dist/core/render.js +43 -83
  46. package/dist/core/segment.d.ts +6 -5
  47. package/dist/core/segment.js +11 -15
  48. package/dist/core/selection.d.ts +12 -0
  49. package/dist/core/selection.js +14 -0
  50. package/dist/core/strip.d.ts +116 -74
  51. package/dist/core/strip.js +266 -217
  52. package/dist/core/style.d.ts +27 -7
  53. package/dist/core/style.js +106 -41
  54. package/dist/core/text.d.ts +43 -16
  55. package/dist/core/text.js +180 -59
  56. package/dist/core/wrap.d.ts +5 -0
  57. package/dist/core/wrap.js +16 -3
  58. package/dist/host/app.d.ts +95 -17
  59. package/dist/host/app.js +201 -61
  60. package/dist/host/index.d.ts +4 -1
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +84 -0
  63. package/dist/host/input.js +498 -0
  64. package/dist/host/terminal-host.d.ts +15 -2
  65. package/dist/host/terminal-host.js +2 -0
  66. package/dist/index.d.ts +26 -14
  67. package/dist/index.js +17 -7
  68. package/dist/node/prompt.d.ts +13 -0
  69. package/dist/node/prompt.js +30 -9
  70. package/dist/node/save.d.ts +4 -3
  71. package/dist/node/save.js +6 -3
  72. package/dist/node/terminal-host.d.ts +10 -2
  73. package/dist/node/terminal-host.js +7 -0
  74. package/dist/node/traceback.d.ts +2 -1
  75. package/dist/node/traceback.js +3 -39
  76. package/dist/renderables/align.js +2 -9
  77. package/dist/renderables/columns.d.ts +45 -29
  78. package/dist/renderables/columns.js +142 -103
  79. package/dist/renderables/constrain.js +6 -12
  80. package/dist/renderables/effect.d.ts +113 -0
  81. package/dist/renderables/effect.js +182 -0
  82. package/dist/renderables/effects.d.ts +252 -0
  83. package/dist/renderables/effects.js +565 -0
  84. package/dist/renderables/embed.d.ts +94 -31
  85. package/dist/renderables/embed.js +141 -45
  86. package/dist/renderables/group.d.ts +6 -3
  87. package/dist/renderables/group.js +5 -0
  88. package/dist/renderables/layout.d.ts +6 -23
  89. package/dist/renderables/layout.js +24 -113
  90. package/dist/renderables/list-view.d.ts +42 -0
  91. package/dist/renderables/list-view.js +78 -0
  92. package/dist/renderables/live.d.ts +40 -16
  93. package/dist/renderables/live.js +96 -83
  94. package/dist/renderables/markdown.d.ts +16 -2
  95. package/dist/renderables/markdown.js +521 -118
  96. package/dist/renderables/padding.d.ts +22 -1
  97. package/dist/renderables/padding.js +30 -24
  98. package/dist/renderables/panel.d.ts +31 -33
  99. package/dist/renderables/panel.js +137 -141
  100. package/dist/renderables/progress.d.ts +110 -16
  101. package/dist/renderables/progress.js +212 -81
  102. package/dist/renderables/progressBar.d.ts +24 -4
  103. package/dist/renderables/progressBar.js +78 -23
  104. package/dist/renderables/prompt.d.ts +37 -15
  105. package/dist/renderables/prompt.js +125 -85
  106. package/dist/renderables/ratio.d.ts +123 -0
  107. package/dist/renderables/ratio.js +213 -0
  108. package/dist/renderables/rule.d.ts +3 -2
  109. package/dist/renderables/rule.js +50 -52
  110. package/dist/renderables/spinner.d.ts +28 -2
  111. package/dist/renderables/spinner.js +52 -19
  112. package/dist/renderables/status.d.ts +13 -7
  113. package/dist/renderables/status.js +15 -29
  114. package/dist/renderables/syntax.d.ts +37 -7
  115. package/dist/renderables/syntax.js +134 -83
  116. package/dist/renderables/table.d.ts +58 -20
  117. package/dist/renderables/table.js +373 -197
  118. package/dist/renderables/traceback.d.ts +5 -4
  119. package/dist/renderables/traceback.js +239 -68
  120. package/dist/renderables/tree.d.ts +10 -2
  121. package/dist/renderables/tree.js +61 -34
  122. package/dist/renderables/viewport.d.ts +30 -0
  123. package/dist/renderables/viewport.js +61 -7
  124. package/dist/template-bindings/color-funcs.d.ts +31 -9
  125. package/dist/template-bindings/color-funcs.js +40 -14
  126. package/dist/template-bindings/helpers.d.ts +4 -2
  127. package/dist/template-bindings/helpers.js +5 -20
  128. package/dist/template-bindings/index.d.ts +29 -17
  129. package/dist/template-bindings/index.js +36 -20
  130. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  131. package/dist/template-bindings/palette-funcs.js +11 -7
  132. package/dist/template-bindings/style-funcs.js +4 -2
  133. package/dist/themes/buildPalette.d.ts +24 -3
  134. package/dist/themes/buildPalette.js +52 -8
  135. package/dist/themes/colorMath.d.ts +29 -17
  136. package/dist/themes/colorMath.js +74 -48
  137. package/dist/themes/colorRef.d.ts +2 -17
  138. package/dist/themes/colorRef.js +13 -31
  139. package/dist/themes/palette.d.ts +24 -0
  140. package/dist/themes/palette.js +46 -3
  141. package/dist/themes/ramp.d.ts +21 -34
  142. package/dist/themes/ramp.js +23 -42
  143. package/dist/themes/registry.js +39 -28
  144. package/dist/widgets/button.d.ts +2 -6
  145. package/dist/widgets/button.js +9 -40
  146. package/dist/widgets/checkbox.d.ts +2 -5
  147. package/dist/widgets/checkbox.js +9 -31
  148. package/dist/widgets/dropdown.d.ts +2 -5
  149. package/dist/widgets/dropdown.js +17 -51
  150. package/dist/widgets/event-router.d.ts +31 -40
  151. package/dist/widgets/event-router.js +52 -483
  152. package/dist/widgets/index.d.ts +2 -1
  153. package/dist/widgets/ink.d.ts +22 -0
  154. package/dist/widgets/ink.js +31 -0
  155. package/dist/widgets/slider.d.ts +2 -5
  156. package/dist/widgets/slider.js +11 -34
  157. package/dist/widgets/text-input.d.ts +19 -10
  158. package/dist/widgets/text-input.js +156 -147
  159. package/dist/widgets/themed-widget.d.ts +20 -0
  160. package/dist/widgets/themed-widget.js +89 -0
  161. package/dist/widgets/toggle.d.ts +2 -5
  162. package/dist/widgets/toggle.js +8 -31
  163. package/dist/widgets/types.d.ts +8 -25
  164. package/dist/widgets/types.js +8 -0
  165. package/dist/widgets/widget-app.d.ts +24 -7
  166. package/dist/widgets/widget-app.js +35 -18
  167. package/dist/widgets/widget-base.d.ts +2 -0
  168. package/dist/widgets/widget-base.js +9 -2
  169. package/package.json +31 -17
@@ -3,15 +3,26 @@
3
3
  */
4
4
  import { Style, Theme } from "./style.js";
5
5
  import { ColorDepth } from "./color.js";
6
- import type { Destination } from "./color.js";
6
+ import type { ColorSystemName, Destination } from "./color.js";
7
+ import type { Env } from "./env.js";
7
8
  import type { TerminalTheme } from "./color.js";
8
9
  import { type JSONOptions } from "./json.js";
9
10
  import type { Highlighter } from "./highlighter.js";
10
11
  import { type RuleOptions } from "../renderables/rule.js";
11
12
  import type { Height, OverflowMethod, RenderOptions, StyleErrorHandler } from "./protocol.js";
13
+ import type { Unsubscribe } from "./subscription.js";
12
14
  export interface ConsoleSink {
13
15
  write(chunk: string | Uint8Array): unknown;
14
16
  }
17
+ /**
18
+ * A frame standing on the terminal under the cursor — a running `Live` — that
19
+ * everything the console writes has to go above. `around` returns the bytes
20
+ * that take the frame off the terminal, write `text`, and paint the frame
21
+ * back under it; the console writes them in one piece.
22
+ */
23
+ export interface LiveRegion {
24
+ around(text: string): string;
25
+ }
15
26
  export interface ConsoleStream extends ConsoleSink {
16
27
  readonly isTTY?: boolean;
17
28
  readonly columns?: number;
@@ -23,18 +34,18 @@ export interface ConsoleStream extends ConsoleSink {
23
34
  * point — injecting a fake terminal needs no adapter at either end.
24
35
  */
25
36
  export interface ConsoleEnvironment {
26
- readonly env: NodeJS.ProcessEnv;
37
+ readonly env: Env;
27
38
  readonly stdout?: ConsoleStream;
28
39
  readonly stderr?: ConsoleStream;
29
40
  }
30
41
  export interface ConsoleOptions {
31
42
  /**
32
- * Color encoding. Accepts a string spec (`"auto"`, `"truecolor"`, `"256"`,
33
- * `"ansi"`, `"none"`), a `ColorDepth` enum value (use this for `WINDOWS`,
34
- * which has no string spec), or `null` for no color. Default `"auto"`.
43
+ * Color encoding. Accepts a `ColorSystemName` (`"auto"`, `"truecolor"`,
44
+ * `"256"`, `"ansi"`, `"none"`), a `ColorDepth` enum value (use this for
45
+ * `WINDOWS`, which has no name), or `null` for no color. Default `"auto"`.
35
46
  * Colour only: an explicit depth, `null` included, keeps hyperlinks.
36
47
  */
37
- colorSystem?: string | ColorDepth | null;
48
+ colorSystem?: ColorSystemName | ColorDepth | null;
38
49
  /**
39
50
  * Whether OSC 8 hyperlinks are emitted. Default: what the destination takes
40
51
  * — true for an explicit `colorSystem`, detected under `"auto"` (no TTY or
@@ -72,6 +83,11 @@ export interface ConsoleOptions {
72
83
  width: number;
73
84
  height: number;
74
85
  };
86
+ /**
87
+ * The clock `log` stamps each row from, read once per call. Default: the
88
+ * wall clock. Pass a fixed one to make logged output repeatable.
89
+ */
90
+ getDatetime?: () => Date;
75
91
  style?: string | Style;
76
92
  forceTerminal?: boolean;
77
93
  forceInteractive?: boolean;
@@ -112,9 +128,11 @@ export interface PrintOptions {
112
128
  export declare class Console {
113
129
  private readonly _destination;
114
130
  private _getSize;
131
+ private readonly _getDatetime;
132
+ private _lastLogTime;
115
133
  private _style;
116
134
  private _isTerminal;
117
- private _forceInteractive;
135
+ private readonly _interactive;
118
136
  private _file;
119
137
  private _stream;
120
138
  private _record;
@@ -126,6 +144,7 @@ export declare class Console {
126
144
  private _highlighter;
127
145
  private _recorded;
128
146
  private _capture;
147
+ private _liveRegion;
129
148
  constructor(options?: ConsoleOptions);
130
149
  get size(): {
131
150
  width: number;
@@ -148,9 +167,16 @@ export declare class Console {
148
167
  height: Height;
149
168
  };
150
169
  print(...args: unknown[]): void;
170
+ private _draw;
151
171
  log(...args: unknown[]): void;
152
172
  rule(title?: string, options?: RuleOptions): void;
153
173
  printJson(json: string | object, options?: JSONOptions): void;
174
+ /**
175
+ * Hold `region` under everything this console writes until the returned
176
+ * call lets it go. One region at a time: a second would be erased and
177
+ * painted over by the first, so claiming one while another is held throws.
178
+ */
179
+ claimLiveRegion(region: LiveRegion): Unsubscribe;
154
180
  beginCapture(): void;
155
181
  endCapture(): string;
156
182
  exportText({ clear }?: {
@@ -160,6 +186,11 @@ export declare class Console {
160
186
  theme?: TerminalTheme;
161
187
  clear?: boolean;
162
188
  }): string;
189
+ exportSvg({ theme, title, clear }?: {
190
+ theme?: TerminalTheme;
191
+ title?: string;
192
+ clear?: boolean;
193
+ }): string;
163
194
  private _writeSegments;
164
195
  private _write;
165
196
  }
@@ -3,11 +3,12 @@
3
3
  */
4
4
  import { Segment } from "./segment.js";
5
5
  import { NULL_STYLE, DEFAULT_THEME } from "./style.js";
6
- import { resolveDestination } from "./color.js";
6
+ import { isDumbTerminal, resolveDestination } from "./color.js";
7
7
  import { encodeHtml } from "./export-html.js";
8
+ import { encodeSvg } from "./export-svg.js";
8
9
  import { RichText } from "./text.js";
9
- import { renderMarkup } from "./markup.js";
10
- import { Pretty } from "./pretty.js";
10
+ import { activeHighlighter, renderStr } from "./markup.js";
11
+ import { Pretty, UNSEEN_DATA_BOUNDS, isExpandable } from "./pretty.js";
11
12
  import { JSONRenderable } from "./json.js";
12
13
  import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
13
14
  // [LAW:one-way-deps] exception: one of the two sanctioned upward edges out of
@@ -18,6 +19,29 @@ import { Rule } from "../renderables/rule.js";
18
19
  import { segmentsToString } from "./render.js";
19
20
  import { placeBlock } from "./place.js";
20
21
  import { isRenderable } from "./protocol.js";
22
+ // [LAW:dataflow-not-control-flow] With no live region claimed, stepping around
23
+ // it is the text itself: every write takes the same path.
24
+ const NO_LIVE_REGION = { around: (text) => text };
25
+ // Rows as the bytes that reach the terminal. Every row but the last is closed
26
+ // by a line break, and the last one when `closed` says so. Crop last, and crop
27
+ // the line-end with the rest: in the reference `end` is the tail of the printed
28
+ // line, so a line cut at the edge loses it too.
29
+ function emit({ rows, closed, cropWidth }) {
30
+ const output = rows.flatMap((row, index) => closed || index < rows.length - 1 ? [...row, Segment.line()] : row);
31
+ return [...Segment.cropLines(output, cropWidth)];
32
+ }
33
+ // The time `log` stamps: local 24-hour `HH:MM:SS`, what the reference's `%X`
34
+ // prints under the C locale a Python program runs in unless it asks for
35
+ // another. The machine's locale never reaches it, so neither does the width
36
+ // an AM/PM suffix would add, and one clock prints one stamp everywhere.
37
+ function logTime(time) {
38
+ // [LAW:no-silent-failure] The clock is the caller's, and an invalid `Date`
39
+ // would otherwise print as `[NaN:NaN:NaN]`.
40
+ if (Number.isNaN(time.getTime()))
41
+ throw new RangeError("Console getDatetime returned an invalid Date");
42
+ const two = (n) => String(n).padStart(2, "0");
43
+ return `[${two(time.getHours())}:${two(time.getMinutes())}:${two(time.getSeconds())}] `;
44
+ }
21
45
  // Three of the five justify methods place what a print draws as a block, the
22
46
  // way Rich's `print` wraps each renderable in `Align`. The other two leave it
23
47
  // where it was drawn: `"default"` asks for no placement, and `"full"` is a way
@@ -36,23 +60,18 @@ const PLACED_BY = {
36
60
  // `justify`, `overflow`, `noWrap` and `tabSize` stay behind, as they do in the
37
61
  // reference: the print's options set the whole run.
38
62
  class TextRun {
39
- items;
40
63
  end;
64
+ text;
41
65
  constructor(items, end) {
42
- this.items = items;
43
66
  this.end = end;
67
+ this.text = items.reduce((joined, item) => joined.append(item), new RichText("", { end: "" }));
44
68
  }
45
69
  *render(options) {
46
- yield* this.text(options).render(options);
70
+ yield* this.text.render(options);
47
71
  yield this.end;
48
72
  }
49
73
  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: "" }));
74
+ return this.text.measure(options);
56
75
  }
57
76
  }
58
77
  // A host with an environment but no streams — what a browser looks like from
@@ -146,25 +165,6 @@ function resolveGetSize(options, environment, stream) {
146
165
  // --- Console ---
147
166
  /** Shared because it is stateless: highlighting nothing has nothing to own. */
148
167
  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
168
  export class Console {
169
169
  _destination;
170
170
  // [LAW:one-source-of-truth] Size flows through a single function. Static
@@ -172,9 +172,15 @@ export class Console {
172
172
  // caller-supplied `getSize` overrides. Every size read in this class goes
173
173
  // through `_getSize()` — no second path, no second source to drift.
174
174
  _getSize;
175
+ _getDatetime;
176
+ // The stamp the last `log` row was given, which the next row's stamp is
177
+ // compared with: the reference's `LogRender._last_time`, one per console.
178
+ // [LAW:no-ambient-temporal-coupling] "same as the previous row" is a fact
179
+ // about an earlier call, so it is state this console owns, not order luck.
180
+ _lastLogTime = null;
175
181
  _style;
176
182
  _isTerminal;
177
- _forceInteractive;
183
+ _interactive;
178
184
  _file;
179
185
  // [LAW:one-source-of-truth] The resolved output stream, not the `stderr`
180
186
  // flag that selected it. Storing the answer instead of the question is what
@@ -194,6 +200,7 @@ export class Console {
194
200
  // boolean+string would admit the impossible state "not capturing, buffer
195
201
  // non-empty" — `string | null` makes that unrepresentable.
196
202
  _capture;
203
+ _liveRegion;
197
204
  constructor(options) {
198
205
  const environment = options?.environment ?? ambientEnvironment();
199
206
  const stream = boundStream(environment, options?.stderr ?? false);
@@ -211,11 +218,13 @@ export class Console {
211
218
  hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
212
219
  };
213
220
  this._getSize = resolveGetSize(options, environment, stream);
221
+ this._getDatetime = options?.getDatetime ?? (() => new Date());
214
222
  // [LAW:no-ambient-temporal-coupling] The theme is assigned before the
215
223
  // style, because the console's own style may be one of the theme's names.
216
224
  this._theme = options?.theme ?? DEFAULT_THEME;
217
225
  this._style = this._theme.resolve(options?.style ?? NULL_STYLE);
218
- this._forceInteractive = options?.forceInteractive;
226
+ // Rich's rule: a terminal is interactive unless it is a dumb one.
227
+ this._interactive = options?.forceInteractive ?? (this._isTerminal && !isDumbTerminal(environment.env));
219
228
  this._file = options?.file;
220
229
  this._record = options?.record ?? false;
221
230
  this._markup = options?.markup !== false;
@@ -225,6 +234,7 @@ export class Console {
225
234
  this._asciiOnly = options?.asciiOnly ?? false;
226
235
  this._recorded = [];
227
236
  this._capture = null;
237
+ this._liveRegion = NO_LIVE_REGION;
228
238
  }
229
239
  // --- Properties ---
230
240
  get size() {
@@ -240,9 +250,7 @@ export class Console {
240
250
  return this._isTerminal;
241
251
  }
242
252
  get isInteractive() {
243
- if (this._forceInteractive !== undefined)
244
- return this._forceInteractive;
245
- return this.isTerminal;
253
+ return this._interactive;
246
254
  }
247
255
  get colorSystem() {
248
256
  return this._destination.colorSystem;
@@ -282,10 +290,17 @@ export class Console {
282
290
  theme: this._theme,
283
291
  onStyleError: this._onStyleError,
284
292
  colorSystem: this._destination.colorSystem,
293
+ markup: this._markup,
294
+ highlight: this._highlight,
295
+ highlighter: this._highlighter,
285
296
  };
286
297
  }
287
298
  // --- Print ---
288
299
  print(...args) {
300
+ this._writeSegments(emit(this._draw(args, this.options)));
301
+ }
302
+ // What `print` draws for `args` at `options.maxWidth`.
303
+ _draw(args, options) {
289
304
  // Extract options from last arg if it's a PrintOptions
290
305
  let opts = {};
291
306
  let items;
@@ -309,53 +324,56 @@ export class Console {
309
324
  else {
310
325
  items = args;
311
326
  }
312
- const doMarkup = opts.markup ?? this._markup;
313
- const doHighlight = opts.highlight ?? this._highlight;
327
+ // What a string is read as, wherever in this print it lands: an argument,
328
+ // or a cell or label inside a renderable argument. `print`'s own flags
329
+ // outrank the console's, as Rich's `options.update` has them.
330
+ const strings = {
331
+ markup: opts.markup ?? this._markup,
332
+ highlight: opts.highlight ?? this._highlight,
333
+ highlighter: this._highlighter,
334
+ };
314
335
  const sep = opts.sep ?? " ";
315
336
  const end = opts.end ?? "\n";
316
337
  const softWrap = opts.softWrap ?? false;
317
338
  const printStyle = this._theme.resolve(opts.style ?? NULL_STYLE);
318
339
  // 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
340
+ // Text — a string, a `RichText`, or a scalar — runs together: adjacent text
320
341
  // items are one block, joined by `sep` and ended by `end`. Any other
321
342
  // renderable is a block of its own that occupies whole lines, so neither
322
343
  // `sep` nor `end` ever touches it. That is the reference's split — `end`
323
344
  // 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.
345
+ // stack with no blank line between them. Data is cut where the reference
346
+ // cuts it: a container (`isExpandable`) is a block, a scalar is text. A
347
+ // container is laid out by `Pretty` across as many lines as it needs, and
348
+ // joined into a run it would start partway along a line and leave its
349
+ // closing bracket where the next item carries on. The line is not closed by
350
+ // asking where the cursor sits: `print("a\n")` is a line and an empty one,
351
+ // here as in the reference and in every other `print`, and only the kind of
352
+ // the item can tell that trailing break from a `Panel`'s. A call with
353
+ // nothing to print is one empty text run, so it still ends the line.
331
354
  const blocks = items.length === 0 ? [{ kind: "text", items: [] }] : [];
332
355
  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,
356
+ // Four arms, and they are the whole domain. A `RichText` is already text.
357
+ // No arm's own `end` reaches the line: `TextRun` joins the run with
358
+ // `append`, which leaves each part's behind, as Rich's `Text(sep).join`
359
+ // does, because the line end of a run is the print's. Any other renderable draws itself,
336
360
  // as a block. A string is the only kind of argument that can *contain*
337
361
  // 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
362
+ // Everything else is data — a block when it is a container, text when it
363
+ // is not — and `Pretty` is the single authority on how a
339
364
  // JavaScript value displays — `String(value)` was a second, weaker one
340
365
  // that answered `[object Object]` for every object and let the markup
341
366
  // parser eat it. [LAW:one-source-of-truth]
342
367
  let text;
343
368
  if (item instanceof RichText) {
344
- const richText = item.copy();
345
- richText.end = "";
346
- text = richText;
369
+ text = item;
347
370
  }
348
371
  else if (isRenderable(item)) {
349
372
  blocks.push({ kind: "lines", renderable: item });
350
373
  continue;
351
374
  }
352
375
  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;
376
+ text = renderStr(item, strings);
359
377
  }
360
378
  else {
361
379
  // The highlighter travels with the value. `highlight` and a custom
@@ -363,18 +381,21 @@ export class Console {
363
381
  // formatted object the same way they reach a printed string; a `Pretty`
364
382
  // left to its own default would outrank both. [LAW:dataflow-not-control-flow]
365
383
  // 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,
384
+ const pretty = new Pretty(item, {
385
+ ...UNSEEN_DATA_BOUNDS,
386
+ highlighter: activeHighlighter(strings) ?? NO_HIGHLIGHT,
373
387
  });
388
+ // A scalar spells the same at every width, so it joins its run as the
389
+ // one text it is.
390
+ if (isExpandable(item)) {
391
+ blocks.push({ kind: "lines", renderable: pretty });
392
+ continue;
393
+ }
394
+ text = pretty.toText(options);
374
395
  }
375
396
  const run = blocks.at(-1);
376
397
  if (run?.kind === "text")
377
- run.items.push(new RichText(sep, { end: "" }), text);
398
+ run.items.push(new RichText(sep), text);
378
399
  else
379
400
  blocks.push({ kind: "text", items: [text] });
380
401
  }
@@ -384,7 +405,8 @@ export class Console {
384
405
  // at the width, as Rich's does. What `"ignore"` does to a line — no edge,
385
406
  // no justify — is `RichText`'s to apply, so it crosses as it was asked for.
386
407
  const renderOpts = {
387
- ...this.options,
408
+ ...options,
409
+ ...strings,
388
410
  justify: opts.justify === "default" ? undefined : opts.justify,
389
411
  overflow: opts.overflow ?? (softWrap ? "ignore" : undefined),
390
412
  noWrap: softWrap,
@@ -404,10 +426,9 @@ export class Console {
404
426
  // block is closed line by line, as `Align` closes it. Unplaced, a text run
405
427
  // is closed exactly where its content — its `end` included — breaks; any
406
428
  // 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.
429
+ // `Panel` ends in a line break and a bare `ProgressBar` does not (a line
430
+ // fragment — test/seam/line-ends.ts), and both leave the next print at the
431
+ // start of a line.
411
432
  const drawBlock = (block) => {
412
433
  const renderable = block.kind === "text" ? new TextRun(block.items, terminator) : block.renderable;
413
434
  if (align)
@@ -422,31 +443,56 @@ export class Console {
422
443
  // Every line end goes through the same writeSegments funnel as the text it
423
444
  // ends, so it survives recording — otherwise `exportText` and `exportHtml`
424
445
  // would join consecutive prints onto a single line. [LAW:single-enforcer]
446
+ // A block left open carries on along the next block's first line, so the
447
+ // rows are read off the joined output rather than counted per block.
425
448
  const output = [];
449
+ let closed = true;
426
450
  for (const block of blocks) {
427
- const { lines, closed } = drawBlock(block);
428
- lines.forEach((line, index) => {
451
+ const drawn = drawBlock(block);
452
+ drawn.lines.forEach((line, index) => {
429
453
  output.push(...styleContent(line));
430
- if (closed || index < lines.length - 1)
454
+ if (drawn.closed || index < drawn.lines.length - 1)
431
455
  output.push(Segment.line());
432
456
  });
457
+ closed = drawn.closed;
433
458
  }
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
459
  // Soft wrap turns cropping off whatever `crop` says, because a line it left
437
460
  // whole is meant to reach the terminal whole. [LAW:dataflow-not-control-flow]
438
461
  // Not cropping is an unbounded width rather than a skipped step — the same
439
462
  // spelling `RichText` uses for `"ignore"`.
440
- const cropWidth = !softWrap && (opts.crop ?? true) ? this.width : Infinity;
441
- this._writeSegments([...Segment.cropLines(output, cropWidth)]);
463
+ const cropWidth = !softWrap && (opts.crop ?? true) ? options.maxWidth : Infinity;
464
+ return { rows: Segment.splitLines(output), closed, cropWidth };
442
465
  }
466
+ // The reference's `LogRender`: a grid row of two cells, the time and beside it
467
+ // whatever `print` would draw for `args` in the width that is left. A grid row
468
+ // is at least one line tall and always closed, whatever `end` left open
469
+ // inside its cell. The time gives up cells before the content does, so a
470
+ // console narrower than the time still shows every argument. The time takes
471
+ // the console's style alone, as the reference styles only the renderables.
472
+ // A time that reads as the previous row's is left blank at its width, the
473
+ // reference's `omit_repeated_times`, so a burst of rows carries one stamp.
443
474
  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);
475
+ const options = this.options;
476
+ const stamped = logTime(this._getDatetime());
477
+ const repeated = stamped === this._lastLogTime;
478
+ const time = new RichText(stamped, { end: "" });
479
+ time.stylize("log.time");
480
+ const width = Math.min(time.cellLength, options.maxWidth - 1);
481
+ const { rows, cropWidth } = this._draw(args, { ...options, maxWidth: options.maxWidth - width });
482
+ const [stampLine = []] = Segment.splitLines(time.render({ ...options, maxWidth: time.cellLength }));
483
+ const styled = (cell) => [...Segment.applyStyle(cell, this._style)];
484
+ const stamp = styled(Segment.adjustLineLength(stampLine, width));
485
+ const blank = styled([new Segment(" ".repeat(width))]);
486
+ const head = repeated ? blank : stamp;
487
+ const grid = Array.from({ length: Math.max(1, rows.length) }, (_, index) => [
488
+ ...(index === 0 ? head : blank),
489
+ ...(rows[index] ?? []),
490
+ ]);
491
+ // The row is the column wider than its cell; an uncropped cell stays so.
492
+ this._writeSegments(emit({ rows: grid, closed: true, cropWidth: cropWidth + width }));
493
+ // Remembered only once the row is written: a log that threw printed no
494
+ // stamp, so the next row's must show.
495
+ this._lastLogTime = stamped;
450
496
  }
451
497
  // [LAW:one-source-of-truth] `RuleOptions` is `Rule`'s, not a restatement of
452
498
  // it. `console.ts` used to declare its own copy and field-by-field forward
@@ -466,6 +512,24 @@ export class Console {
466
512
  : JSONRenderable.fromData(json, options);
467
513
  this.print(renderable, { softWrap: true });
468
514
  }
515
+ // --- Live region ---
516
+ /**
517
+ * Hold `region` under everything this console writes until the returned
518
+ * call lets it go. One region at a time: a second would be erased and
519
+ * painted over by the first, so claiming one while another is held throws.
520
+ */
521
+ claimLiveRegion(region) {
522
+ if (this._liveRegion !== NO_LIVE_REGION) {
523
+ throw new Error("A live display is already running on this console; stop it before starting another.");
524
+ }
525
+ this._liveRegion = region;
526
+ // Letting go of a region no longer held — a second call, or after another
527
+ // was claimed — leaves the one that is.
528
+ return () => {
529
+ if (this._liveRegion === region)
530
+ this._liveRegion = NO_LIVE_REGION;
531
+ };
532
+ }
469
533
  // --- Capture ---
470
534
  // [LAW:one-source-of-truth] Redirect semantics match Python Rich's
471
535
  // `Console.capture()`: while a capture is active the underlying target
@@ -495,12 +559,17 @@ export class Console {
495
559
  this._recorded = [];
496
560
  return html;
497
561
  }
562
+ // [LAW:single-enforcer] As `exportHtml`: `encodeSvg` draws, this clears.
563
+ exportSvg({ theme, title = "Rich", clear = true } = {}) {
564
+ const svg = encodeSvg(this._recorded, { theme, title, width: this.width });
565
+ if (clear)
566
+ this._recorded = [];
567
+ return svg;
568
+ }
498
569
  // --- Internal ---
499
570
  // [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.
571
+ // same `segmentsToString` used by `renderToString`. Recording captures every
572
+ // non-control segment for replay.
504
573
  _writeSegments(segments) {
505
574
  if (this._record) {
506
575
  for (const segment of segments) {
@@ -522,6 +591,6 @@ export class Console {
522
591
  return;
523
592
  }
524
593
  const target = this._file ?? defaultSink(this._stream);
525
- target.write(text);
594
+ target.write(this._liveRegion.around(text));
526
595
  }
527
596
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Easing and phase: the one vocabulary for "how far along" — every function
3
+ * here returns progress in [0, 1].
4
+ *
5
+ * Two kinds of curve, composed as `ease(phase(t))`:
6
+ *
7
+ * - A **phase** turns a time `t` (seconds) into raw progress: `once` runs
8
+ * 0 → 1 over a span and holds, `loop` wraps 0 → 1 every period,
9
+ * `pingPong` goes 0 → 1 → 0 every period. A per-cell offset is added to `t`
10
+ * before the phase sees it — `phase(t + offset)` — so the cells of one
11
+ * effect run the same curve shifted in time.
12
+ * - An **ease** reshapes that progress: any `(x) => y` on [0, 1]. The
13
+ * built-ins are CSS's easing functions (CSS Easing Functions Level 1):
14
+ * `linear`, `cubic-bezier()` with its named curves, and `steps()` with its
15
+ * jump terms, plus `sine` for pulses — a half-cosine, so a `pingPong`
16
+ * through it brightens and dims with no corner at either end.
17
+ *
18
+ * [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. `t` is an
19
+ * argument; whoever draws frames owns the clock and samples these at
20
+ * whatever rate it likes, so the same `t` always gives the same progress.
21
+ *
22
+ * [LAW:one-type-per-behavior] An ease is a plain function value. CSS's named
23
+ * curves are four instances of `cubicBezier`, not four implementations, and
24
+ * `ColorRamp` and every effect take the same `Ease`.
25
+ *
26
+ * Tier 0 of `src/core/`: imports nothing, so `themes/` and every later
27
+ * `core/` module reach it downhill. [LAW:one-way-deps]
28
+ */
29
+ /** Progress in, progress out: a reshaping of [0, 1]. */
30
+ export type Ease = (x: number) => number;
31
+ /** Time in seconds (plus any per-cell offset) in, progress in [0, 1] out. */
32
+ export type Phase = (t: number) => number;
33
+ /**
34
+ * CSS `cubic-bezier(x1, y1, x2, y2)`: the curve from (0, 0) to (1, 1) with
35
+ * those two control points, read as y at a given x. `x1` and `x2` must lie in
36
+ * [0, 1] — as in CSS — which keeps x monotonic in the curve parameter, so
37
+ * every input has exactly one output. `y1` and `y2` may leave [0, 1]; the
38
+ * curve then overshoots, as a CSS "back" curve does.
39
+ */
40
+ export declare function cubicBezier(x1: number, y1: number, x2: number, y2: number): Ease;
41
+ /**
42
+ * Where a `steps()` curve jumps, in CSS's terms. `jump-start` jumps at 0,
43
+ * `jump-end` at 1, `jump-both` at both, `jump-none` at neither; `start` and
44
+ * `end` are CSS's older spellings of the first two.
45
+ */
46
+ export type StepPosition = "jump-start" | "jump-end" | "jump-none" | "jump-both" | "start" | "end";
47
+ /**
48
+ * CSS `steps(n, position)`: progress held flat in `n` equal intervals, jumping
49
+ * between them. The output takes `n` distinct values between the jumps
50
+ * `position` names — CSS's step algorithm, so `steps(1, "end")` is 0 until
51
+ * the very end.
52
+ */
53
+ export declare function steps(n: number, position?: StepPosition): Ease;
54
+ /**
55
+ * Every built-in ease by the name a template spells it with — CSS's keyword
56
+ * names, plus `sine`, plus `step`: the spelling `ColorRamp` used before this
57
+ * vocabulary existed, kept so every template written against it renders the
58
+ * same. It is CSS's `step-end`, the same function value.
59
+ */
60
+ export declare const EASES: {
61
+ readonly linear: Ease;
62
+ readonly ease: Ease;
63
+ readonly "ease-in": Ease;
64
+ readonly "ease-out": Ease;
65
+ readonly "ease-in-out": Ease;
66
+ readonly "step-start": Ease;
67
+ readonly "step-end": Ease;
68
+ readonly step: Ease;
69
+ readonly sine: Ease;
70
+ };
71
+ export type EaseName = keyof typeof EASES;
72
+ /**
73
+ * The gate a spelled ease crosses. [LAW:parse-dont-validate] — a name in, the
74
+ * function out, so nothing downstream holds a string it must check again.
75
+ * Unknown names throw naming every legal one. [LAW:no-silent-failure]
76
+ */
77
+ export declare function parseEase(name: string): Ease;
78
+ /** The three phase shapes. Each takes seconds and returns a `Phase`. */
79
+ export declare const Phase: {
80
+ /** 0 before `start`, rising to 1 over `duration` seconds, then held at 1. */
81
+ once(start: number, duration: number): Phase;
82
+ /** 0 → 1 every `period` seconds, wrapping back to 0; negative `t` wraps too. */
83
+ loop(period: number): Phase;
84
+ /** 0 → 1 → 0 every `period` seconds: there in the first half, back in the second. */
85
+ pingPong(period: number): Phase;
86
+ };