@promptctl/rich-js 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +9 -7
  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 +98 -18
  9. package/dist/core/color.js +331 -144
  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/osc8.d.ts +6 -6
  33. package/dist/core/osc8.js +9 -21
  34. package/dist/core/paint.d.ts +107 -0
  35. package/dist/core/paint.js +150 -0
  36. package/dist/core/place.d.ts +1 -1
  37. package/dist/core/place.js +4 -6
  38. package/dist/core/pretty.d.ts +105 -2
  39. package/dist/core/pretty.js +527 -97
  40. package/dist/core/protocol.d.ts +41 -3
  41. package/dist/core/protocol.js +20 -0
  42. package/dist/core/render.d.ts +33 -30
  43. package/dist/core/render.js +43 -83
  44. package/dist/core/segment.d.ts +6 -5
  45. package/dist/core/segment.js +11 -15
  46. package/dist/core/strip.d.ts +13 -2
  47. package/dist/core/strip.js +44 -23
  48. package/dist/core/style.d.ts +27 -7
  49. package/dist/core/style.js +97 -34
  50. package/dist/core/text.d.ts +43 -16
  51. package/dist/core/text.js +180 -59
  52. package/dist/core/wrap.d.ts +5 -0
  53. package/dist/core/wrap.js +16 -3
  54. package/dist/host/app.d.ts +31 -19
  55. package/dist/host/app.js +90 -60
  56. package/dist/host/index.d.ts +2 -1
  57. package/dist/host/terminal-host.d.ts +3 -2
  58. package/dist/index.d.ts +17 -10
  59. package/dist/index.js +11 -4
  60. package/dist/node/prompt.d.ts +13 -0
  61. package/dist/node/prompt.js +30 -9
  62. package/dist/node/save.d.ts +4 -3
  63. package/dist/node/save.js +6 -3
  64. package/dist/node/terminal-host.d.ts +3 -2
  65. package/dist/node/traceback.d.ts +2 -1
  66. package/dist/node/traceback.js +3 -39
  67. package/dist/renderables/align.js +2 -9
  68. package/dist/renderables/columns.d.ts +45 -29
  69. package/dist/renderables/columns.js +142 -103
  70. package/dist/renderables/constrain.js +6 -12
  71. package/dist/renderables/effect.d.ts +98 -0
  72. package/dist/renderables/effect.js +160 -0
  73. package/dist/renderables/embed.d.ts +87 -30
  74. package/dist/renderables/embed.js +134 -45
  75. package/dist/renderables/group.d.ts +6 -3
  76. package/dist/renderables/group.js +5 -0
  77. package/dist/renderables/layout.d.ts +6 -23
  78. package/dist/renderables/layout.js +24 -113
  79. package/dist/renderables/live.d.ts +40 -16
  80. package/dist/renderables/live.js +96 -83
  81. package/dist/renderables/markdown.d.ts +16 -2
  82. package/dist/renderables/markdown.js +521 -118
  83. package/dist/renderables/padding.d.ts +22 -1
  84. package/dist/renderables/padding.js +30 -24
  85. package/dist/renderables/panel.d.ts +31 -33
  86. package/dist/renderables/panel.js +137 -141
  87. package/dist/renderables/progress.d.ts +104 -16
  88. package/dist/renderables/progress.js +205 -81
  89. package/dist/renderables/progressBar.d.ts +2 -2
  90. package/dist/renderables/progressBar.js +30 -19
  91. package/dist/renderables/prompt.d.ts +37 -15
  92. package/dist/renderables/prompt.js +125 -85
  93. package/dist/renderables/ratio.d.ts +123 -0
  94. package/dist/renderables/ratio.js +213 -0
  95. package/dist/renderables/rule.d.ts +1 -0
  96. package/dist/renderables/rule.js +44 -48
  97. package/dist/renderables/spinner.d.ts +28 -2
  98. package/dist/renderables/spinner.js +52 -19
  99. package/dist/renderables/status.d.ts +13 -7
  100. package/dist/renderables/status.js +15 -29
  101. package/dist/renderables/syntax.d.ts +37 -7
  102. package/dist/renderables/syntax.js +134 -83
  103. package/dist/renderables/table.d.ts +58 -20
  104. package/dist/renderables/table.js +370 -192
  105. package/dist/renderables/traceback.d.ts +5 -4
  106. package/dist/renderables/traceback.js +239 -68
  107. package/dist/renderables/tree.d.ts +10 -2
  108. package/dist/renderables/tree.js +61 -34
  109. package/dist/renderables/viewport.js +2 -4
  110. package/dist/template-bindings/color-funcs.d.ts +31 -9
  111. package/dist/template-bindings/color-funcs.js +40 -14
  112. package/dist/template-bindings/helpers.d.ts +4 -2
  113. package/dist/template-bindings/helpers.js +5 -20
  114. package/dist/template-bindings/index.d.ts +29 -17
  115. package/dist/template-bindings/index.js +36 -20
  116. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  117. package/dist/template-bindings/palette-funcs.js +11 -7
  118. package/dist/template-bindings/style-funcs.js +4 -2
  119. package/dist/themes/buildPalette.d.ts +24 -3
  120. package/dist/themes/buildPalette.js +52 -8
  121. package/dist/themes/colorMath.d.ts +29 -17
  122. package/dist/themes/colorMath.js +74 -48
  123. package/dist/themes/colorRef.d.ts +2 -17
  124. package/dist/themes/colorRef.js +13 -31
  125. package/dist/themes/palette.d.ts +24 -0
  126. package/dist/themes/palette.js +46 -3
  127. package/dist/themes/ramp.d.ts +21 -34
  128. package/dist/themes/ramp.js +23 -42
  129. package/dist/themes/registry.js +39 -28
  130. package/dist/widgets/button.d.ts +2 -6
  131. package/dist/widgets/button.js +9 -40
  132. package/dist/widgets/checkbox.d.ts +2 -5
  133. package/dist/widgets/checkbox.js +9 -31
  134. package/dist/widgets/dropdown.d.ts +2 -5
  135. package/dist/widgets/dropdown.js +14 -48
  136. package/dist/widgets/ink.d.ts +22 -0
  137. package/dist/widgets/ink.js +31 -0
  138. package/dist/widgets/slider.d.ts +2 -5
  139. package/dist/widgets/slider.js +11 -34
  140. package/dist/widgets/text-input.d.ts +19 -10
  141. package/dist/widgets/text-input.js +156 -147
  142. package/dist/widgets/themed-widget.d.ts +20 -0
  143. package/dist/widgets/themed-widget.js +89 -0
  144. package/dist/widgets/toggle.d.ts +2 -5
  145. package/dist/widgets/toggle.js +8 -31
  146. package/dist/widgets/widget-app.d.ts +2 -0
  147. package/dist/widgets/widget-app.js +13 -6
  148. package/dist/widgets/widget-base.d.ts +2 -0
  149. package/dist/widgets/widget-base.js +9 -2
  150. package/package.json +11 -1
