@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
@@ -1,42 +1,71 @@
1
1
  /**
2
2
  * Live — animates a portion of the terminal by continuously re-rendering.
3
3
  *
4
- * Two modes:
5
- * - **Inline** (default): clears and redraws N lines in the current scroll
6
- * region. Good for spinners/progress bars below other output.
4
+ * Two modes, each painting every frame over the last in place
5
+ * (`core/paint`):
6
+ * - **Inline** (default): the frame starts at the cursor's line and keeps its
7
+ * own height. Good for spinners/progress bars below other output.
7
8
  * - **Alt-screen** (`altScreen: true`): enters the alternate screen buffer
8
- * on start, cursor-homes on each refresh (no clear flicker), and restores
9
- * the original buffer on stop. The screen is the frame's region, so a
10
- * `Layout` fills it. Good for full-screen TUI apps.
9
+ * on start and restores the original buffer on stop. The screen is the
10
+ * frame's region, so a `Layout` fills it. Good for full-screen TUI apps.
11
+ *
12
+ * On a console that is not interactive — a file, a pipe, a CI log — neither
13
+ * mode paints: what is printed goes out plain, and the frame is written once,
14
+ * when the display stops, as Rich's `Live` does.
15
+ *
16
+ * Live is not built on `App`, though both take a terminal, paint a frame over
17
+ * the last and hand the terminal back. That much is the `Painter`'s, and both
18
+ * hold one. The rest differs in who owns what. An `App` owns its terminal: it
19
+ * starts and stops a `TerminalHost`, sets raw mode, reads keys and pointers,
20
+ * suspends, and builds its own `Console` from the host. A `Live` owns none of
21
+ * that. It draws into a `Console` its caller owns and goes on writing
22
+ * through — a sink that may be a file, whose every other write has to step
23
+ * around the frame (`Console.claimLiveRegion`) — and it reads no input. An
24
+ * `App` over that console would need a host that cannot start, stop, read or
25
+ * suspend, and a second owner of the console's writes.
11
26
  */
27
+ import { frameRate, systemClock } from "../core/clock.js";
12
28
  import { Console } from "../core/console.js";
13
29
  import { Segment } from "../core/segment.js";
14
- import { segmentsToString } from "../core/render.js";
30
+ import { FinalFramePainter, SurfacePainter } from "../core/paint.js";
15
31
  import { fitHeight } from "../core/protocol.js";
