@promptctl/rich-js 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/dist/core/anchor.d.ts +45 -0
  2. package/dist/core/anchor.js +39 -0
  3. package/dist/core/ansi.d.ts +65 -0
  4. package/dist/core/ansi.js +231 -0
  5. package/dist/core/box.d.ts +143 -0
  6. package/dist/core/box.js +379 -0
  7. package/dist/core/cells.d.ts +159 -0
  8. package/dist/core/cells.js +287 -0
  9. package/dist/core/color.d.ts +246 -0
  10. package/dist/core/color.js +1038 -0
  11. package/dist/core/console.d.ts +165 -0
  12. package/dist/core/console.js +527 -0
  13. package/dist/core/emoji.d.ts +24 -0
  14. package/dist/core/emoji.js +391 -0
  15. package/dist/core/export-html.d.ts +59 -0
  16. package/dist/core/export-html.js +200 -0
  17. package/dist/core/export-lines.d.ts +121 -0
  18. package/dist/core/export-lines.js +141 -0
  19. package/dist/core/highlighter.d.ts +33 -0
  20. package/dist/core/highlighter.js +124 -0
  21. package/dist/core/json.d.ts +27 -0
  22. package/dist/core/json.js +53 -0
  23. package/dist/core/markup.d.ts +119 -0
  24. package/dist/core/markup.js +731 -0
  25. package/dist/core/measure.d.ts +27 -0
  26. package/dist/core/measure.js +62 -0
  27. package/dist/core/oklch.d.ts +161 -0
  28. package/dist/core/oklch.js +312 -0
  29. package/dist/core/osc8.d.ts +68 -0
  30. package/dist/core/osc8.js +90 -0
  31. package/dist/core/place.d.ts +30 -0
  32. package/dist/core/place.js +51 -0
  33. package/dist/core/pretty.d.ts +144 -0
  34. package/dist/core/pretty.js +476 -0
  35. package/dist/core/protocol.d.ts +267 -0
  36. package/dist/core/protocol.js +163 -0
  37. package/dist/core/render.d.ts +84 -0
  38. package/dist/core/render.js +145 -0
  39. package/dist/core/segment.d.ts +161 -0
  40. package/dist/core/segment.js +492 -0
  41. package/dist/core/spinnerData.d.ts +12 -0
  42. package/dist/core/spinnerData.js +117 -0
  43. package/dist/core/strip.d.ts +141 -0
  44. package/dist/core/strip.js +305 -0
  45. package/dist/core/style.d.ts +169 -0
  46. package/dist/core/style.js +695 -0
  47. package/dist/core/subscription.d.ts +12 -0
  48. package/dist/core/subscription.js +12 -0
  49. package/dist/core/text.d.ts +256 -0
  50. package/dist/core/text.js +976 -0
  51. package/dist/core/wrap.d.ts +41 -0
  52. package/dist/core/wrap.js +127 -0
  53. package/dist/host/app.d.ts +121 -0
  54. package/dist/host/app.js +255 -0
  55. package/dist/host/host-environment.d.ts +26 -0
  56. package/dist/host/host-environment.js +44 -0
  57. package/dist/host/index.d.ts +5 -0
  58. package/dist/host/index.js +3 -0
  59. package/dist/host/terminal-host.d.ts +179 -0
  60. package/dist/host/terminal-host.js +186 -0
  61. package/dist/index.d.ts +91 -0
  62. package/dist/index.js +107 -0
  63. package/dist/node/prompt.d.ts +21 -0
  64. package/dist/node/prompt.js +30 -0
  65. package/dist/node/save.d.ts +17 -0
  66. package/dist/node/save.js +21 -0
  67. package/dist/node/terminal-host.d.ts +113 -0
  68. package/dist/node/terminal-host.js +227 -0
  69. package/dist/node/traceback.d.ts +37 -0
  70. package/dist/node/traceback.js +134 -0
  71. package/dist/renderables/align.d.ts +16 -0
  72. package/dist/renderables/align.js +33 -0
  73. package/dist/renderables/columns.d.ts +61 -0
  74. package/dist/renderables/columns.js +152 -0
  75. package/dist/renderables/constrain.d.ts +15 -0
  76. package/dist/renderables/constrain.js +37 -0
  77. package/dist/renderables/embed.d.ts +50 -0
  78. package/dist/renderables/embed.js +71 -0
  79. package/dist/renderables/flexStrip.d.ts +41 -0
  80. package/dist/renderables/flexStrip.js +200 -0
  81. package/dist/renderables/group.d.ts +11 -0
  82. package/dist/renderables/group.js +17 -0
  83. package/dist/renderables/layout.d.ts +92 -0
  84. package/dist/renderables/layout.js +312 -0
  85. package/dist/renderables/live.d.ts +50 -0
  86. package/dist/renderables/live.js +175 -0
  87. package/dist/renderables/markdown.d.ts +24 -0
  88. package/dist/renderables/markdown.js +184 -0
  89. package/dist/renderables/padding.d.ts +38 -0
  90. package/dist/renderables/padding.js +141 -0
  91. package/dist/renderables/panel.d.ts +116 -0
  92. package/dist/renderables/panel.js +307 -0
  93. package/dist/renderables/progress.d.ts +90 -0
  94. package/dist/renderables/progress.js +238 -0
  95. package/dist/renderables/progressBar.d.ts +31 -0
  96. package/dist/renderables/progressBar.js +53 -0
  97. package/dist/renderables/prompt.d.ts +49 -0
  98. package/dist/renderables/prompt.js +125 -0
  99. package/dist/renderables/rule.d.ts +25 -0
  100. package/dist/renderables/rule.js +90 -0
  101. package/dist/renderables/spinner.d.ts +31 -0
  102. package/dist/renderables/spinner.js +70 -0
  103. package/dist/renderables/status.d.ts +23 -0
  104. package/dist/renderables/status.js +60 -0
  105. package/dist/renderables/syntax.d.ts +35 -0
  106. package/dist/renderables/syntax.js +115 -0
  107. package/dist/renderables/table.d.ts +194 -0
  108. package/dist/renderables/table.js +795 -0
  109. package/dist/renderables/traceback.d.ts +31 -0
  110. package/dist/renderables/traceback.js +108 -0
  111. package/dist/renderables/tree.d.ts +68 -0
  112. package/dist/renderables/tree.js +150 -0
  113. package/dist/renderables/viewport.d.ts +114 -0
  114. package/dist/renderables/viewport.js +198 -0
  115. package/dist/template-bindings/color-funcs.d.ts +87 -0
  116. package/dist/template-bindings/color-funcs.js +217 -0
  117. package/dist/template-bindings/helpers.d.ts +24 -0
  118. package/dist/template-bindings/helpers.js +48 -0
  119. package/dist/template-bindings/index.d.ts +107 -0
  120. package/dist/template-bindings/index.js +146 -0
  121. package/dist/template-bindings/palette-funcs.d.ts +94 -0
  122. package/dist/template-bindings/palette-funcs.js +141 -0
  123. package/dist/template-bindings/style-funcs.d.ts +28 -0
  124. package/dist/template-bindings/style-funcs.js +155 -0
  125. package/dist/themes/buildPalette.d.ts +36 -0
  126. package/dist/themes/buildPalette.js +56 -0
  127. package/dist/themes/colorMath.d.ts +82 -0
  128. package/dist/themes/colorMath.js +254 -0
  129. package/dist/themes/colorRef.d.ts +51 -0
  130. package/dist/themes/colorRef.js +96 -0
  131. package/dist/themes/data/ansi-tables.d.ts +14 -0
  132. package/dist/themes/data/ansi-tables.js +49 -0
  133. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  134. package/dist/themes/data/atom-one-dark.js +177 -0
  135. package/dist/themes/data/atom-one-light.d.ts +3 -0
  136. package/dist/themes/data/atom-one-light.js +177 -0
  137. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  138. package/dist/themes/data/catppuccin-frappe.js +177 -0
  139. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  140. package/dist/themes/data/catppuccin-latte.js +177 -0
  141. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  142. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  143. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  144. package/dist/themes/data/catppuccin-mocha.js +177 -0
  145. package/dist/themes/data/cyberpunk.d.ts +17 -0
  146. package/dist/themes/data/cyberpunk.js +193 -0
  147. package/dist/themes/data/default.d.ts +3 -0
  148. package/dist/themes/data/default.js +160 -0
  149. package/dist/themes/data/dracula.d.ts +3 -0
  150. package/dist/themes/data/dracula.js +177 -0
  151. package/dist/themes/data/flexoki.d.ts +3 -0
  152. package/dist/themes/data/flexoki.js +177 -0
  153. package/dist/themes/data/gruvbox.d.ts +3 -0
  154. package/dist/themes/data/gruvbox.js +177 -0
  155. package/dist/themes/data/index.d.ts +35 -0
  156. package/dist/themes/data/index.js +64 -0
  157. package/dist/themes/data/monokai.d.ts +3 -0
  158. package/dist/themes/data/monokai.js +177 -0
  159. package/dist/themes/data/nord.d.ts +3 -0
  160. package/dist/themes/data/nord.js +177 -0
  161. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  162. package/dist/themes/data/rose-pine-dawn.js +177 -0
  163. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  164. package/dist/themes/data/rose-pine-moon.js +177 -0
  165. package/dist/themes/data/rose-pine.d.ts +3 -0
  166. package/dist/themes/data/rose-pine.js +177 -0
  167. package/dist/themes/data/solarized-dark.d.ts +3 -0
  168. package/dist/themes/data/solarized-dark.js +177 -0
  169. package/dist/themes/data/solarized-light.d.ts +3 -0
  170. package/dist/themes/data/solarized-light.js +177 -0
  171. package/dist/themes/data/svg-export.d.ts +3 -0
  172. package/dist/themes/data/svg-export.js +177 -0
  173. package/dist/themes/data/textual-ansi.d.ts +3 -0
  174. package/dist/themes/data/textual-ansi.js +160 -0
  175. package/dist/themes/data/textual-dark.d.ts +3 -0
  176. package/dist/themes/data/textual-dark.js +160 -0
  177. package/dist/themes/data/textual-light.d.ts +3 -0
  178. package/dist/themes/data/textual-light.js +160 -0
  179. package/dist/themes/data/tokyo-night.d.ts +3 -0
  180. package/dist/themes/data/tokyo-night.js +177 -0
  181. package/dist/themes/data/types.d.ts +31 -0
  182. package/dist/themes/data/types.js +10 -0
  183. package/dist/themes/palette.d.ts +21 -0
  184. package/dist/themes/palette.js +28 -0
  185. package/dist/themes/ramp.d.ts +83 -0
  186. package/dist/themes/ramp.js +122 -0
  187. package/dist/themes/registry.d.ts +44 -0
  188. package/dist/themes/registry.js +94 -0
  189. package/dist/themes/terminalThemes.d.ts +49 -0
  190. package/dist/themes/terminalThemes.js +63 -0
  191. package/dist/themes/transpose.d.ts +87 -0
  192. package/dist/themes/transpose.js +138 -0
  193. package/dist/widgets/button.d.ts +49 -0
  194. package/dist/widgets/button.js +208 -0
  195. package/dist/widgets/checkbox.d.ts +41 -0
  196. package/dist/widgets/checkbox.js +157 -0
  197. package/dist/widgets/dropdown.d.ts +67 -0
  198. package/dist/widgets/dropdown.js +450 -0
  199. package/dist/widgets/event-router.d.ts +71 -0
  200. package/dist/widgets/event-router.js +629 -0
  201. package/dist/widgets/focus-manager.d.ts +45 -0
  202. package/dist/widgets/focus-manager.js +191 -0
  203. package/dist/widgets/hit.d.ts +64 -0
  204. package/dist/widgets/hit.js +113 -0
  205. package/dist/widgets/index.d.ts +20 -0
  206. package/dist/widgets/index.js +14 -0
  207. package/dist/widgets/slider.d.ts +57 -0
  208. package/dist/widgets/slider.js +270 -0
  209. package/dist/widgets/static-item.d.ts +34 -0
  210. package/dist/widgets/static-item.js +54 -0
  211. package/dist/widgets/text-input.d.ts +274 -0
  212. package/dist/widgets/text-input.js +1165 -0
  213. package/dist/widgets/toggle.d.ts +44 -0
  214. package/dist/widgets/toggle.js +180 -0
  215. package/dist/widgets/types.d.ts +87 -0
  216. package/dist/widgets/types.js +30 -0
  217. package/dist/widgets/widget-app.d.ts +63 -0
  218. package/dist/widgets/widget-app.js +199 -0
  219. package/dist/widgets/widget-base.d.ts +40 -0
  220. package/dist/widgets/widget-base.js +165 -0
  221. package/package.json +6 -2
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Traceback — renders error tracebacks with formatting.
3
+ *
4
+ * [LAW:effects-at-boundaries] Pure rendering: an `Error` in, `Segment`s out.
5
+ * Installing this as the process-wide crash handler touches `process.on` and
6
+ * `process.exit`, so that lives behind the node seam as `installTraceback` in
7
+ * `src/node/traceback.ts` — which is what keeps this module, and therefore the
8
+ * main barrel, importable in a browser.
9
+ *
10
+ * [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
11
+ * reads. There is no `showLocals`: an `Error` carries a stack of locations and
12
+ * nothing of the values in scope at them. The one way to get those in node —
13
+ * an inspector session pausing on exceptions — would have to pause on every
14
+ * throw, caught ones included, to serve `new Traceback(caughtError)`, and it
15
+ * cannot exist in a browser at all. Nor is there a `width` or `theme`: those
16
+ * sized and coloured a source-code excerpt this renderer does not produce.
17
+ */
18
+ import { Segment } from "../core/segment.js";
19
+ import type { Renderable, RenderOptions } from "../core/protocol.js";
20
+ export interface TracebackOptions {
21
+ suppress?: string[];
22
+ maxFrames?: number;
23
+ }
24
+ export declare class Traceback implements Renderable {
25
+ readonly error: Error;
26
+ readonly maxFrames: number;
27
+ readonly suppress: string[];
28
+ constructor(error: Error, options?: TracebackOptions);
29
+ render(options: RenderOptions): Iterable<Segment>;
30
+ private _renderFrame;
31
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Traceback — renders error tracebacks with formatting.
3
+ *
4
+ * [LAW:effects-at-boundaries] Pure rendering: an `Error` in, `Segment`s out.
5
+ * Installing this as the process-wide crash handler touches `process.on` and
6
+ * `process.exit`, so that lives behind the node seam as `installTraceback` in
7
+ * `src/node/traceback.ts` — which is what keeps this module, and therefore the
8
+ * main barrel, importable in a browser.
9
+ *
10
+ * [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
11
+ * reads. There is no `showLocals`: an `Error` carries a stack of locations and
12
+ * nothing of the values in scope at them. The one way to get those in node —
13
+ * an inspector session pausing on exceptions — would have to pause on every
14
+ * throw, caught ones included, to serve `new Traceback(caughtError)`, and it
15
+ * cannot exist in a browser at all. Nor is there a `width` or `theme`: those
16
+ * sized and coloured a source-code excerpt this renderer does not produce.
17
+ */
18
+ import { Segment } from "../core/segment.js";
19
+ import { Style } from "../core/style.js";
20
+ import { getStyle } from "../core/protocol.js";
21
+ function parseStack(error) {
22
+ const stack = error.stack ?? "";
23
+ const lines = stack.split("\n");
24
+ const frames = [];
25
+ for (const line of lines) {
26
+ // The two V8 frame shapes located by file:line:column: "at fn (file:…)"
27
+ // and "at file:…", either one prefixed "async " when the frame is an
28
+ // awaited call. The marker is not part of any name, so it is read and
29
+ // dropped; the lookahead keeps "at async (file:…)" — a function named
30
+ // `async` — a name.
31
+ const match = /^at\s+(?:async\s+(?!\())?(?:(.+?)\s+\()?(.+?):(\d+):\d+\)?/.exec(line.trim());
32
+ if (match) {
33
+ frames.push({
34
+ function: match[1] || undefined,
35
+ file: match[2],
36
+ line: parseInt(match[3], 10),
37
+ });
38
+ }
39
+ }
40
+ return frames;
41
+ }
42
+ export class Traceback {
43
+ error;
44
+ maxFrames;
45
+ suppress;
46
+ constructor(error, options) {
47
+ this.error = error;
48
+ this.maxFrames = options?.maxFrames ?? 100;
49
+ this.suppress = options?.suppress ?? [];
50
+ }
51
+ *render(options) {
52
+ const excTypeStyle = getStyle(options, "traceback.exc_type");
53
+ const textStyle = getStyle(options, "traceback.text");
54
+ // Error type and message
55
+ const errorName = this.error.name || "Error";
56
+ const errorMessage = this.error.message || "";
57
+ yield new Segment(errorName, excTypeStyle);
58
+ yield new Segment(": ");
59
+ yield new Segment(errorMessage, textStyle);
60
+ yield Segment.line();
61
+ yield Segment.line();
62
+ // Stack frames — suppressed frames show file/line only (not removed)
63
+ const frames = parseStack(this.error);
64
+ const filteredFrames = this.suppress.length > 0
65
+ ? frames.map((f) => ({
66
+ ...f,
67
+ suppressed: this.suppress.some((s) => f.file.includes(s)),
68
+ }))
69
+ : frames;
70
+ let displayFrames = filteredFrames;
71
+ if (this.maxFrames > 0 && displayFrames.length > this.maxFrames) {
72
+ // Spend the budget exactly: the tail takes `maxFrames - head` rather than
73
+ // a second `head`, so an odd budget shows every frame it counts, and it
74
+ // slices from an index because `slice(-0)` is the whole array.
75
+ const head = Math.floor(this.maxFrames / 2);
76
+ const first = displayFrames.slice(0, head);
77
+ const last = displayFrames.slice(displayFrames.length - (this.maxFrames - head));
78
+ const omitted = displayFrames.length - this.maxFrames;
79
+ for (const frame of first) {
80
+ yield* this._renderFrame(frame, options);
81
+ }
82
+ yield new Segment(` ... ${omitted} frames omitted ...`, textStyle);
83
+ yield Segment.line();
84
+ for (const frame of last) {
85
+ yield* this._renderFrame(frame, options);
86
+ }
87
+ }
88
+ else {
89
+ for (const frame of displayFrames) {
90
+ yield* this._renderFrame(frame, options);
91
+ }
92
+ }
93
+ }
94
+ *_renderFrame(frame, options) {
95
+ const pathStyle = Style.parse("dim");
96
+ const lineNoStyle = getStyle(options, "traceback.offset");
97
+ yield new Segment(" ");
98
+ // Spec: suppressed frames show file and line only — no function name
99
+ if (frame.function && !frame.suppressed) {
100
+ yield new Segment(frame.function, Style.parse("bold"));
101
+ yield new Segment(" ");
102
+ }
103
+ yield new Segment(frame.file, pathStyle);
104
+ yield new Segment(":");
105
+ yield new Segment(String(frame.line), lineNoStyle);
106
+ yield Segment.line();
107
+ }
108
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tree — hierarchical view with guide lines.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Style } from "../core/style.js";
6
+ import { RichText } from "../core/text.js";
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
+ export interface TreeOptions {
9
+ expanded?: boolean;
10
+ hideRoot?: boolean;
11
+ guide_style?: string | Style;
12
+ style?: string | Style;
13
+ }
14
+ export declare class Tree implements Renderable, Measurable {
15
+ readonly label: Renderable;
16
+ readonly children: Tree[];
17
+ expanded: boolean;
18
+ readonly hideRoot: boolean;
19
+ readonly guideStyle: string | Style;
20
+ readonly style: string | Style;
21
+ constructor(label: string | RichText | Renderable, options?: TreeOptions);
22
+ add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
23
+ render(rawOptions: RenderOptions): Iterable<Segment>;
24
+ /**
25
+ * The rows `render` and `measure` both read, entered in exactly one place so
26
+ * the two cannot start the walk differently. A hidden root takes its own row
27
+ * and the column its children hang from with it, as Rich does — its children
28
+ * stand at the left edge, and its guide style still reaches their guides.
29
+ */
30
+ private _walk;
31
+ /**
32
+ * The tree walked into the rows it emits, one row per label, guides first.
33
+ *
34
+ * [LAW:one-source-of-truth] The walk that decides which rows exist and what
35
+ * leads them is here and only here. `render` turns each row into segments and
36
+ * `measure` reads each row's width off the same sequence, so the two cannot
37
+ * come to disagree about how many rows there are or how wide the guides on
38
+ * them run — which they would the moment a second walk existed.
39
+ *
40
+ * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
41
+ * this one recursion: its children get the columns above it, settled, plus a
42
+ * branch of their own, so no depth is drawn by a rule of its own.
43
+ */
44
+ private _rows;
45
+ /**
46
+ * One row of the tree: its label in whatever width the guides left, each of
47
+ * the label's lines led by the guides.
48
+ *
49
+ * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
50
+ * Before this, each call site emitted its guides and then handed the label
51
+ * `options` unchanged — the label was told it had the whole outer width while
52
+ * the guides had already spent four cells of it, so a tree at maxWidth 5
53
+ * emitted rows of 9. That is the same defect Panel and Table were fixed for,
54
+ * and the same fix: divide the requested width once and let every part read
55
+ * its share off the division. `options` arrives parsed from `render`.
56
+ *
57
+ * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
58
+ * depending on label renderables to invent trailing newlines: the label's
59
+ * output is cut into lines and every line ends where Tree ends it. A label
60
+ * that emits nothing still holds its row, and one that ends in a newline of
61
+ * its own does not add a blank one.
62
+ */
63
+ private _renderRow;
64
+ measure(options: RenderOptions): {
65
+ minimum: number;
66
+ maximum: number;
67
+ };
68
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tree — hierarchical view with guide lines.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { cellFit, cellLen, asCellCol } from "../core/cells.js";
6
+ import { Style, NULL_STYLE } from "../core/style.js";
7
+ import { embed } from "./embed.js";
8
+ import { drawable, getStyle, isMeasurable, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
+ import { Measurement } from "../core/measure.js";
10
+ const UNICODE_GUIDES = {
11
+ fork: { first: "├── ", rest: "│ " },
12
+ end: { first: "└── ", rest: " " },
13
+ };
14
+ const ASCII_GUIDES = {
15
+ fork: { first: "+-- ", rest: "| " },
16
+ end: { first: "`-- ", rest: " " },
17
+ };
18
+ const guideGlyphs = ({ fork, end }) => fork.first + fork.rest + end.first + end.rest;
19
+ /**
20
+ * How wide a label wants to be. A label with no `measure` cannot say, and the
21
+ * offer is the honest stand-in — including when the offer is unbounded, which
22
+ * is where `withBoundedWidth` reports it rather than guessing a number.
23
+ */
24
+ function labelWidth(options, label) {
25
+ if (!isMeasurable(label))
26
+ return options.maxWidth;
27
+ return Measurement.get(options, label).maximum;
28
+ }
29
+ export class Tree {
30
+ label;
31
+ children;
32
+ expanded;
33
+ hideRoot;
34
+ guideStyle;
35
+ style;
36
+ constructor(label, options) {
37
+ this.label = embed(label);
38
+ this.children = [];
39
+ this.expanded = options?.expanded !== false;
40
+ this.hideRoot = options?.hideRoot ?? false;
41
+ this.guideStyle = options?.guide_style ?? NULL_STYLE;
42
+ this.style = options?.style ?? NULL_STYLE;
43
+ }
44
+ add(label, options) {
45
+ const child = new Tree(label, {
46
+ guide_style: options?.guide_style,
47
+ style: options?.style,
48
+ expanded: options?.expanded,
49
+ });
50
+ this.children.push(child);
51
+ return child;
52
+ }
53
+ *render(rawOptions) {
54
+ const options = withBoundedWidth(rawOptions, this);
55
+ for (const row of this._walk(options)) {
56
+ yield* this._renderRow(options, row);
57
+ }
58
+ }
59
+ /**
60
+ * The rows `render` and `measure` both read, entered in exactly one place so
61
+ * the two cannot start the walk differently. A hidden root takes its own row
62
+ * and the column its children hang from with it, as Rich does — its children
63
+ * stand at the left edge, and its guide style still reaches their guides.
64
+ */
65
+ _walk(options) {
66
+ const rows = [...this._rows(drawable(options, UNICODE_GUIDES, ASCII_GUIDES, guideGlyphs), [], [])];
67
+ return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
68
+ }
69
+ /**
70
+ * The tree walked into the rows it emits, one row per label, guides first.
71
+ *
72
+ * [LAW:one-source-of-truth] The walk that decides which rows exist and what
73
+ * leads them is here and only here. `render` turns each row into segments and
74
+ * `measure` reads each row's width off the same sequence, so the two cannot
75
+ * come to disagree about how many rows there are or how wide the guides on
76
+ * them run — which they would the moment a second walk existed.
77
+ *
78
+ * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
79
+ * this one recursion: its children get the columns above it, settled, plus a
80
+ * branch of their own, so no depth is drawn by a rule of its own.
81
+ */
82
+ *_rows(glyphs, guides, inherited) {
83
+ yield { guides, label: this.label };
84
+ const children = this.expanded ? this.children : [];
85
+ const styles = [...inherited, this.guideStyle];
86
+ const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
87
+ for (let i = 0; i < children.length; i++) {
88
+ const branch = i === children.length - 1 ? glyphs.end : glyphs.fork;
89
+ yield* children[i]._rows(glyphs, [...above, { ...branch, styles }], styles);
90
+ }
91
+ }
92
+ /**
93
+ * One row of the tree: its label in whatever width the guides left, each of
94
+ * the label's lines led by the guides.
95
+ *
96
+ * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
97
+ * Before this, each call site emitted its guides and then handed the label
98
+ * `options` unchanged — the label was told it had the whole outer width while
99
+ * the guides had already spent four cells of it, so a tree at maxWidth 5
100
+ * emitted rows of 9. That is the same defect Panel and Table were fixed for,
101
+ * and the same fix: divide the requested width once and let every part read
102
+ * its share off the division. `options` arrives parsed from `render`.
103
+ *
104
+ * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
105
+ * depending on label renderables to invent trailing newlines: the label's
106
+ * output is cut into lines and every line ends where Tree ends it. A label
107
+ * that emits nothing still holds its row, and one that ends in a newline of
108
+ * its own does not add a blank one.
109
+ */
110
+ *_renderRow(options, row) {
111
+ const styles = row.guides.map((guide) => Style.combine(guide.styles.map((style) => getStyle(options, style))));
112
+ const width = Math.max(0, options.maxWidth - row.guides.reduce((sum, guide) => sum + cellLen(guide.first), 0));
113
+ // The guides are cropped by `cellFit`; the label is cropped too, so a label
114
+ // that ignores the width it is handed cannot push the row past the offer the
115
+ // guides were fitted into.
116
+ const lines = Segment.splitLines(Segment.cropLines(row.label.render({ ...options, maxWidth: width, height: stackedHeight(options.height) }), width));
117
+ if (lines.length === 0)
118
+ lines.push([]);
119
+ for (let i = 0; i < lines.length; i++) {
120
+ let left = options.maxWidth;
121
+ for (let g = 0; g < row.guides.length; g++) {
122
+ const piece = cellFit(i === 0 ? row.guides[g].first : row.guides[g].rest, asCellCol(left));
123
+ if (piece.length > 0)
124
+ yield new Segment(piece, styles[g]);
125
+ left -= cellLen(piece);
126
+ }
127
+ yield* lines[i];
128
+ yield Segment.line();
129
+ }
130
+ }
131
+ measure(options) {
132
+ // The ceiling is parsed for the same reason the rows are: an unparsed NaN
133
+ // ceiling yields a range no parent layout can divide.
134
+ const parsed = withCellWidth(options);
135
+ const ceiling = parsed.maxWidth;
136
+ // The widest row the walk emits, not the offer. Reported as the offer, a
137
+ // tree told any parent that asked "I want all of it" — so `Panel` in fit
138
+ // mode drew a 40-cell frame around nine cells of tree, and an unbounded
139
+ // offer came back unbounded.
140
+ let natural = 0;
141
+ for (const row of this._walk(parsed)) {
142
+ const guideWidth = row.guides.reduce((sum, guide) => sum + cellLen(guide.first), 0);
143
+ natural = Math.max(natural, guideWidth + labelWidth(parsed, row.label));
144
+ }
145
+ const maximum = Math.min(natural, ceiling);
146
+ // A row is its guides plus at least one cell of label — but never more than
147
+ // this tree turned out to want, or the floor would sit above its own ceiling.
148
+ return { minimum: Math.min(4, maximum), maximum };
149
+ }
150
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Viewport — a fixed number of rows onto a taller renderable, scrolled by an
3
+ * offset that cannot leave the content.
4
+ *
5
+ * Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
6
+ * when it is given one; otherwise its own configured rows, or the content's
7
+ * full length when it has none, capped by any ceiling. It renders its content
8
+ * with no height, because the content's natural length is the scroll extent,
9
+ * and pads or crops what comes back to its rows — its rows are its own height,
10
+ * not a region someone else will shape.
11
+ *
12
+ * Its width is the width it is given, in both directions: each row is cropped
13
+ * or padded to it, so content that ignores its width cannot spill past the
14
+ * viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
15
+ * down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
16
+ * has a gutter zero cells wide, and every row takes the same path either way.
17
+ *
18
+ * [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
19
+ * against exists before a render: the rows come from the budget the render is
20
+ * handed, and the content's length from rendering it at the width it is handed.
21
+ * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
+ * are called. Each queues a move, and the next render resolves the queue in
23
+ * call order, clamping after every move, against the rows and length that
24
+ * render found. Calling an operation before the first render, or three of them
25
+ * between two renders, is the same code path as calling one.
26
+ *
27
+ * Every cell of its rows, gutter included, is stamped as its own
28
+ * (`Segment.anchorLines`), around whatever anchors its content drew. So the
29
+ * composed frame names the viewport under any of its cells however deep it is
30
+ * nested, and a cell of a widget inside it names both: that is how the wheel
31
+ * finds the viewport to scroll. `canScrollBy` is how it passes over one that
32
+ * cannot move that way, to the viewport around it.
33
+ */
34
+ import { Segment } from "../core/segment.js";
35
+ import type { Style } from "../core/style.js";
36
+ import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
37
+ /** How one part of a scrollbar draws each row it covers. */
38
+ export interface ScrollbarPart {
39
+ readonly glyph: string;
40
+ /** A style, or a theme name resolved when the viewport renders. */
41
+ readonly style: string | Style;
42
+ }
43
+ /**
44
+ * The look of a scrollbar: the thumb, over the rows of the track that stand for
45
+ * the lines in view, and the track, over the rest. Its gutter is as wide as the
46
+ * wider of the two glyphs.
47
+ */
48
+ export interface Scrollbar {
49
+ readonly thumb: ScrollbarPart;
50
+ readonly track: ScrollbarPart;
51
+ }
52
+ /** A heavy line for the thumb on a light line for the track. */
53
+ export declare const SCROLLBAR: Scrollbar;
54
+ export interface ViewportOptions {
55
+ /**
56
+ * The rows shown when the viewport is given no region. Absent, it shows the
57
+ * content's full length. A ceiling caps either.
58
+ */
59
+ rows?: number;
60
+ /** A scrollbar down the right edge. Absent, the content has the full width. */
61
+ scrollbar?: Scrollbar;
62
+ }
63
+ export declare class Viewport implements Renderable, Measurable, Scrollable {
64
+ /**
65
+ * What the viewport shows. Replace it to show new content from the same
66
+ * scroll position — a view rebuilt every frame keeps one `Viewport`.
67
+ */
68
+ content: Renderable;
69
+ readonly rows: number | undefined;
70
+ readonly scrollbar: Scrollbar;
71
+ private _offset;
72
+ private _extent;
73
+ private _moves;
74
+ constructor(content: Renderable, options?: ViewportOptions);
75
+ /**
76
+ * The first content line the last render showed. A move requested since is
77
+ * not reflected until the next render resolves it.
78
+ */
79
+ get offset(): number;
80
+ /** Scroll so `line` is the first line shown. */
81
+ scrollTo(line: number): void;
82
+ /** Scroll by `lines`: down when positive, up when negative. */
83
+ scrollBy(lines: number): void;
84
+ /**
85
+ * Whether `scrollBy(lines)` would move the viewport from where the moves
86
+ * queued so far leave it, against the rows and length the last render
87
+ * found.
88
+ */
89
+ canScrollBy(lines: number): boolean;
90
+ /**
91
+ * Scroll the least distance that shows lines `start` up to but not
92
+ * including `end` — lines of the content as it renders at the viewport's
93
+ * width, so an item that wraps spans more than one. A range already in view does not move; one taller than
94
+ * the viewport shows its first line at the top.
95
+ */
96
+ ensureVisible(start: number, end: number): void;
97
+ /**
98
+ * The width its content renders at, out of `width`: what the scrollbar's
99
+ * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
100
+ */
101
+ contentWidth(width: number): number;
102
+ render(rawOptions: RenderOptions): Iterable<Segment>;
103
+ /**
104
+ * [LAW:one-source-of-truth] Where the queued moves leave the offset against
105
+ * `extent`: what a render commits, and what `canScrollBy` asks from.
106
+ * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
107
+ * queued moves: content that shrank since the last render pulls it back.
108
+ */
109
+ private resolve;
110
+ measure(rawOptions: RenderOptions): {
111
+ minimum: number;
112
+ maximum: number;
113
+ };
114
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Viewport — a fixed number of rows onto a taller renderable, scrolled by an
3
+ * offset that cannot leave the content.
4
+ *
5
+ * Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
6
+ * when it is given one; otherwise its own configured rows, or the content's
7
+ * full length when it has none, capped by any ceiling. It renders its content
8
+ * with no height, because the content's natural length is the scroll extent,
9
+ * and pads or crops what comes back to its rows — its rows are its own height,
10
+ * not a region someone else will shape.
11
+ *
12
+ * Its width is the width it is given, in both directions: each row is cropped
13
+ * or padded to it, so content that ignores its width cannot spill past the
14
+ * viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
15
+ * down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
16
+ * has a gutter zero cells wide, and every row takes the same path either way.
17
+ *
18
+ * [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
19
+ * against exists before a render: the rows come from the budget the render is
20
+ * handed, and the content's length from rendering it at the width it is handed.
21
+ * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
+ * are called. Each queues a move, and the next render resolves the queue in
23
+ * call order, clamping after every move, against the rows and length that
24
+ * render found. Calling an operation before the first render, or three of them
25
+ * between two renders, is the same code path as calling one.
26
+ *
27
+ * Every cell of its rows, gutter included, is stamped as its own
28
+ * (`Segment.anchorLines`), around whatever anchors its content drew. So the
29
+ * composed frame names the viewport under any of its cells however deep it is
30
+ * nested, and a cell of a widget inside it names both: that is how the wheel
31
+ * finds the viewport to scroll. `canScrollBy` is how it passes over one that
32
+ * cannot move that way, to the viewport around it.
33
+ */
34
+ import { Segment } from "../core/segment.js";
35
+ import { Measurement } from "../core/measure.js";
36
+ import { cellCount, cellLen } from "../core/cells.js";
37
+ import { NULL_STYLE } from "../core/style.js";
38
+ import { drawable, fitHeight, getStyle, isMeasurable, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
39
+ /** A heavy line for the thumb on a light line for the track. */
40
+ export const SCROLLBAR = {
41
+ thumb: { glyph: "┃", style: "scrollbar.thumb" },
42
+ track: { glyph: "│", style: "scrollbar.track" },
43
+ };
44
+ /** The absence of a scrollbar, as a gutter zero cells wide. */
45
+ const NO_SCROLLBAR = {
46
+ thumb: { glyph: "", style: NULL_STYLE },
47
+ track: { glyph: "", style: NULL_STYLE },
48
+ };
49
+ export class Viewport {
50
+ /**
51
+ * What the viewport shows. Replace it to show new content from the same
52
+ * scroll position — a view rebuilt every frame keeps one `Viewport`.
53
+ */
54
+ content;
55
+ rows;
56
+ scrollbar;
57
+ _offset = 0;
58
+ // Nothing rendered, nothing shown: no move can take until the first render.
59
+ _extent = { rows: 0, lines: 0 };
60
+ _moves = [];
61
+ constructor(content, options = {}) {
62
+ this.content = content;
63
+ this.rows = options.rows;
64
+ this.scrollbar = options.scrollbar ?? NO_SCROLLBAR;
65
+ }
66
+ /**
67
+ * The first content line the last render showed. A move requested since is
68
+ * not reflected until the next render resolves it.
69
+ */
70
+ get offset() {
71
+ return this._offset;
72
+ }
73
+ /** Scroll so `line` is the first line shown. */
74
+ scrollTo(line) {
75
+ this._moves.push(() => line);
76
+ }
77
+ /** Scroll by `lines`: down when positive, up when negative. */
78
+ scrollBy(lines) {
79
+ this._moves.push((offset) => offset + lines);
80
+ }
81
+ /**
82
+ * Whether `scrollBy(lines)` would move the viewport from where the moves
83
+ * queued so far leave it, against the rows and length the last render
84
+ * found.
85
+ */
86
+ canScrollBy(lines) {
87
+ const offset = this.resolve(this._extent);
88
+ return clampOffset(offset + lines, this._extent) !== offset;
89
+ }
90
+ /**
91
+ * Scroll the least distance that shows lines `start` up to but not
92
+ * including `end` — lines of the content as it renders at the viewport's
93
+ * width, so an item that wraps spans more than one. A range already in view does not move; one taller than
94
+ * the viewport shows its first line at the top.
95
+ */
96
+ ensureVisible(start, end) {
97
+ this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
98
+ }
99
+ /**
100
+ * The width its content renders at, out of `width`: what the scrollbar's
101
+ * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
102
+ */
103
+ contentWidth(width) {
104
+ return cellCount(width - gutterWidth(this.scrollbar));
105
+ }
106
+ *render(rawOptions) {
107
+ const { height, ...options } = withBoundedWidth(rawOptions, this);
108
+ const gutter = gutterWidth(this.scrollbar);
109
+ const contentWidth = this.contentWidth(options.maxWidth);
110
+ const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
111
+ const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
112
+ this._extent = extent;
113
+ this._offset = this.resolve(extent);
114
+ this._moves = [];
115
+ const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
116
+ // [LAW:no-ambient-temporal-coupling] The thumb is drawn from the offset
117
+ // this render just resolved, not from `offset` as it stood before it.
118
+ const thumb = thumbRows(extent, this._offset);
119
+ // The content's cells and the gutter's drawn cells sum to the offer: a
120
+ // gutter wider than the whole offer is drawn cropped to it.
121
+ const drawn = Math.min(gutter, options.maxWidth);
122
+ // The style reaches the padding too, so a glyph narrower than the gutter
123
+ // still fills its cell with its part's background.
124
+ const cell = ({ glyph, style }) => {
125
+ const resolved = getStyle(options, style);
126
+ return Segment.adjustLineLength([new Segment(glyph, resolved)], drawn, resolved);
127
+ };
128
+ // The gutter keeps the chosen scrollbar's width, so an ASCII stand-in
129
+ // padded into it leaves the content where `contentWidth` said it would be.
130
+ const bar = drawable(options, this.scrollbar, asciiScrollbar(this.scrollbar), scrollbarGlyphs);
131
+ const thumbCell = cell(bar.thumb);
132
+ const trackCell = cell(bar.track);
133
+ const rows = shown.map((line, row) => [
134
+ ...Segment.adjustLineLength(line, contentWidth),
135
+ ...(row >= thumb.start && row < thumb.end ? thumbCell : trackCell),
136
+ ]);
137
+ for (const line of Segment.anchorLines(rows, this)) {
138
+ yield* line;
139
+ yield Segment.line();
140
+ }
141
+ }
142
+ /**
143
+ * [LAW:one-source-of-truth] Where the queued moves leave the offset against
144
+ * `extent`: what a render commits, and what `canScrollBy` asks from.
145
+ * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
146
+ * queued moves: content that shrank since the last render pulls it back.
147
+ */
148
+ resolve(extent) {
149
+ return this._moves.reduce((offset, move) => clampOffset(move(offset, extent), extent), clampOffset(this._offset, extent));
150
+ }
151
+ measure(rawOptions) {
152
+ // Its width is its content's and its gutter's. `withCellWidth` and not
153
+ // `withBoundedWidth`: the bounded parse asks this very method for the
154
+ // natural width.
155
+ const options = withCellWidth(rawOptions);
156
+ const gutter = gutterWidth(this.scrollbar);
157
+ const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
158
+ const content = isMeasurable(this.content)
159
+ ? Measurement.get(inner, this.content)
160
+ : new Measurement(Math.min(1, inner.maxWidth), inner.maxWidth);
161
+ return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
162
+ }
163
+ }
164
+ const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
165
+ /** `scrollbar` in its own styles, drawn in ASCII. */
166
+ function asciiScrollbar({ thumb, track }) {
167
+ return { thumb: { ...thumb, glyph: "#" }, track: { ...track, glyph: "|" } };
168
+ }
169
+ /** The cells a scrollbar's gutter takes from the content: its wider glyph. */
170
+ function gutterWidth({ thumb, track }) {
171
+ return Math.max(cellLen(thumb.glyph), cellLen(track.glyph));
172
+ }
173
+ /**
174
+ * The rows of the track the thumb covers, from `start` up to but not including
175
+ * `end`: its length is the share of the content in view, and its position the
176
+ * share of the scroll travelled. Content that fits fills the track.
177
+ *
178
+ * An end of the track means an end of the content. Rounded alone, the position
179
+ * reached the bottom while lines were still hidden below — at 10 rows of 20
180
+ * lines, offset 9 rounds onto the last position. So an offset short of an end
181
+ * is held one row off it, wherever the track has a row between its ends.
182
+ */
183
+ function thumbRows({ rows, lines }, offset) {
184
+ const size = Math.min(rows, Math.max(1, Math.round((rows * rows) / Math.max(lines, rows, 1))));
185
+ const travel = rows - size;
186
+ const last = Math.max(1, lines - rows);
187
+ const proportional = Math.round((offset * travel) / last);
188
+ const start = Math.max(Math.min(proportional, travel - Math.min(last - offset, 1)), Math.min(offset, 1, travel));
189
+ return { start, end: start + size };
190
+ }
191
+ /** The rows a viewport shows, from its budget, its own configured rows, and its content's length. */
192
+ function viewRows(height, rows, lines) {
193
+ return regionRows(height) ?? cellCount(Math.min(rows ?? lines, height?.rows ?? Infinity));
194
+ }
195
+ /** An offset held to the lines that exist: never above the first, never past the last full view. */
196
+ function clampOffset(offset, { rows, lines }) {
197
+ return cellCount(Math.min(offset, lines - rows));
198
+ }