package/dist/host/app.js CHANGED
@@ -24,63 +24,65 @@
24
24
  * under us, how it is suspended — is the `TerminalHost`'s, so the same app
25
25
  * runs in node and in a browser with no path of its own for either.
26
26
  */
27
+ import { frameRate, systemClock } from "../core/clock.js";
27
28
  import { Console } from "../core/console.js";
28
29
  import { Segment } from "../core/segment.js";
29
- import { segmentsToString } from "../core/render.js";
30
+ import { SurfacePainter } from "../core/paint.js";
30
31
  import { fitHeight } from "../core/protocol.js";
31
32
  import { hostEnvironment } from "./host-environment.js";
33
+ // [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
34
+ // it knows, what happens to a style it cannot resolve, markup and
35
+ // highlighting — is the console's to say, so App takes those options as the
36
+ // console declares them. This list is both the type and what is copied: the
37
+ // host alone decides the console's size, colours and sink, so no other
38
+ // property of the options object reaches it, whatever the object carries.
39
+ const DRAW_OPTIONS = ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
40
+ function drawOptions(options) {
41
+ const picked = {};
42
+ const copy = (key) => {
43
+ picked[key] = options[key];
44
+ };
45
+ DRAW_OPTIONS.forEach(copy);
46
+ return picked;
47
+ }
48
+ const DEFAULT_RATE = frameRate(30);
32
49
  // Button presses, motion and the wheel, in the SGR encoding: coordinates as
33
50
  // decimal numbers, so a terminal wider than 223 columns still reports them.