16
32
  export class Live {
17
33
  _renderable;
18
34
  _console;
19
- _refreshPerSecond;
35
+ _rate;
36
+ _clock;
20
37
  _autoRefresh;
21
38
  _transient;
22
39
  _verticalOverflow;
23
- _altScreen;
24
- _timer;
25
- _lastLineCount;
26
- _started;
27
- _firstRefresh;
40
+ _painter;
41
+ _stopTicking;
42
+ // [LAW:one-source-of-truth] The console's live region, held from `start()`
43
+ // to the hand-back — which is what "started" means, so there is no separate
44
+ // flag to disagree with it.
45
+ _held;
28
46
  constructor(renderable, options) {
29
47
  this._renderable = renderable;
30
- this._console = options?.console ?? new Console({ forceTerminal: true });
31
- this._refreshPerSecond = options?.refreshPerSecond ?? 4;
48
+ this._console = options?.console ?? new Console();
49
+ // [LAW:parse-dont-validate] Parsed whether or not it auto-refreshes, so a
50
+ // rate that could never tick is refused where it is given.
51
+ this._rate = frameRate(options?.refreshPerSecond ?? 4);
52
+ this._clock = options?.clock ?? systemClock();
32
53
  this._autoRefresh = options?.autoRefresh !== false;
33
54
  this._transient = options?.transient ?? false;
34
55
  this._verticalOverflow = options?.verticalOverflow ?? "ellipsis";
35
- this._altScreen = options?.altScreen ?? false;
36
- this._lastLineCount = 0;
37
- this._started = false;
38
- this._firstRefresh = true;
56
+ // [LAW:dataflow-not-control-flow] What the console is decides which
57
+ // painter Live holds; every frame then takes the same path through it.
58
+ // The final frame is a print, as in Rich, so a recording or a capture
59
+ // holds it with everything else the console wrote.
60
+ this._painter = this._console.isInteractive
61
+ ? new SurfacePainter(options?.altScreen ? "alternate" : "inline", (bytes) => void this._console.file.write(bytes))
62
+ : new FinalFramePainter((frame) => this._console.print(linesOf(frame)));
63
+ this._held = null;
39
64
  }
65
+ /**
66
+ * The console to print through while this Live runs: what it prints lands
67
+ * above an inline frame. The alternate screen is all frame, and paints over it.
68
+ */
40
69
  get console() {
41
70
  return this._console;
42
71
  }
@@ -44,48 +73,45 @@ export class Live {
44
73
  return this._renderable;
45
74
  }
46
75
  start() {
47
- if (this._started)
76
+ if (this._held !== null)
48
77
  return;
49
- this._started = true;
50
- this._firstRefresh = true;
51
- const stream = this._console.file;
52
- if (this._altScreen) {
53
- stream.write("\x1b[?1049h"); // enter alt screen
54
- }
55
- this._writeCursorControl(false);
78
+ // Claimed before a byte is written, so a console another Live is running
79
+ // on refuses this one with the terminal untouched.
80
+ const held = this._console.claimLiveRegion({ around: (text) => this._stepAround(text) });
81
+ this._held = held;
82
+ this._painter.take();
56
83
  if (this._autoRefresh) {
57
- const interval = Math.floor(1000 / this._refreshPerSecond);
58
84
  // No caller is on the stack to run `stop()` for a frame that throws
59
85
  // here, and in Node the throw ends the process — so the terminal is
60
86
  // handed back first, and the error then goes on to wherever an uncaught
61
87
  // one goes.
62
- this._timer = setInterval(() => {
88
+ this._stopTicking = this._clock.every(this._rate, () => {
63
89
  try {
64
90
  this.refresh();
65
91
  }
66
92
  catch (error) {
67
- this._release();
93
+ this._release(held);
68
94
  throw error;
69
95
  }
70
- }, interval);
96
+ });
71
97
  }
72
98
  }
73
99
  stop() {
74
- if (!this._started)
100
+ const held = this._held;
101
+ if (held === null)
75
102
  return;
76
103
  // [LAW:no-silent-failure] A final frame that throws still propagates, and
77
104
  // the terminal is handed back on the way out.
78
105
  try {
79
106
  if (this._transient) {
80
- this._console.file.write(this._erase());
81
- this._lastLineCount = 0;
107
+ this._paint(() => []);
82
108
  }
83
109
  else {
84
110
  this.refresh();
85
111
  }
86
112
  }
87
113
  finally {
88
- this._release();
114
+ this._release(held);
89
115
  }
90
116
  }
91
117
  update(renderable, options) {
@@ -100,47 +126,39 @@ export class Live {
100
126
  // A frame is drawn only while this Live holds the terminal, as in Rich:
101
127
  // before `start()` or after the hand-back it would land on a screen that
102
128
  // belongs to someone else — the main buffer, under the user's own output.
103
- if (!this._started || !this._renderable)
129
+ if (this._held === null)
104
130
  return;
131
+ this._paint(() => this._frame());
132
+ }
133
+ _frame() {
134
+ if (!this._renderable)
135
+ return [];
105
136
  // [LAW:dataflow-not-control-flow] Both modes render under the terminal's
106
137
  // rows and differ only in what those rows are: the alternate screen is a
107
138
  // region the frame stands in, and an inline frame keeps its natural height
108
139
  // under them as a ceiling. Live set the budget, so Live shapes what comes
109
140
  // back — its overflow policy, then `fitHeight`, which pads only a region.
110
141
  const options = this._console.options;
111
- const height = { rows: options.height.rows, exact: this._altScreen };
142
+ const height = this._painter.height(options.height.rows);
112
143
  const lines = Segment.splitLines(this._renderable.render({ ...options, height }));
113
- const displayLines = fitHeight(this._overflow(lines, height.rows), height);
114
- // [LAW:single-enforcer] Per-line encoding routes through the same
115
- // tree-coalescer `Console._writeSegments` uses, so Live frames coalesce
116
- // adjacent same-style segments into shared SGR pairs on the wire and
117
- // encode for the console's own destination.
118
- // The alternate screen is not erased between frames — the cursor only goes
119
- // home — so each of its rows is erased before it is drawn, or a shorter
120
- // frame leaves the last one's rows and line tails showing. The erase comes
121
- // first because the cursor is then at the row's start; after a row that
122
- // fills the width it would take the last cell. An inline frame's rows are
123
- // erased by `_erase`.
124
- const lead = this._altScreen ? "\x1b[2K" : "";
125
- const destination = this._console.destination;
126
- const output = displayLines
127
- .map((line) => lead + segmentsToString(line, destination))
128
- .join("\n");
129
- // [LAW:effects-at-boundaries] The frame is fully rendered before a byte
130
- // reaches the terminal, and whatever takes the last frame away goes out in
131
- // the same write as the new one — so a render that throws leaves the last
132
- // good frame showing. The first alternate-screen frame clears the buffer
133
- // it walked into; after that the cursor only goes home.
134
- const replace = this._altScreen ? (this._firstRefresh ? "\x1b[2J\x1b[H" : "\x1b[H") : this._erase();
135
- // Inline, the newline leaves the cursor under the frame, where `_erase`
136
- // counts up from. On the alternate screen the next frame starts from home,
137
- // and a newline after a full-height frame's last row scrolls its first row
138
- // off the top.
139
- this._console.file.write(replace + (this._altScreen ? output : output + "\n"));
140
- this._firstRefresh = false;
141
- // What `_erase` erases: an inline frame's rows. An alternate-screen frame
142
- // is erased by leaving the buffer.
143
- this._lastLineCount = this._altScreen ? 0 : displayLines.length;
144
+ return fitHeight(this._overflow(lines, height.rows), height);
145
+ }
146
+ // What the console writes goes where the frame was, and the frame is painted
147
+ // again under it, as Rich's render hook does — in the bytes the console
148
+ // writes, so a render that throws writes nothing.
149
+ _stepAround(text) {
150
+ return this._painter.around(text, () => this._frame(), this._screen(), this._console.destination);
151
+ }
152
+ // [LAW:effects-at-boundaries] The frame is fully rendered before a byte
153
+ // reaches the terminal, and whatever takes the last frame away goes out in
154
+ // the same write as the new one — so a render that throws leaves the last
155
+ // good frame showing.
156
+ _paint(frame) {
157
+ this._painter.paint(frame, this._screen(), this._console.destination);
158
+ }
159
+ _screen() {
160
+ const { height, maxWidth } = this._console.options;
161
+ return { rows: height.rows, cols: maxWidth };
144
162
  }
145
163
  // Lines past `rows`: dropped, with the last kept row replaced by an ellipsis,
146
164
  // or left for the terminal to scroll.
@@ -153,23 +171,18 @@ export class Live {
153
171
  }
154
172
  return kept;
155
173
  }
156
- // Cursor-up and erase-line for each row of the last inline frame.
157
- _erase() {
158
- return "\x1b[1A\x1b[2K".repeat(this._lastLineCount);
159
- }
160
174
  // [LAW:single-enforcer] The one way the terminal is handed back — the timer
161
175
  // stopped, the cursor shown, the alternate screen left — whether `stop()`
162
176
  // ran or a frame threw with no caller to run it.
163
- _release() {
164
- this._started = false;
165
- clearInterval(this._timer);
166
- this._timer = undefined;
167
- this._writeCursorControl(true);
168
- if (this._altScreen) {
169
- this._console.file.write("\x1b[0m\x1b[?1049l"); // reset attrs + exit alt screen
170
- }
171
- }
172
- _writeCursorControl(show) {
173
- this._console.file.write(show ? "\x1b[?25h" : "\x1b[?25l");
177
+ _release(held) {
178
+ held();
179
+ this._held = null;
180
+ this._stopTicking?.();
181
+ this._stopTicking = undefined;
182
+ this._painter.handBack();
174
183
  }
175
184
  }
185
+ // A drawn frame as a renderable, each row a line, for the console to print.
186
+ function linesOf(frame) {
187
+ return { render: () => frame.flatMap((row) => [...row, Segment.line()]) };
188
+ }
@@ -1,20 +1,33 @@
1
1
  /**
2
2
  * Markdown — renders Markdown content to the terminal.
3
3
  * Uses built-in parsing (no external dependency).
4
+ *
5
+ * Block structure is read as CommonMark reads it: a blockquote and a list
6
+ * item are both containers, their lines tokenized as Markdown of their own
7
+ * once the container's marker or indentation is cut off, and drawn beside a
8
+ * gutter — the quote's bar, the item's marker then its hang. Where CommonMark
9
+ * leaves the drawing open, Rich's is followed.
4
10
  */
5
11
  import { Segment } from "../core/segment.js";
6
12
  import { Style } from "../core/style.js";
7
13
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
14
  export interface MarkdownOptions {
9
- codeTheme?: string;
15
+ /** The style of `inline code`: a theme name or a style definition. Default `markdown.code`. */
10
16
  inlineCodeStyle?: string | Style;
17
+ /**
18
+ * Whether a link's text is the link. `false` writes the URL after the text
19
+ * in parentheses, for a reader who cannot click it. Default `true`.
20
+ */
11
21
  hyperlinks?: boolean;
12
- justify?: "left" | "center" | "right" | "full";
22
+ /** How body text — paragraphs, list items, quotes — is placed in its width. Default `"left"`; headings keep their own. */
23
+ justify?: Justify;
13
24
  }
25
+ type Justify = NonNullable<RenderOptions["justify"]>;
14
26
  export declare class Markdown implements Renderable, Measurable {
15
27
  readonly markdown: string;
16
28
  readonly inlineCodeStyle: string | Style;
17
29
  readonly hyperlinks: boolean;
30
+ readonly justify: Justify;
18
31
  constructor(markdown: string, options?: MarkdownOptions);
19
32
  render(rawOptions: RenderOptions): Iterable<Segment>;
20
33
  measure(options: RenderOptions): {
@@ -22,3 +35,4 @@ export declare class Markdown implements Renderable, Measurable {
22
35
  maximum: number;
23
36
  };
24
37
  }
38
+ export {};