@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Measurement — min/max cell width calculation for renderables.
3
3
  */
4
- import { measuring } from "./protocol.js";
4
+ import { isMeasurable, measuring } from "./protocol.js";
5
5
  export class Measurement {
6
6
  minimum;
7
7
  maximum;
@@ -40,20 +40,25 @@ export class Measurement {
40
40
  clamp(minWidth, maxWidth) {
41
41
  return new Measurement(Math.min(Math.max(this.minimum, minWidth), maxWidth), Math.min(Math.max(this.maximum, minWidth), maxWidth));
42
42
  }
43
- // [LAW:single-enforcer] Single entry point for measuring a Measurable
44
- static get(options, measurable) {
43
+ // [LAW:single-enforcer] Single entry point for measuring a renderable. One
44
+ // that cannot measure itself asks for the whole offer, as the reference's
45
+ // `Measurement.get` answers for it — so under an unbounded offer it asks for
46
+ // `Infinity`, and `withBoundedWidth` refuses rather than guessing a width.
47
+ static get(options, renderable) {
45
48
  if (options.maxWidth < 1)
46
49
  return new Measurement(0, 0);
47
- const { minimum, maximum } = measurable.measure(measuring(options));
50
+ if (!isMeasurable(renderable))
51
+ return new Measurement(0, options.maxWidth);
52
+ const { minimum, maximum } = renderable.measure(measuring(options));
48
53
  return new Measurement(minimum, Math.min(maximum, options.maxWidth)).normalize();
49
54
  }
50
55
  }
51
- export function measureRenderables(options, measurables) {
52
- if (measurables.length === 0)
56
+ export function measureRenderables(options, renderables) {
57
+ if (renderables.length === 0)
53
58
  return new Measurement(0, 0);
54
59
  let minOfAll = 0;
55
60
  let maxOfAll = 0;
56
- for (const m of measurables) {
61
+ for (const m of renderables) {
57
62
  const measurement = Measurement.get(options, m);
58
63
  minOfAll = Math.max(minOfAll, measurement.minimum);
59
64
  maxOfAll = Math.max(maxOfAll, measurement.maximum);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * memo — a string-keyed memo with a size bound: the one cache shape for a
3
+ * pure computation whose keys a long-running host derives without end. A
4
+ * statusline daemon redrawing a time-varying colour parses a new hex string,
5
+ * matches a new RGB triple, measures a new cell, on every frame; a memo that
6
+ * never evicts grows by one entry per frame for the life of the process.
7
+ */
8
+ export declare const MEMO_MAX = 4096;
9
+ /**
10
+ * `get(key, compute)` returns the value remembered for `key`, computing and
11
+ * remembering it on a miss. Holds at most `MEMO_MAX` entries. `compute` must
12
+ * be a pure function of `key`, so a value recomputed after a clear is the one
13
+ * that was dropped.
14
+ */
15
+ export declare class Memo<V extends NonNullable<unknown>> {
16
+ private readonly entries;
17
+ get size(): number;
18
+ get(key: string, compute: (key: string) => V): V;
19
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * memo — a string-keyed memo with a size bound: the one cache shape for a
3
+ * pure computation whose keys a long-running host derives without end. A
4
+ * statusline daemon redrawing a time-varying colour parses a new hex string,
5
+ * matches a new RGB triple, measures a new cell, on every frame; a memo that
6
+ * never evicts grows by one entry per frame for the life of the process.
7
+ */
8
+ // [LAW:single-enforcer] The one size policy for every memo in the library.
9
+ // Clearing at the cap rather than evicting least-recently-used: a refill costs
10
+ // one recomputation per key, and a clear needs no bookkeeping on the hit path.
11
+ export const MEMO_MAX = 4096;
12
+ /**
13
+ * `get(key, compute)` returns the value remembered for `key`, computing and
14
+ * remembering it on a miss. Holds at most `MEMO_MAX` entries. `compute` must
15
+ * be a pure function of `key`, so a value recomputed after a clear is the one
16
+ * that was dropped.
17
+ */
18
+ export class Memo {
19
+ entries = new Map();
20
+ get size() {
21
+ return this.entries.size;
22
+ }
23
+ get(key, compute) {
24
+ const remembered = this.entries.get(key);
25
+ if (remembered !== undefined)
26
+ return remembered;
27
+ // Computed before the clear, so a compute that throws costs no entries.
28
+ const value = compute(key);
29
+ if (this.entries.size >= MEMO_MAX)
30
+ this.entries.clear();
31
+ this.entries.set(key, value);
32
+ return value;
33
+ }
34
+ }
@@ -2,7 +2,7 @@
2
2
  * The OSC 8 hyperlink wire grammar — which bytes may not appear inside a
3
3
  * link, how a link becomes bytes, and how bytes are read back as a link.
4
4
  *
5
- * [LAW:one-source-of-truth] The producer (`segmentsToString`, the one encoder
5
+ * [LAW:one-source-of-truth] The producer (`segmentToString`, the one encoder
6
6
  * every link reaches the wire through) opens a link with `osc8Open` and closes
7
7
  * it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
8
8
  * test extracting URLs) matches them with `OSC8`; the data-model boundary
@@ -10,16 +10,16 @@
10
10
  * terminator set below, so the bytes a sanitizer removes and the bytes a
11
11
  * reader stops at cannot disagree.
12
12
  *
13
- * [LAW:locality-or-seam] This module depends on nothing; every consumer
13
+ * [LAW:locality-or-seam] This module depends only on `fnv1a`; every consumer
14
14
  * imports downward.
15
15
  *
16
16
  * The `id=` parameter is what makes one link hover as one link. A terminal
17
17
  * treats cells as the same hyperlink when they share BOTH the URI and the id
18
18
  * (the OSC 8 spec, and VTE / iTerm2 / kitty / WezTerm alike); without an id,
19
- * each open sequence is its own link. The coalescer can only share one OSC 8
20
- * pair across a run of identical SGR, so a link whose text changes style
21
- * mid-span — a bold glyph beside plain text, a padded cell — is emitted as
22
- * several pairs, and would highlight piecewise on hover.
19
+ * each open sequence is its own link. The encoder writes one OSC 8 pair per
20
+ * segment, as the reference does, so a link whose text spans several segments
21
+ * — a bold glyph beside plain text, a padded cell — is emitted as several
22
+ * pairs, and would highlight piecewise on hover.
23
23
  *
24
24
  * [LAW:types-are-the-program] The id is a pure function of the URI, so the
25
25
  * byte stream stays a pure function of (style, text, destination): no counter,
package/dist/core/osc8.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The OSC 8 hyperlink wire grammar — which bytes may not appear inside a
3
3
  * link, how a link becomes bytes, and how bytes are read back as a link.
4
4
  *
5
- * [LAW:one-source-of-truth] The producer (`segmentsToString`, the one encoder
5
+ * [LAW:one-source-of-truth] The producer (`segmentToString`, the one encoder
6
6
  * every link reaches the wire through) opens a link with `osc8Open` and closes
7
7
  * it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
8
8
  * test extracting URLs) matches them with `OSC8`; the data-model boundary
@@ -10,16 +10,16 @@
10
10
  * terminator set below, so the bytes a sanitizer removes and the bytes a
11
11
  * reader stops at cannot disagree.
12
12
  *
13
- * [LAW:locality-or-seam] This module depends on nothing; every consumer
13
+ * [LAW:locality-or-seam] This module depends only on `fnv1a`; every consumer
14
14
  * imports downward.
15
15
  *
16
16
  * The `id=` parameter is what makes one link hover as one link. A terminal
17
17
  * treats cells as the same hyperlink when they share BOTH the URI and the id
18
18
  * (the OSC 8 spec, and VTE / iTerm2 / kitty / WezTerm alike); without an id,
19
- * each open sequence is its own link. The coalescer can only share one OSC 8
20
- * pair across a run of identical SGR, so a link whose text changes style
21
- * mid-span — a bold glyph beside plain text, a padded cell — is emitted as
22
- * several pairs, and would highlight piecewise on hover.
19
+ * each open sequence is its own link. The encoder writes one OSC 8 pair per
20
+ * segment, as the reference does, so a link whose text spans several segments
21
+ * — a bold glyph beside plain text, a padded cell — is emitted as several
22
+ * pairs, and would highlight piecewise on hover.
23
23
  *
24
24
  * [LAW:types-are-the-program] The id is a pure function of the URI, so the
25
25
  * byte stream stays a pure function of (style, text, destination): no counter,
@@ -30,6 +30,7 @@
30
30
  * different URIs merges nothing — the URIs still differ — so a 32-bit hash is
31
31
  * exact, not approximate.
32
32
  */
33
+ import { fnv1a } from "./fnv1a.js";
33
34
  /**
34
35
  * The bytes that prematurely terminate an OSC 8 sequence: ESC (`\x1b`, which
35
36
  * begins ST `ESC \`), BEL (`\x07`), and the 8-bit ST (`\x9c`).
@@ -40,20 +41,6 @@ const TERMINATOR_RE = new RegExp(`[${TERMINATORS}]`, "g");
40
41
  export function stripOscTerminators(url) {
41
42
  return url.replace(TERMINATOR_RE, "");
42
43
  }
43
- const utf8 = new TextEncoder();
44
- /**
45
- * FNV-1a, 32-bit, over the URL's UTF-8 bytes, as 8 lowercase hex digits — a
46
- * legal `id=` value (no `:`/`;`). UTF-8 rather than UTF-16 code units, so any
47
- * other runtime computing standard FNV-1a over the same URL gets the same id.
48
- */
49
- function linkId(url) {
50
- let h = 0x811c9dc5;
51
- for (const byte of utf8.encode(url)) {
52
- h ^= byte;
53
- h = Math.imul(h, 0x01000193);
54
- }
55
- return (h >>> 0).toString(16).padStart(8, "0");
56
- }
57
44
  /**
58
45
  * The bytes that open a hyperlink to `url`.
59
46
  *
@@ -64,7 +51,8 @@ function linkId(url) {
64
51
  */
65
52
  export function osc8Open(url) {
66
53
  const clean = stripOscTerminators(url);
67
- return `\x1b]8;id=${linkId(clean)};${clean}\x1b\\`;
54
+ // Hex digits are a legal `id=` value: no `:` or `;`.
55
+ return `\x1b]8;id=${fnv1a(clean)};${clean}\x1b\\`;
68
56
  }
69
57
  /** The bytes that close the current hyperlink. */
70
58
  export const OSC8_CLOSE = "\x1b]8;;\x1b\\";
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Painting frames over one another on a terminal: the bytes that take it,
3
+ * paint each frame over the last one in place, and hand it back — and, on an
4
+ * output that is not a terminal, the one frame that is left when it is handed
5
+ * back.
6
+ *
7
+ * [LAW:one-source-of-truth] Where the cursor rests between frames, and how
8
+ * many rows the next paint goes back over, is one fact, and a `SurfacePainter`
9
+ * is its only owner: `App` holds one, and so does `Live` on a terminal, so
10
+ * they cannot disagree about it. They once did: `Live` wrote a newline after its last row, and on a
11
+ * frame as tall as the terminal that newline scrolled the screen, so every
12
+ * refresh left the frame's top row behind in scrollback. The cursor rests on
13
+ * the last row, and the newline that ends it is written once, when the frame
14
+ * is handed back.
15
+ */
16
+ import type { Destination } from "./color.js";
17
+ import type { Height } from "./protocol.js";
18
+ import { Segment } from "./segment.js";
19
+ /**
20
+ * Where a frame is painted. `alternate` is the whole terminal, in the
21
+ * alternate screen buffer, so the rows printed before it are there again when
22
+ * it is handed back. `inline` starts at the cursor's line, as tall as the
23
+ * frame, and stays on the terminal when it is handed back.
24
+ */
25
+ export type Surface = "alternate" | "inline";
26
+ /** The terminal a frame is painted on, in cells. */
27
+ export interface Screen {
28
+ readonly rows: number;
29
+ readonly cols: number;
30
+ }
31
+ /**
32
+ * A frame not yet drawn: the painter calls it when it is about to put the
33
+ * frame on the output, so one that never shows a frame never pays to draw it.
34
+ */
35
+ export type FrameSource = () => Segment[][];
36
+ /**
37
+ * What paints a run of frames on an output, from `take` to `handBack`.
38
+ */
39
+ export interface Painter {
40
+ /** The budget a frame renders under, on a screen `screenRows` tall. */
41
+ height(screenRows: number): Height;
42
+ /** Take the output. */
43
+ take(): void;
44
+ /**
45
+ * Paint `frame` over the last frame. `frame` is already shaped to its
46
+ * height by whoever set its budget. An empty frame erases the last one.
47
+ */
48
+ paint(frame: FrameSource, screen: Screen, destination: Destination): void;
49
+ /**
50
+ * The bytes that write `text` while `frame` is the frame standing on the
51
+ * output, for the caller to write in one piece.
52
+ */
53
+ around(text: string, frame: FrameSource, screen: Screen, destination: Destination): string;
54
+ /** Hand the output back. The next `take` starts from wherever it then is. */
55
+ handBack(): void;
56
+ }
57
+ /**
58
+ * Paints frames on one surface of a terminal. It holds the last frame's own
59
+ * rows on the terminal — what the next paint goes back over — from `take` to
60
+ * `handBack`.
61
+ */
62
+ export declare class SurfacePainter implements Painter {
63
+ private readonly write;
64
+ private readonly geometry;
65
+ private rows;
66
+ constructor(surface: Surface, write: (bytes: string) => void);
67
+ height(screenRows: number): Height;
68
+ /** Take the terminal: the surface entered, the cursor hidden. */
69
+ take(): void;
70
+ /**
71
+ * Paint `frame` over the last frame, leaving the cursor on its last row,
72
+ * and return it as painted. No row is wider than the screen, since one that
73
+ * soft-wrapped would push every row below it down a row the next paint does
74
+ * not go back over.
75
+ */
76
+ paint(frame: FrameSource, screen: Screen, destination: Destination): Segment[][];
77
+ /**
78
+ * The frame erased, `text` written, and `frame` painted again on the line
79
+ * under it — in one string, so no terminal ever shows the frame gone. The
80
+ * frame fills whole lines, so text that stops mid-line is ended before it.
81
+ */
82
+ around(text: string, frame: FrameSource, screen: Screen, destination: Destination): string;
83
+ private over;
84
+ /**
85
+ * Hand the terminal back: style reset, then the cursor shown on the
86
+ * program's next line, the surface left.
87
+ */
88
+ handBack(): void;
89
+ }
90
+ /**
91
+ * Paints on an output that is not a terminal — a file, a pipe, a CI log — as
92
+ * Rich's `Live` does: nothing while the frames change, since every one would
93
+ * stay there for good with the escape sequences that move over it as literal
94
+ * bytes, and the last frame once, printed as plain lines through `print`, when
95
+ * the output is handed back. What is written around the frame goes out as it
96
+ * was given. No frame is drawn but the one handed back.
97
+ */
98
+ export declare class FinalFramePainter implements Painter {
99
+ private readonly print;
100
+ private last;
101
+ constructor(print: (frame: Segment[][]) => void);
102
+ height(screenRows: number): Height;
103
+ take(): void;
104
+ paint(frame: FrameSource): void;
105
+ around(text: string): string;
106
+ handBack(): void;
107
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Painting frames over one another on a terminal: the bytes that take it,
3
+ * paint each frame over the last one in place, and hand it back — and, on an
4
+ * output that is not a terminal, the one frame that is left when it is handed
5
+ * back.
6
+ *
7
+ * [LAW:one-source-of-truth] Where the cursor rests between frames, and how
8
+ * many rows the next paint goes back over, is one fact, and a `SurfacePainter`
9
+ * is its only owner: `App` holds one, and so does `Live` on a terminal, so
10
+ * they cannot disagree about it. They once did: `Live` wrote a newline after its last row, and on a
11
+ * frame as tall as the terminal that newline scrolled the screen, so every
12
+ * refresh left the frame's top row behind in scrollback. The cursor rests on
13
+ * the last row, and the newline that ends it is written once, when the frame
14
+ * is handed back.
15
+ */
16
+ import { segmentsToString } from "./render.js";
17
+ import { Segment } from "./segment.js";
18
+ const GEOMETRY = {
19
+ alternate: {
20
+ exact: true,
21
+ enter: "\x1b[?1049h",
22
+ // Leaving the buffer puts back the cursor the program had.
23
+ leave: "\x1b[?1049l",
24
+ home: () => "\x1b[H",
25
+ // The frame is every row of the screen, so there is nothing left under it
26
+ // — and after the terminal shrinks, painting the old count would scroll.
27
+ painted: (frameRows) => frameRows,
28
+ below: () => "",
29
+ },
30
+ inline: {
31
+ exact: false,
32
+ enter: "",
33
+ leave: "",
34
+ // After N rows written with N-1 newlines between them, the cursor is on
35
+ // the last. `ESC[0A` still moves a row on some terminals, so a one-row
36
+ // frame has no move at all.
37
+ home: (rows) => (rows > 1 ? `\x1b[${rows - 1}A` : ""),
38
+ // A shorter frame blanks the rows the last one left below it, as far as
39
+ // the screen still reaches after a shrink.
40
+ painted: (frameRows, lastRows, screenRows) => Math.max(frameRows, Math.min(lastRows, screenRows)),
41
+ // Where the program's next line belongs.
42
+ below: (rows) => (rows > 0 ? "\n" : ""),
43
+ },
44
+ };
45
+ // Each row starts at its line's first cell and is erased as it is reached,
46
+ // rather than the frame cleared first, so no blank screen shows between two
47
+ // frames. The erase leads its row: after a row that fills the width, the
48
+ // cursor sits on its last cell, and an erase there would take it.
49
+ const ROW_START = "\r\x1b[2K";
50
+ const HIDE_CURSOR = "\x1b[?25l";
51
+ const SHOW_CURSOR = "\x1b[?25h";
52
+ const RESET_STYLE = "\x1b[0m";
53
+ const fitRows = (frame, screen) => frame.map((line) => Segment.adjustLineLength(line, screen.cols, undefined, false));
54
+ /**
55
+ * Paints frames on one surface of a terminal. It holds the last frame's own
56
+ * rows on the terminal — what the next paint goes back over — from `take` to
57
+ * `handBack`.
58
+ */
59
+ export class SurfacePainter {
60
+ write;
61
+ geometry;
62
+ rows = 0;
63
+ constructor(surface, write) {
64
+ this.write = write;
65
+ this.geometry = GEOMETRY[surface];
66
+ }
67
+ height(screenRows) {
68
+ return { rows: screenRows, exact: this.geometry.exact };
69
+ }
70
+ /** Take the terminal: the surface entered, the cursor hidden. */
71
+ take() {
72
+ this.write(this.geometry.enter + HIDE_CURSOR);
73
+ }
74
+ /**
75
+ * Paint `frame` over the last frame, leaving the cursor on its last row,
76
+ * and return it as painted. No row is wider than the screen, since one that
77
+ * soft-wrapped would push every row below it down a row the next paint does
78
+ * not go back over.
79
+ */
80
+ paint(frame, screen, destination) {
81
+ const rows = fitRows(frame(), screen);
82
+ this.write(this.over(rows, screen, destination));
83
+ return rows;
84
+ }
85
+ /**
86
+ * The frame erased, `text` written, and `frame` painted again on the line
87
+ * under it — in one string, so no terminal ever shows the frame gone. The
88
+ * frame fills whole lines, so text that stops mid-line is ended before it.
89
+ */
90
+ around(text, frame, screen, destination) {
91
+ // Drawn before `over` moves the row count, so a frame that throws leaves
92
+ // the painter matching the terminal it left untouched.
93
+ const rows = fitRows(frame(), screen);
94
+ const erase = this.over([], screen, destination);
95
+ const ended = text.endsWith("\n") ? text : `${text}\n`;
96
+ return erase + ended + this.over(rows, screen, destination);
97
+ }
98
+ // The bytes that paint `rows` over the last frame. They go out in one
99
+ // string, so whatever takes the last frame away arrives with the new one.
100
+ over(rows, screen, destination) {
101
+ const painted = this.geometry.painted(rows.length, this.rows, screen.rows);
102
+ const body = Array.from({ length: painted }, (_, row) => ROW_START + segmentsToString(rows[row] ?? [], destination)).join("\n");
103
+ // Rows blanked below the frame are not the frame's: the cursor goes back
104
+ // up to its last row, so the next frame and the program's next line start
105
+ // from the frame's own height.
106
+ const blanked = painted - Math.max(rows.length, 1);
107
+ const back = blanked > 0 ? `\x1b[${blanked}A` : "";
108
+ const bytes = this.geometry.home(this.rows) + body + back;
109
+ this.rows = rows.length;
110
+ return bytes;
111
+ }
112
+ /**
113
+ * Hand the terminal back: style reset, then the cursor shown on the
114
+ * program's next line, the surface left.
115
+ */
116
+ handBack() {
117
+ this.write(RESET_STYLE + SHOW_CURSOR + this.geometry.below(this.rows) + this.geometry.leave);
118
+ this.rows = 0;
119
+ }
120
+ }
121
+ /**
122
+ * Paints on an output that is not a terminal — a file, a pipe, a CI log — as
123
+ * Rich's `Live` does: nothing while the frames change, since every one would
124
+ * stay there for good with the escape sequences that move over it as literal
125
+ * bytes, and the last frame once, printed as plain lines through `print`, when
126
+ * the output is handed back. What is written around the frame goes out as it
127
+ * was given. No frame is drawn but the one handed back.
128
+ */
129
+ export class FinalFramePainter {
130
+ print;
131
+ last = () => [];
132
+ constructor(print) {
133
+ this.print = print;
134
+ }
135
+ height(screenRows) {
136
+ return { rows: screenRows, exact: false };
137
+ }
138
+ take() { }
139
+ paint(frame) {
140
+ this.last = frame;
141
+ }
142
+ around(text) {
143
+ return text;
144
+ }
145
+ handBack() {
146
+ const frame = this.last;
147
+ this.last = () => [];
148
+ this.print(frame());
149
+ }
150
+ }
@@ -18,7 +18,7 @@
18
18
  * interface. [LAW:one-source-of-truth]
19
19
  */
20
20
  import { Segment } from "./segment.js";
21
- import { type Renderable, type RenderOptions } from "./protocol.js";
21
+ import type { Renderable, RenderOptions } from "./protocol.js";
22
22
  export type Alignment = "left" | "center" | "right";
23
23
  /**
24
24
  * `renderable`'s lines, the block they form placed by `align` in
@@ -19,7 +19,6 @@
19
19
  */
20
20
  import { Segment } from "./segment.js";
21
21
  import { Measurement } from "./measure.js";
22
- import { isMeasurable } from "./protocol.js";
23
22
  // The share of the spare cells that goes on the left; the rest go on the right.
24
23
  // Right-alignment pads no right side at all, which is the reference's choice
25
24
  // and not a zero: a line that ends at the edge carries no trailing blanks.
@@ -36,11 +35,10 @@ const PADS = {
36
35
  * its full width, for whatever holds it to crop or keep.
37
36
  */
38
37
  export function placeBlock(renderable, align, options) {
39
- // A renderable that cannot say how wide it wants to be is offered all of it,
40
- // and the block is then only as wide as what it actually drew.
41
- const width = isMeasurable(renderable)
42
- ? Measurement.get(options, renderable).maximum
43
- : options.maxWidth;
38
+ // A renderable that cannot say how wide it wants to be is offered all of it
39
+ // (`Measurement.get` answers for it), and the block is then only as wide as
40
+ // what it actually drew.
41
+ const width = Measurement.get(options, renderable).maximum;
44
42
  const drawn = Segment.splitLines(renderable.render({ ...options, maxWidth: width, height: undefined }));
45
43
  const [blockWidth, height] = Segment.getShape(drawn);
46
44
  const [left, right] = PADS[align](Math.max(0, options.maxWidth - blockWidth));
@@ -24,6 +24,11 @@ export interface PrettyOptions {
24
24
  indent?: number;
25
25
  expandAll?: boolean;
26
26
  maxLength?: number;
27
+ /**
28
+ * How many characters of a string to show before cutting it, counting the
29
+ * rest after the closing quote. A character is a grapheme cluster, not a
30
+ * UTF-16 code unit: `"😀😀"` is two, though its `length` is four.
31
+ */
27
32
  maxString?: number;
28
33
  /**
29
34
  * How many levels of nesting to descend before eliding. Unbounded by default.
@@ -34,6 +39,11 @@ export interface PrettyOptions {
34
39
  * `Set {...}` — visible, so the reader knows the value continues.
35
40
  */
36
41
  maxDepth?: number;
42
+ /**
43
+ * Draw a `│` in the first cell of each level of indent. Off by default, as
44
+ * the reference's is, and never drawn on an ASCII-only console, where the
45
+ * reference draws none.
46
+ */
37
47
  indentGuides?: boolean;
38
48
  /**
39
49
  * Who colours the formatted text. Defaults to a `ReprHighlighter`.
@@ -43,9 +53,46 @@ export interface PrettyOptions {
43
53
  * `highlighter` have to reach the output the same way they reach a printed
44
54
  * string — a `Pretty` reaching for its own singleton would silently outrank
45
55
  * both. `NullHighlighter` is the "none" case.
56
+ *
57
+ * It reads each `Map` entry's `=>` as `= `: the arrow is this formatter's
58
+ * punctuation, not the data's, and its `>` would close a repr tag opened by
59
+ * any `<` before it.
46
60
  */
47
61
  highlighter?: Highlighter;
48
62
  }
63
+ /**
64
+ * What a value is bounded by when whoever formats it has not seen it:
65
+ * `Console.print`'s data arguments, and a thrown value `Traceback` reports.
66
+ *
67
+ * Such a caller has to assume nothing about the value's size, in any of the
68
+ * three ways a value can be large. Unbounded, a buffer emits a line per byte, a
69
+ * deeply nested object descends until the stack gives out, and a single
70
+ * response body assigned to a field arrives in full — each of them a line that
71
+ * costs megabytes or hangs the terminal. All three bounds announce themselves
72
+ * in the output (`... +N`, `{...}`, `+N` after the closing quote), so this
73
+ * truncates visibly and never silently. [LAW:no-silent-failure]
74
+ *
75
+ * A caller constructing a `Pretty` has seen their data and gets no defaults;
76
+ * these belong to the paths that format what they were handed, not to the
77
+ * formatter.
78
+ */
79
+ export declare const UNSEEN_DATA_BOUNDS: {
80
+ readonly maxLength: 100;
81
+ readonly maxDepth: 16;
82
+ readonly maxString: 1000;
83
+ };
84
+ /**
85
+ * Whether `Pretty` lays a value out as a container — brackets and positions —
86
+ * rather than spelling it as one piece of text. Emptiness and the depth cap do
87
+ * not enter into it; `[]` is still a container that happens to print on one
88
+ * line. This is Python Rich's `is_expandable`, and `Console.print` reads it to
89
+ * give a container lines of its own.
90
+ *
91
+ * A value whose reflection throws is not one, as in the reference's
92
+ * `_safe_isinstance`: `Pretty` then formats it as text, and the throw reaches
93
+ * the output as `threw`'s marker rather than taking the print down.
94
+ */
95
+ export declare function isExpandable(value: unknown): boolean;
49
96
  export declare class Pretty implements Renderable, Measurable {
50
97
  readonly data: unknown;
51
98
  readonly indent: number;
@@ -55,6 +102,7 @@ export declare class Pretty implements Renderable, Measurable {
55
102
  readonly maxDepth: number;
56
103
  readonly indentGuides: boolean;
57
104
  readonly highlighter: Highlighter;
105
+ private readonly cutStrings;
58
106
  constructor(data: unknown, options?: PrettyOptions);
59
107
  render(options: RenderOptions): Iterable<Segment>;
60
108
  /** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
@@ -72,6 +120,21 @@ export declare class Pretty implements Renderable, Measurable {
72
120
  * traversals.
73
121
  */
74
122
  private _scalar;
123
+ /**
124
+ * `value` cut to `max` grapheme clusters, with the rest counted after the
125
+ * closing quote.
126
+ *
127
+ * The unit is the cluster of the value, counted before `JSON.stringify`
128
+ * escapes it. A code unit cut can keep half a surrogate pair, which
129
+ * JSON.stringify prints as a `\ud83d` escape; a code point cut can keep half
130
+ * a flag or a ZWJ family. Either way the kept prefix shows something the
131
+ * value never held, and the dropped count is in a unit nobody can see.
132
+ *
133
+ * Counting clusters reads the whole string, and the traversals ask for the
134
+ * same scalar once per probe at every level above it, so the answer is
135
+ * memoised per value.
136
+ */
137
+ private _cutString;
75
138
  /**
76
139
  * A container, or the text standing in for one that is empty or past the
77
140
  * depth cap.
@@ -100,6 +163,24 @@ export declare class Pretty implements Renderable, Measurable {
100
163
  private _format;
101
164
  /** The arms for a non-null object, with `value` already on the open path. */
102
165
  private _formatObject;
166
+ /**
167
+ * Where a container starts: where it stands, or — when its opener will not
168
+ * fit after what precedes it on its line and will fit on a hanging line —
169
+ * that hanging line, with `lead` the break that reaches it.
170
+ *
171
+ * This is `_place`'s rule for one-piece text, with `open` as the first word:
172
+ * a bracket is a literal no cut can shorten, so moving is the only way it
173
+ * fits. A container moved there is on a line of that indent, its children
174
+ * and closer included, so it reads as its key's value rather than its key's
175
+ * sibling. It moves only to fit: past the edge, the hanging line has no more
176
+ * room than this one.
177
+ */
178
+ private _opening;
179
+ /**
180
+ * `at` on a line that starts at `inset`. The one place a hang is derived: a
181
+ * line's continuation rows and its children both sit one indent past it.
182
+ */
183
+ private _onLine;
103
184
  /**
104
185
  * One position, with every value in it laid out.
105
186
  *
@@ -113,7 +194,8 @@ export declare class Pretty implements Renderable, Measurable {
113
194
  * of the same fact. `_joinOneLine`'s probe sibling tracks the analogous
114
195
  * thing via `budget` shrinking instead, because a probe already discards
115
196
  * anything that overruns and so never needs to know where a multi-line
116
- * insert's last line ends.
197
+ * insert's last line ends. Where that line *starts* is the one fact `out`
198
+ * cannot give back, so it comes from each piece's `Laid.inset` instead.
117
199
  *
118
200
  * Each hole's `reserve` is its own `tail` plus, only for the slot's last
119
201
  * hole, whatever `at.reserve` already asked this whole slot to leave room
@@ -122,6 +204,28 @@ export declare class Pretty implements Renderable, Measurable {
122
204
  * `out` grows past it.
123
205
  */
124
206
  private _expandSlot;
207
+ /**
208
+ * Text that is one piece — a scalar, a value that spells itself, a marker —
209
+ * set down at `at.column` on a line this formatter owns.
210
+ *
211
+ * A container that will not fit expands; one piece of text has no structure
212
+ * to expand into, so it wraps, and the wrap is laid out here rather than left
213
+ * to `RichText`. Left there, it broke at the console's edge and resumed at
214
+ * column 0 — under the enclosing key, or under nothing — which is the
215
+ * structure a reader of nested data relies on, broken. So every line after the
216
+ * first hangs at `at.hang`, a line of the value's own (`Error`'s message, a
217
+ * multi-line `toString`) as much as a wrapped one, and each is cut to the
218
+ * width left there. Every row of the last line also leaves `at.reserve`, as a
219
+ * container's compact try does, for the `,` or `" => "` that follows it.
220
+ *
221
+ * The first line is where the text already stands. When not even its first
222
+ * word fits there, it starts on a hanging line instead — the only case that
223
+ * moves it, and one taken only when that line starts further left and so has
224
+ * more room; otherwise the word is folded where it stands. A Map value after
225
+ * a long key is the case: `[1, 2, 3, 10] =>` ends the line and `"v"` hangs
226
+ * beneath it, where it used to wrap to column 0.
227
+ */
228
+ private _place;
125
229
  /** The one-line form of a value, or `null` when it will not fit `at.budget`. */
126
230
  private _oneLine;
127
231
  /**
@@ -140,5 +244,4 @@ export declare class Pretty implements Renderable, Measurable {
140
244
  private _joinOneLine;
141
245
  /** One position on one line, or `null` when any value in it will not fit. */
142
246
  private _slotOneLine;
143
- private _addIndentGuides;
144
247
  }