34
- const POINTER_ON = "\x1b[?1006h\x1b[?1000h\x1b[?1003h";
35
- const POINTER_OFF = "\x1b[?1003l\x1b[?1000l\x1b[?1006l";
36
- const SURFACES = {
37
- alternate: {
38
- exact: true,
39
- enter: "\x1b[?1049h" + POINTER_ON,
40
- leave: () => POINTER_OFF + "\x1b[?1049l",
41
- home: () => "\x1b[H",
42
- // The frame is every row of the screen, so there is nothing left under it
43
- // — and after the terminal shrinks, painting the old count would scroll.
44
- painted: (frameRows) => frameRows,
45
- },
46
- inline: {
47
- exact: false,
48
- enter: "",
49
- // Below the frame, where the program's next line belongs.
50
- leave: (rows) => (rows > 0 ? "\n" : ""),
51
- // After N rows written with N-1 newlines between them, the cursor is on
52
- // the last. `ESC[0A` still moves a row on some terminals, so a one-row
53
- // frame is returned to with the carriage return alone.
54
- home: (rows) => (rows > 1 ? `\x1b[${rows - 1}A\r` : "\r"),
55
- // A shorter frame blanks the rows the last one left below it.
56
- painted: (frameRows, lastRows) => Math.max(frameRows, lastRows),
57
- },
51
+ // The terminal reports a pointer by its row on the screen, and an inline
52
+ // frame does not know which row of the screen it starts on, so only the
53
+ // alternate surface gets them.
54
+ const POINTER = {
55
+ alternate: { on: "\x1b[?1006h\x1b[?1000h\x1b[?1003h", off: "\x1b[?1003l\x1b[?1000l\x1b[?1006l" },
56
+ inline: { on: "", off: "" },
58
57
  };
59
- const HIDE_CURSOR = "\x1b[?25l";
60
- const SHOW_CURSOR = "\x1b[?25h";
61
- const RESET_STYLE = "\x1b[0m";
62
- const ERASE_LINE = "\x1b[2K";
63
58
  export class App {
64
59
  host;
65
60
  surface;
66
61
  view;
62
+ clock;
63
+ rate;
64
+ // What is animating now: one token per `animate()` not yet released.
65
+ animations = new Set();
66
+ stopTicking = null;
67
67
  // [LAW:one-source-of-truth] The host is the console's whole environment:
68
68
  // its size, its colours, where bytes go. Every frame renders with this
69
69
  // console's options and is encoded for its destination.
70
70
  console;
71
71
  _phase = "idle";
72
72
  _frame = [];
73
- // The last frame's own rows on the terminal — what `home` rewinds.
74
- rows = 0;
73
+ painter;
75
74
  refreshQueued = false;
76
75
  subscriptions = [];
77
76
  paintHandlers = new Set();
78
77
  settle = () => { };
79
78
  constructor(options) {
80
79
  this.host = options.host;
81
- this.surface = SURFACES[options.surface];
80
+ this.surface = options.surface;
81
+ this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
82
82
  this.view = options.view;
83
- this.console = new Console({ environment: hostEnvironment(options.host), asciiOnly: options.asciiOnly });
83
+ this.clock = options.clock ?? systemClock();
84
+ this.rate = options.rate ?? DEFAULT_RATE;
85
+ this.console = new Console({ ...drawOptions(options), environment: hostEnvironment(options.host) });
84
86
  }
85
87
  get phase() {
86
88
  return this._phase;
@@ -124,6 +126,7 @@ export class App {
124
126
  ];
125
127
  this.enter();
126
128
  this.paint();
129
+ this.tick();
127
130
  });
128
131
  }
129
132
  /**
@@ -140,6 +143,10 @@ export class App {
140
143
  return;
141
144
  this.refreshQueued = true;
142
145
  setTimeout(() => {
146
+ // A frame painted since it was asked for — a tick — drew every change
147
+ // made before it, so there is nothing left to paint.
148
+ if (!this.refreshQueued)
149
+ return;
143
150
  this.refreshQueued = false;
144
151
  // [LAW:types-are-the-program] Only a running app holds the terminal; a
145
152
  // suspended one repaints when it resumes, and a stopped one never does.
@@ -147,6 +154,24 @@ export class App {
147
154
  this.paint();
148
155
  });
149
156
  }
157
+ /**
158
+ * Paint at the app's `rate` until the returned function is called: what an
159
+ * effect sampled at the frame's time asks for while it moves. The app ticks
160
+ * while anything is animating and it holds the terminal, and stops when
161
+ * nothing is, so a still app paints only when asked. The animation's first
162
+ * frame does not wait out an interval: at one frame every two seconds, that
163
+ * would hold its start off screen for two.
164
+ */
165
+ animate() {
166
+ const animation = {};
167
+ this.animations.add(animation);
168
+ this.tick();
169
+ this.refresh();
170
+ return () => {
171
+ this.animations.delete(animation);
172
+ this.tick();
173
+ };
174
+ }
150
175
  /**
151
176
  * Hand the terminal to whatever launched the program, and take it back and
152
177
  * repaint when that resumes it. What sends the app here is the app's own —
@@ -156,6 +181,7 @@ export class App {
156
181
  if (this._phase !== "running")
157
182
  return;
158
183
  this._phase = "suspended";
184
+ this.tick();
159
185
  this.leave();
160
186
  await this.host.suspend();
161
187
  // Stopped while suspended — by a signal, say: the terminal is already
@@ -163,11 +189,9 @@ export class App {
163
189
  if (this._phase !== "suspended")
164
190
  return;
165
191
  this._phase = "running";
166
- // What the last frame occupied is gone under whatever ran meanwhile, so
167
- // the next one starts where the cursor is.
168
- this.rows = 0;
169
192
  this.enter();
170
193
  this.paint();
194
+ this.tick();
171
195
  }
172
196
  /**
173
197
  * Hear each frame once it is on screen — what `frame` now returns. A
@@ -193,10 +217,12 @@ export class App {
193
217
  // --- the terminal ---
194
218
  enter() {
195
219
  this.host.setRawMode(true);
196
- this.host.write(this.surface.enter + HIDE_CURSOR);
220
+ this.painter.take();
221
+ this.host.write(POINTER[this.surface].on);
197
222
  }
198
223
  leave() {
199
- this.host.write(RESET_STYLE + SHOW_CURSOR + this.surface.leave(this.rows));
224
+ this.host.write(POINTER[this.surface].off);
225
+ this.painter.handBack();
200
226
  this.host.setRawMode(false);
201
227
  }
202
228
  end(outcome) {
@@ -204,6 +230,7 @@ export class App {
204
230
  if (phase === "idle" || phase === "stopped")
205
231
  return;
206
232
  this._phase = "stopped";
233
+ this.tick();
207
234
  for (const unsubscribe of this.subscriptions)
208
235
  unsubscribe();
209
236
  this.subscriptions = [];
@@ -213,6 +240,22 @@ export class App {
213
240
  this.host.stop();
214
241
  this.settle(outcome);
215
242
  }
243
+ // [LAW:single-enforcer] Whether the clock is ticking follows from the
244
+ // phase and what is animating, and is brought in line with them here
245
+ // alone, after every change to either — so a timer runs exactly while a
246
+ // running app has something animating, and none outlives the app.
247
+ tick() {
248
+ const wanted = this._phase === "running" && this.animations.size > 0;
249
+ const ticking = this.stopTicking;
250
+ if (ticking === null) {
251
+ if (wanted)
252
+ this.stopTicking = this.clock.every(this.rate, () => this.paint());
253
+ }
254
+ else if (!wanted) {
255
+ ticking();
256
+ this.stopTicking = null;
257
+ }
258
+ }
216
259
  // [LAW:no-silent-failure] A frame that throws ends the app with that
217
260
  // error: the terminal is handed back, and `run` rejects with it.
218
261
  paint() {
@@ -224,31 +267,18 @@ export class App {
224
267
  }
225
268
  }
226
269
  draw() {
270
+ // This frame draws every change made before it, a queued refresh's included.
271
+ this.refreshQueued = false;
227
272
  const options = this.console.options;
228
- const { maxWidth: cols } = options;
229
- const height = { rows: options.height.rows, exact: this.surface.exact };
230
- const lines = Segment.splitLines(this.view().render({ ...options, height }));
273
+ const height = this.painter.height(options.height.rows);
274
+ // [LAW:no-ambient-temporal-coupling] The frame owner reads the clock,
275
+ // once a frame; the view is handed the time as data.
276
+ const lines = Segment.splitLines(this.view(this.clock.now()).render({ ...options, height }));
231
277
  // The app set the budget, so the app shapes what comes back: no deeper
232
- // than the terminal — a taller frame would scroll the rows `home` counts
233
- // back over — and, as a region, exactly that deep. No row wider than the
234
- // terminal either: one that soft-wrapped would push every row below it
235
- // down a row the frame does not know about.
236
- const frame = fitHeight(lines.slice(0, height.rows), height).map((line) => Segment.adjustLineLength(line, cols, undefined, false));
237
- const painted = Math.min(this.surface.painted(frame.length, this.rows), height.rows);
238
- // Each row is erased as it is reached rather than the frame cleared
239
- // first, so no blank screen shows between two frames. The erase leads
240
- // its row: after a row that fills the width, the cursor sits on its last
241
- // cell, and an erase there would take it.
242
- const destination = this.console.destination;
243
- const body = Array.from({ length: painted }, (_, row) => ERASE_LINE + segmentsToString(frame[row] ?? [], destination)).join("\n");
244
- // Rows blanked below the frame are not the frame's: the cursor goes back
245
- // up to its last row, so the next frame and the program's next line start
246
- // from the frame's own height.
247
- const blanked = painted - Math.max(frame.length, 1);
248
- const back = blanked > 0 ? `\x1b[${blanked}A` : "";
249
- this.host.write(this.surface.home(this.rows) + body + back);
278
+ // than the terminal — a taller frame would scroll the rows the painter
279
+ // goes back over — and, as a region, exactly that deep.
280
+ const frame = this.painter.paint(() => fitHeight(lines.slice(0, height.rows), height), { rows: height.rows, cols: options.maxWidth }, this.console.destination);
250
281
  this._frame = frame;
251
- this.rows = frame.length;
252
282
  for (const handler of [...this.paintHandlers])
253
283
  handler(frame);
254
284
  }
@@ -2,4 +2,5 @@ export { BrowserTerminalHost } from "./terminal-host.js";
2
2
  export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
3
3
  export { hostEnvironment } from "./host-environment.js";
4
4
  export { App } from "./app.js";
5
- export type { AppOptions, AppPhase, Surface } from "./app.js";
5
+ export type { AppOptions, AppPhase } from "./app.js";
6
+ export type { Surface } from "../core/paint.js";
@@ -37,6 +37,7 @@
37
37
  * not an omniscient handle to "the process."
38
38
  */
39
39
  import type { Unsubscribe } from "../core/subscription.js";
40
+ import type { Env } from "../core/env.js";
40
41
  export interface TerminalSize {
41
42
  readonly cols: number;
42
43
  readonly rows: number;
@@ -90,7 +91,7 @@ export interface TerminalHost {
90
91
  * rather than being read off whatever `process` the renderer happens to
91
92
  * share a runtime with, which in a browser is none.
92
93
  */
93
- readonly env: NodeJS.ProcessEnv;
94
+ readonly env: Env;
94
95
  /**
95
96
  * Run `handler` once, when the program ends by any path this host can
96
97
  * observe, so whoever changed the terminal can hand it back. The program
@@ -166,7 +167,7 @@ export declare class BrowserTerminalHost implements TerminalHost {
166
167
  constructor(options: BrowserTerminalHostOptions);
167
168
  get isTTY(): boolean;
168
169
  get writesToTerminal(): boolean;
169
- get env(): NodeJS.ProcessEnv;
170
+ get env(): Env;
170
171
  write(data: Uint8Array | string): void;
171
172
  size(): TerminalSize;
172
173
  setRawMode(_raw: boolean): void;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export { cellLen, setCellSize, splitText, chopCells, cellFit, asCellCol, asCodePoint, } from "./core/cells.js";
2
2
  export type { CellCol, CodeUnit, CodePoint } from "./core/cells.js";
3
- export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
- export type { DetectColorOptions, Destination } from "./core/color.js";
3
+ export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
+ export type { ColorSystemName, DetectColorOptions, Destination } from "./core/color.js";
5
5
  export { Oklch, IDENTITY, INVERT_LIGHTNESS, isIdentityKey, } from "./core/oklch.js";
6
6
  export type { ThemeKey, OklchWeights } from "./core/oklch.js";
7
7
  export { Palette } from "./themes/palette.js";
8
- export { resolveColorRef, parseHexColor, ColorRefError, HEX_COLOR_RE, } from "./themes/colorRef.js";
8
+ export { resolveColorRef, ColorRefError } from "./themes/colorRef.js";
9
9
  export { buildPalette } from "./themes/buildPalette.js";
10
10
  export type { BaseColors } from "./themes/buildPalette.js";
11
11
  export { DEFAULT_TERMINAL_THEME, MONOKAI, SVG_EXPORT_THEME, NORD, GRUVBOX, DRACULA, TOKYO_NIGHT, FLEXOKI, CYBERPUNK, CATPPUCCIN_MOCHA, CATPPUCCIN_LATTE, CATPPUCCIN_FRAPPE, CATPPUCCIN_MACCHIATO, SOLARIZED_DARK, SOLARIZED_LIGHT, ROSE_PINE, ROSE_PINE_MOON, ROSE_PINE_DAWN, ATOM_ONE_DARK, ATOM_ONE_LIGHT, TEXTUAL_DARK, TEXTUAL_LIGHT, TEXTUAL_ANSI, } from "./themes/terminalThemes.js";
@@ -13,10 +13,14 @@ export { getThemePalette, listThemePalettes } from "./themes/registry.js";
13
13
  export type { ThemeName } from "./themes/registry.js";
14
14
  export { transposePalette, themeKeyForRoot, isAnchored, ANCHORED_ROOTS, } from "./themes/transpose.js";
15
15
  export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, ensureDrawn, drawnColour, } from "./themes/colorMath.js";
16
- export { ColorRamp, RAMP_EASING_NAMES, parseRampEasing } from "./themes/ramp.js";
17
- export type { ColorStop, RampEasing } from "./themes/ramp.js";
16
+ export { ColorRamp } from "./themes/ramp.js";
17
+ export type { ColorStop } from "./themes/ramp.js";
18
+ export { EASES, Phase, cubicBezier, parseEase, steps } from "./core/easing.js";
19
+ export type { Ease, StepPosition } from "./core/easing.js";
20
+ export { frameRate, systemClock } from "./core/clock.js";
21
+ export type { Clock, FrameRate } from "./core/clock.js";
18
22
  export { Style, StyleSyntaxError, StyleStack, Theme, NULL_STYLE, DEFAULT_STYLES, } from "./core/style.js";
19
- export type { StyleOptions } from "./core/style.js";
23
+ export type { StyleOptions, TextStyle, LayeredStyle } from "./core/style.js";
20
24
  export { Segment, ControlType, } from "./core/segment.js";
21
25
  export type { ControlCode } from "./core/segment.js";
22
26
  export type { Anchor } from "./core/anchor.js";
@@ -48,8 +52,11 @@ export type { JSONOptions } from "./core/json.js";
48
52
  export { SPINNERS, DEFAULT_SPINNER } from "./core/spinnerData.js";
49
53
  export type { SpinnerData } from "./core/spinnerData.js";
50
54
  export { Console } from "./core/console.js";
51
- export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, PrintOptions, } from "./core/console.js";
55
+ export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, LiveRegion, PrintOptions, } from "./core/console.js";
56
+ export type { Env } from "./core/env.js";
52
57
  export { Constrain } from "./renderables/constrain.js";
58
+ export { Effected } from "./renderables/effect.js";
59
+ export type { CellColors, Effect, EffectCell, EffectedOptions } from "./renderables/effect.js";
53
60
  export { Align } from "./renderables/align.js";
54
61
  export type { Alignment } from "./core/place.js";
55
62
  export { Padding } from "./renderables/padding.js";
@@ -78,13 +85,13 @@ export type { LiveOptions } from "./renderables/live.js";
78
85
  export { Status } from "./renderables/status.js";
79
86
  export type { StatusOptions } from "./renderables/status.js";
80
87
  export { Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn, SpinnerColumn, MofNCompleteColumn, track, } from "./renderables/progress.js";
81
- export type { ProgressOptions, TaskOptions, TaskUpdateOptions } from "./renderables/progress.js";
88
+ export type { MofNCompleteColumnOptions, ProgressColumn, ProgressOptions, SpinnerColumnOptions, Task, TaskOptions, TaskUpdateOptions, TimeRemainingColumnOptions, } from "./renderables/progress.js";
82
89
  export { Prompt, IntPrompt, FloatPrompt, Confirm } from "./renderables/prompt.js";
83
- export type { PromptInput, PromptOptions } from "./renderables/prompt.js";
90
+ export type { ConfirmOptions, PromptInput, PromptOptions } from "./renderables/prompt.js";
84
91
  export { Traceback } from "./renderables/traceback.js";
85
92
  export type { TracebackOptions } from "./renderables/traceback.js";
86
93
  export { Syntax } from "./renderables/syntax.js";
87
- export type { SyntaxOptions } from "./renderables/syntax.js";
94
+ export type { SyntaxLanguage, SyntaxOptions } from "./renderables/syntax.js";
88
95
  export { Markdown } from "./renderables/markdown.js";
89
96
  export type { MarkdownOptions } from "./renderables/markdown.js";
90
97
  export { Layout } from "./renderables/layout.js";
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  // Core primitives
2
2
  export { cellLen, setCellSize, splitText, chopCells, cellFit, asCellCol, asCodePoint, } from "./core/cells.js";
3
- export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
3
+ export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
4
  // Perceptually-uniform color space (manipulation, transposition).
5
5
  export { Oklch, IDENTITY, INVERT_LIGHTNESS, isIdentityKey, } from "./core/oklch.js";
6
6
  // Themes — semantic palettes (distinct from ColorTable quantization LUTs)
7
7
  export { Palette } from "./themes/palette.js";
8
8
  // The one checkpoint turning an author-written colour string — a palette
9
9
  // variable name or a `#RRGGBB` literal — into a colour. [LAW:parse-dont-validate]
10
- export { resolveColorRef, parseHexColor, ColorRefError, HEX_COLOR_RE, } from "./themes/colorRef.js";
10
+ export { resolveColorRef, ColorRefError } from "./themes/colorRef.js";
11
11
  export { buildPalette } from "./themes/buildPalette.js";
12
12
  export { DEFAULT_TERMINAL_THEME, MONOKAI, SVG_EXPORT_THEME, NORD, GRUVBOX, DRACULA, TOKYO_NIGHT, FLEXOKI, CYBERPUNK, CATPPUCCIN_MOCHA, CATPPUCCIN_LATTE, CATPPUCCIN_FRAPPE, CATPPUCCIN_MACCHIATO, SOLARIZED_DARK, SOLARIZED_LIGHT, ROSE_PINE, ROSE_PINE_MOON, ROSE_PINE_DAWN, ATOM_ONE_DARK, ATOM_ONE_LIGHT, TEXTUAL_DARK, TEXTUAL_LIGHT, TEXTUAL_ANSI, } from "./themes/terminalThemes.js";
13
13
  export { getThemePalette, listThemePalettes } from "./themes/registry.js";
@@ -24,9 +24,15 @@ export { transposePalette, themeKeyForRoot, isAnchored, ANCHORED_ROOTS, } from "
24
24
  export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, ensureDrawn, drawnColour, } from "./themes/colorMath.js";
25
25
  // A number → a color through ordered stops, interpolated in OKLCH — the one
26
26
  // color operation whose input is a measurement rather than a color, so a
27
- // gradient (or, with the `step` easing, a threshold cascade) stays inside the
27
+ // gradient (or, with the `step` ease, a threshold cascade) stays inside the
28
28
  // theme system instead of being computed beside it.
29
- export { ColorRamp, RAMP_EASING_NAMES, parseRampEasing } from "./themes/ramp.js";
29
+ export { ColorRamp } from "./themes/ramp.js";
30
+ // Progress in [0, 1]: phases turn seconds into it, eases reshape it. One
31
+ // vocabulary for ramps and time-based effects alike, CSS's names throughout.
32
+ export { EASES, Phase, cubicBezier, parseEase, steps } from "./core/easing.js";
33
+ // The time source and timer whoever draws frames is handed, and the one
34
+ // parser of the rate it ticks at.
35
+ export { frameRate, systemClock } from "./core/clock.js";
30
36
  export { Style, StyleSyntaxError, StyleStack, Theme, NULL_STYLE, DEFAULT_STYLES, } from "./core/style.js";
31
37
  export { Segment, ControlType, } from "./core/segment.js";
32
38
  export { Box } from "./core/box.js";
@@ -63,6 +69,7 @@ export { SPINNERS, DEFAULT_SPINNER } from "./core/spinnerData.js";
63
69
  export { Console } from "./core/console.js";
64
70
  // Renderables
65
71
  export { Constrain } from "./renderables/constrain.js";
72
+ export { Effected } from "./renderables/effect.js";
66
73
  export { Align } from "./renderables/align.js";
67
74
  export { Padding } from "./renderables/padding.js";
68
75
  export { Viewport, SCROLLBAR } from "./renderables/viewport.js";
@@ -13,6 +13,19 @@
13
13
  * // or with options:
14
14
  * const choice = await Prompt.ask("Pick one", nodeAsk, { choices: ["a", "b"] });
15
15
  *
16
+ * The prompt is printed by the `Console` it was asked on — the app's, when it
17
+ * passed one as `console`, a new default one otherwise — and then a line is
18
+ * read, as Rich's `Console.input` prints its prompt and calls `input()`. So
19
+ * the prompt is coloured exactly as that console's own output would be and
20
+ * goes to its target: a console on stderr asks on stderr, beside the
21
+ * invalid-answer messages it prints. It goes as one logical line, never broken
22
+ * at the console's width: the terminal wraps it at whatever width it has.
23
+ *
24
+ * readline reads in non-terminal mode, the terminal's own cooked line
25
+ * discipline, as Python's `input()` does without its readline module: in
26
+ * terminal mode readline redraws the line itself, prompt included, and needs a
27
+ * whole `tty.WriteStream` to draw on, which a console's target need not be.
28
+ *
16
29
  * [LAW:single-enforcer] One readline interface per `nodeAsk` call —
17
30
  * created, asked, closed. No shared `rl` across prompts, no listener-leak
18
31
  * pitfalls when callers stack prompts in a loop.
@@ -13,18 +13,39 @@
13
13
  * // or with options:
14
14
  * const choice = await Prompt.ask("Pick one", nodeAsk, { choices: ["a", "b"] });
15
15
  *
16
+ * The prompt is printed by the `Console` it was asked on — the app's, when it
17
+ * passed one as `console`, a new default one otherwise — and then a line is
18
+ * read, as Rich's `Console.input` prints its prompt and calls `input()`. So
19
+ * the prompt is coloured exactly as that console's own output would be and
20
+ * goes to its target: a console on stderr asks on stderr, beside the
21
+ * invalid-answer messages it prints. It goes as one logical line, never broken
22
+ * at the console's width: the terminal wraps it at whatever width it has.
23
+ *
24
+ * readline reads in non-terminal mode, the terminal's own cooked line
25
+ * discipline, as Python's `input()` does without its readline module: in
26
+ * terminal mode readline redraws the line itself, prompt included, and needs a
27
+ * whole `tty.WriteStream` to draw on, which a console's target need not be.
28
+ *
16
29
  * [LAW:single-enforcer] One readline interface per `nodeAsk` call —
17
30
  * created, asked, closed. No shared `rl` across prompts, no listener-leak
18
31
  * pitfalls when callers stack prompts in a loop.
19
32
  */
20
33
  import * as readline from "node:readline";
21
- export const nodeAsk = (prompt) => new Promise((resolve) => {
22
- const rl = readline.createInterface({
23
- input: process.stdin,
24
- output: process.stdout,
25
- });
26
- rl.question(prompt, (answer) => {
27
- rl.close();
28
- resolve(answer);
34
+ export const nodeAsk = (prompt, console) => {
35
+ console.print(prompt, { end: "", softWrap: true });
36
+ return new Promise((resolve) => {
37
+ const rl = readline.createInterface({
38
+ input: process.stdin,
39
+ // [LAW:types-are-the-program] exception: readline types its output as a
40
+ // whole `NodeJS.WritableStream`, and a console's target is only a
41
+ // `ConsoleSink`. With `terminal: false` readline only ever calls its
42
+ // `write`, whatever its `isTTY` says.
43
+ output: console.file,
44
+ terminal: false,
45
+ });
46
+ rl.question("", (answer) => {
47
+ rl.close();
48
+ resolve(answer);
49
+ });
29
50
  });
30
- });
51
+ };
@@ -8,10 +8,11 @@
8
8
  * refuse to resolve `fs` if a consumer ever pulls it in by accident.
9
9
  *
10
10
  * [LAW:single-enforcer] One funnel per format: `saveText` consumes
11
- * `Console.exportText()` and `saveHtml` consumes `Console.exportHtml()`.
12
- * The encoding and recording logic stays on Console; this file is purely
13
- * the IO sink.
11
+ * `Console.exportText()`, `saveHtml` consumes `Console.exportHtml()`, and
12
+ * `saveSvg` consumes `Console.exportSvg()`. The encoding and recording logic
13
+ * stays on Console; this file is purely the IO sink.
14
14
  */
15
15
  import type { Console } from "../core/console.js";
16
16
  export declare function saveText(out: Console, path: string, options?: Parameters<Console["exportText"]>[0]): void;
17
17
  export declare function saveHtml(out: Console, path: string, options?: Parameters<Console["exportHtml"]>[0]): void;
18
+ export declare function saveSvg(out: Console, path: string, options?: Parameters<Console["exportSvg"]>[0]): void;
package/dist/node/save.js CHANGED
@@ -8,9 +8,9 @@
8
8
  * refuse to resolve `fs` if a consumer ever pulls it in by accident.
9
9
  *
10
10
  * [LAW:single-enforcer] One funnel per format: `saveText` consumes
11
- * `Console.exportText()` and `saveHtml` consumes `Console.exportHtml()`.
12
- * The encoding and recording logic stays on Console; this file is purely
13
- * the IO sink.
11
+ * `Console.exportText()`, `saveHtml` consumes `Console.exportHtml()`, and
12
+ * `saveSvg` consumes `Console.exportSvg()`. The encoding and recording logic
13
+ * stays on Console; this file is purely the IO sink.
14
14
  */
15
15
  import { writeFileSync } from "node:fs";
16
16
  export function saveText(out, path, options) {
@@ -19,3 +19,6 @@ export function saveText(out, path, options) {
19
19
  export function saveHtml(out, path, options) {
20
20
  writeFileSync(path, out.exportHtml(options), "utf-8");
21
21
  }
22
+ export function saveSvg(out, path, options) {
23
+ writeFileSync(path, out.exportSvg(options), "utf-8");
24
+ }
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import type { DataHandler, ExitHandler, ResizeHandler, TerminalHost, TerminalSize } from "../host/terminal-host.js";
31
31
  import type { Unsubscribe } from "../core/subscription.js";
32
+ import type { Env } from "../core/env.js";
32
33
  /**
33
34
  * Subset of NodeJS stream API the node host actually depends on. Tests
34
35
  * construct a host over PassThrough / Writable mocks; production wires it
@@ -79,7 +80,7 @@ export interface NodeTerminalHostOptions {
79
80
  * pass a fixed map so what the host reports does not depend on the shell
80
81
  * that ran them.
81
82
  */
82
- env?: NodeJS.ProcessEnv;
83
+ env?: Env;
83
84
  /**
84
85
  * What the host hears the program end through and suspends it with.
85
86
  * Defaults to `process`; tests pass an emitter that records `kill`.
@@ -90,7 +91,7 @@ export declare class NodeTerminalHost implements TerminalHost {
90
91
  private readonly stdin;
91
92
  private readonly stdout;
92
93
  private readonly process;
93
- readonly env: NodeJS.ProcessEnv;
94
+ readonly env: Env;
94
95
  private readonly dataHandlers;
95
96
  private readonly resizeHandlers;
96
97
  private dataListener;
@@ -15,7 +15,8 @@
15
15
  * payload is trusted to be an `Error`: `@types/node` annotates
16
16
  * `uncaughtException` with `Error`, but `throw "boom"` delivers the raw
17
17
  * string, so the annotation is a claim about the common case rather than a
18
- * guarantee. `report` takes `unknown`.
18
+ * guarantee. `report` takes `unknown` and hands it to `Traceback` as it came,
19
+ * which reads a caught value of any shape.
19
20
  *
20
21
  * [LAW:no-ambient-temporal-coupling] The one channel is also what puts the
21
22
  * report after the terminal is handed back. A program holding the alternate
@@ -15,7 +15,8 @@
15
15
  * payload is trusted to be an `Error`: `@types/node` annotates
16
16
  * `uncaughtException` with `Error`, but `throw "boom"` delivers the raw
17
17
  * string, so the annotation is a claim about the common case rather than a
18
- * guarantee. `report` takes `unknown`.
18
+ * guarantee. `report` takes `unknown` and hands it to `Traceback` as it came,
19
+ * which reads a caught value of any shape.
19
20
  *
20
21
  * [LAW:no-ambient-temporal-coupling] The one channel is also what puts the
21
22
  * report after the terminal is handed back. A program holding the alternate
@@ -26,45 +27,8 @@
26
27
  * the terminal. Node's crash sequence owns the order, not the order in which
27
28
  * `installTraceback` and a host happened to add their listeners.
28
29
  */
29
- import { inspect, types } from "node:util";
30
30
  import { Console } from "../core/console.js";
31
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
32
  /**
69
33
  * The handler currently registered, or a no-op standing in for "none yet".
70
34
  *
@@ -116,7 +80,7 @@ export function installTraceback(options) {
116
80
  // sink would report `isTTY === false` and silently strip colour.
117
81
  const out = new Console({ stderr: true });
118
82
  out.beginCapture();
119
- out.print(new Traceback(toError(reason), options));
83
+ out.print(new Traceback(reason, options));
120
84
  // [LAW:no-silent-failure] Exit only once the bytes have drained. When
121
85
  // stderr is a pipe (CI logs, `2>&1 | tee`), writes are asynchronous and
122
86
  // an immediate `process.exit(1)` truncates the traceback at the pipe
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Segment } from "../core/segment.js";
5
5
  import { Measurement } from "../core/measure.js";
6
- import { isMeasurable, withBoundedWidth } from "../core/protocol.js";
6
+ import { withBoundedWidth } from "../core/protocol.js";
7
7
  import { placeBlock } from "../core/place.js";
8
8
  export class Align {
9
9
  renderable;
@@ -21,13 +21,6 @@ export class Align {
21
21
  }
22
22
  }
23
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 };
24
+ return Measurement.get(options, this.renderable);
32
25
  }
33
26
  }