@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,92 @@
1
+ /**
2
+ * Layout — divides the screen into rectangular regions.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
6
+ export interface LayoutOptions {
7
+ name?: string;
8
+ ratio?: number;
9
+ size?: number;
10
+ minimumSize?: number;
11
+ visible?: boolean;
12
+ }
13
+ export declare class Layout implements Renderable, Measurable {
14
+ name: string | undefined;
15
+ visible: boolean;
16
+ private _ratio;
17
+ private _size;
18
+ private _minimumSize;
19
+ private _renderable;
20
+ private _children;
21
+ private _splitDirection;
22
+ constructor(renderable?: Renderable | string, options?: LayoutOptions);
23
+ /**
24
+ * The three declared numbers, parsed on assignment rather than at the
25
+ * constructor. All three are public and a caller reaches them long after
26
+ * construction — `layout.getByName("pane")!.ratio = -1` walked straight past a
27
+ * constructor-only parse and put a negative weight back into the division that
28
+ * `_rowBudgetFor` and `_distributeSpace` are written to trust.
29
+ *
30
+ * [LAW:parse-dont-validate] The setter is the border, so the guarantee holds
31
+ * for the object's whole lifetime and nothing downstream re-checks. `size` and
32
+ * `minimumSize` are cell counts; `ratio` is a share weight and keeps its
33
+ * fractions. Absence is preserved rather than parsed: an undefined `size`
34
+ * selects a flex pane, and `cellCount` would read it as a declared zero.
35
+ */
36
+ get ratio(): number;
37
+ set ratio(value: number);
38
+ get size(): number | undefined;
39
+ set size(value: number | undefined);
40
+ get minimumSize(): number;
41
+ set minimumSize(value: number);
42
+ get children(): Layout[];
43
+ /**
44
+ * Whether this layout draws `_renderable` or its children.
45
+ *
46
+ * [LAW:one-source-of-truth] `render` and `_naturalWidth` must answer this the
47
+ * same way. Asked separately they did not: `_naturalWidth` read it off the
48
+ * *visible* children, so a layout holding content and a single hidden child
49
+ * reported that content's width while `render` emitted nothing at all, and a
50
+ * fit-mode `Panel` framed twelve cells of air.
51
+ */
52
+ private get _isLeaf();
53
+ splitColumn(...layouts: Layout[]): void;
54
+ splitRow(...layouts: Layout[]): void;
55
+ update(renderable: Renderable | string): void;
56
+ getByName(name: string): Layout | undefined;
57
+ render(rawOptions: RenderOptions): Iterable<Segment>;
58
+ private _renderColumn;
59
+ private _renderRow;
60
+ private _distributeSpace;
61
+ /**
62
+ * The width this layout would take if nothing constrained it: its content's
63
+ * for a leaf, its children's laid out the way the split lays them out.
64
+ *
65
+ * A `size` counts only across a row, which is the one direction in which it
66
+ * is a width — down a column the same field is a height, and reading it as a
67
+ * width there would report a two-line pane as two cells wide.
68
+ */
69
+ private _naturalWidth;
70
+ /**
71
+ * The budget at which `_distributeSpace` gives every child of a row at least
72
+ * the width it wants — the inverse of the ratio rule above, and the reason
73
+ * this is not the sum of those widths.
74
+ *
75
+ * [LAW:one-source-of-truth] A growing child receives `floor(budget * ratio /
76
+ * totalRatio)`, so to receive `want` cells it needs the budget to reach
77
+ * `want * totalRatio / ratio`, and one budget serves them all: the row needs
78
+ * the largest demand, never their total. Summed instead, a 1:1 split of
79
+ * "left" and "right" reported 9 and then rendered "right" into the 4 cells
80
+ * that `floor(9/2)` actually hands it.
81
+ *
82
+ * The panes that do not grow are paid first here for the same reason they
83
+ * are paid first there — and it is the reason this divides by no zero: a
84
+ * `ratio` of 0 never reaches the division, because a pane that does not grow
85
+ * demands nothing of a budget for growing.
86
+ */
87
+ private _rowBudgetFor;
88
+ measure(rawOptions: RenderOptions): {
89
+ minimum: number;
90
+ maximum: number;
91
+ };
92
+ }
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Layout — divides the screen into rectangular regions.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { embed } from "./embed.js";
6
+ import { fitHeight, isMeasurable, regionRows, stackedHeight, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
7
+ import { Measurement } from "../core/measure.js";
8
+ import { cellCount } from "../core/cells.js";
9
+ /**
10
+ * A share weight, not a cell count: fractions divide space meaningfully, so
11
+ * this parses where `cellCount` would floor. A weight that cannot name a share
12
+ * — negative, NaN, infinite — reads as zero, which already means "this pane
13
+ * does not grow" and is filtered out before any division. That is what makes
14
+ * every ratio reaching `_distributeSpace` and `_rowBudgetFor` positive.
15
+ */
16
+ function growthRatio(ratio) {
17
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : 0;
18
+ }
19
+ /**
20
+ * A pane rendered into a region of `rows` and held to exactly that many lines,
21
+ * or — with `rows` undefined — rendered under the layout's own budget, as a
22
+ * ceiling, at its natural height.
23
+ *
24
+ * [LAW:single-enforcer] The region's setter shapes it (`fitHeight`). Forwarded
25
+ * unshaped, a pane whose content ran short pulled every pane below it up, and
26
+ * one that ran long pushed them down.
27
+ */
28
+ function paneLines(pane, options, rows) {
29
+ const height = rows === undefined ? stackedHeight(options.height) : { rows, exact: true };
30
+ return fitHeight(Segment.splitLines(pane.render({ ...options, height })), height);
31
+ }
32
+ export class Layout {
33
+ name;
34
+ visible;
35
+ _ratio;
36
+ _size;
37
+ _minimumSize;
38
+ _renderable;
39
+ _children;
40
+ _splitDirection;
41
+ constructor(renderable, options) {
42
+ if (renderable !== undefined) {
43
+ this._renderable = embed(renderable);
44
+ }
45
+ this.name = options?.name;
46
+ this.ratio = options?.ratio ?? 1;
47
+ this.size = options?.size;
48
+ this.minimumSize = options?.minimumSize ?? 1;
49
+ this.visible = options?.visible !== false;
50
+ this._children = [];
51
+ this._splitDirection = undefined;
52
+ }
53
+ /**
54
+ * The three declared numbers, parsed on assignment rather than at the
55
+ * constructor. All three are public and a caller reaches them long after
56
+ * construction — `layout.getByName("pane")!.ratio = -1` walked straight past a
57
+ * constructor-only parse and put a negative weight back into the division that
58
+ * `_rowBudgetFor` and `_distributeSpace` are written to trust.
59
+ *
60
+ * [LAW:parse-dont-validate] The setter is the border, so the guarantee holds
61
+ * for the object's whole lifetime and nothing downstream re-checks. `size` and
62
+ * `minimumSize` are cell counts; `ratio` is a share weight and keeps its
63
+ * fractions. Absence is preserved rather than parsed: an undefined `size`
64
+ * selects a flex pane, and `cellCount` would read it as a declared zero.
65
+ */
66
+ get ratio() {
67
+ return this._ratio;
68
+ }
69
+ set ratio(value) {
70
+ this._ratio = growthRatio(value);
71
+ }
72
+ get size() {
73
+ return this._size;
74
+ }
75
+ set size(value) {
76
+ this._size = value === undefined ? undefined : cellCount(value);
77
+ }
78
+ get minimumSize() {
79
+ return this._minimumSize;
80
+ }
81
+ set minimumSize(value) {
82
+ this._minimumSize = cellCount(value);
83
+ }
84
+ get children() {
85
+ return this._children;
86
+ }
87
+ /**
88
+ * Whether this layout draws `_renderable` or its children.
89
+ *
90
+ * [LAW:one-source-of-truth] `render` and `_naturalWidth` must answer this the
91
+ * same way. Asked separately they did not: `_naturalWidth` read it off the
92
+ * *visible* children, so a layout holding content and a single hidden child
93
+ * reported that content's width while `render` emitted nothing at all, and a
94
+ * fit-mode `Panel` framed twelve cells of air.
95
+ */
96
+ get _isLeaf() {
97
+ return this._children.length === 0;
98
+ }
99
+ splitColumn(...layouts) {
100
+ this._children = layouts;
101
+ this._splitDirection = "column";
102
+ }
103
+ splitRow(...layouts) {
104
+ this._children = layouts;
105
+ this._splitDirection = "row";
106
+ }
107
+ update(renderable) {
108
+ this._renderable = embed(renderable);
109
+ }
110
+ getByName(name) {
111
+ if (this.name === name)
112
+ return this;
113
+ for (const child of this._children) {
114
+ const found = child.getByName(name);
115
+ if (found)
116
+ return found;
117
+ }
118
+ return undefined;
119
+ }
120
+ *render(rawOptions) {
121
+ if (!this.visible)
122
+ return;
123
+ // Parsed once at the top of the layout rather than at the row split below,
124
+ // because a column split forwards the width to its children untouched and
125
+ // would otherwise hand each of them the caller's raw number.
126
+ const options = withBoundedWidth(rawOptions, this);
127
+ if (this._isLeaf) {
128
+ // Cropped rather than forwarded: a leaf hands its content the offer and
129
+ // content is free to ignore it, and a pane wider than the region it was
130
+ // given is the one thing a layout may never emit — in a row split it
131
+ // overwrites the pane beside it. The row path already crops each share,
132
+ // so this is the same rule at the one place that skipped it.
133
+ if (this._renderable) {
134
+ yield* Segment.cropLines(this._renderable.render(options), options.maxWidth);
135
+ }
136
+ return;
137
+ }
138
+ const visibleChildren = this._children.filter((c) => c.visible);
139
+ if (visibleChildren.length === 0)
140
+ return;
141
+ if (this._splitDirection === "row") {
142
+ yield* this._renderRow(visibleChildren, options);
143
+ }
144
+ else {
145
+ yield* this._renderColumn(visibleChildren, options);
146
+ }
147
+ }
148
+ *_renderColumn(children, options) {
149
+ // Each pane gets full width and its share of the region. With no region —
150
+ // a ceiling, or no budget at all — a pane's share is its declared `size`,
151
+ // and a pane without one takes its content's height.
152
+ const region = regionRows(options.height);
153
+ const shares = region === undefined
154
+ ? children.map((child) => child.size)
155
+ : this._distributeSpace(children, region);
156
+ for (let i = 0; i < children.length; i++) {
157
+ for (const line of paneLines(children[i], options, shares[i])) {
158
+ yield* line;
159
+ yield Segment.line();
160
+ }
161
+ }
162
+ }
163
+ *_renderRow(children, options) {
164
+ // Horizontal side-by-side: divide the requested width once, then merge
165
+ // child lines. The budget arrives parsed from `render` — unparsed, a NaN
166
+ // width made every share NaN and the merge threw `Invalid array length`.
167
+ // Every pane stands in the whole region, and with none the merge pads the
168
+ // row to its tallest pane.
169
+ const widths = this._distributeSpace(children, options.maxWidth);
170
+ const region = regionRows(options.height);
171
+ const cells = children.map((child, i) => ({
172
+ width: widths[i],
173
+ lines: paneLines(child, { ...options, maxWidth: widths[i] }, region),
174
+ }));
175
+ yield* Segment.mergeHorizontal(cells);
176
+ }
177
+ _distributeSpace(children, totalSpace) {
178
+ const sizes = new Array(children.length).fill(0);
179
+ let remaining = totalSpace;
180
+ // Children that do not grow are paid first: a declared `size`, and equally
181
+ // a `ratio` of 0, which is the caller saying this pane stays at its
182
+ // `minimumSize`. Left in the ratio pass below, a zero-ratio child divided
183
+ // by its own ratio and drew its minimum out of a budget the shares had
184
+ // already spent — three panes of `zzz`/`aaaaa`/`bbbbb` consumed
185
+ // `3 + 2*floor(T/2)` cells out of `T`, which no `T` satisfies, and the row
186
+ // only came out right at 16 because the `Math.min(remaining, ...)` below
187
+ // happened to clip the last share to the right number.
188
+ const growing = [];
189
+ for (let i = 0; i < children.length; i++) {
190
+ const child = children[i];
191
+ if (child.size !== undefined) {
192
+ sizes[i] = Math.min(child.size, remaining);
193
+ }
194
+ else if (child.ratio === 0) {
195
+ sizes[i] = Math.min(child.minimumSize, remaining);
196
+ }
197
+ else {
198
+ growing.push(i);
199
+ continue;
200
+ }
201
+ remaining -= sizes[i];
202
+ }
203
+ // Distribute what is left by ratio. Each child's share comes off one
204
+ // budget and is bounded by what that budget still holds: `minimumSize` is
205
+ // what a child asks for, `remaining` is what there is to give, and the
206
+ // second beats the first. Applied without that bound — as
207
+ // `Math.max(minimumSize, share)` alone — every child claimed its whole
208
+ // minimum out of a budget that could not cover one of them, so two
209
+ // children of a Layout asked to fit one cell merged into a two-cell row.
210
+ if (growing.length > 0 && remaining > 0) {
211
+ const budget = remaining;
212
+ // Every ratio in here is positive — `growthRatio` parses the rest to zero
213
+ // at the constructor and zero is filtered out above — which is what makes
214
+ // the division safe and `_rowBudgetFor`'s inverse of it finite.
215
+ const totalRatio = growing.reduce((s, idx) => s + children[idx].ratio, 0);
216
+ for (const idx of growing) {
217
+ const child = children[idx];
218
+ const share = Math.floor(budget * child.ratio / totalRatio);
219
+ const allocated = Math.min(remaining, Math.max(child.minimumSize, share));
220
+ sizes[idx] = allocated;
221
+ remaining -= allocated;
222
+ }
223
+ }
224
+ return sizes;
225
+ }
226
+ /**
227
+ * The width this layout would take if nothing constrained it: its content's
228
+ * for a leaf, its children's laid out the way the split lays them out.
229
+ *
230
+ * A `size` counts only across a row, which is the one direction in which it
231
+ * is a width — down a column the same field is a height, and reading it as a
232
+ * width there would report a two-line pane as two cells wide.
233
+ */
234
+ _naturalWidth(options) {
235
+ if (!this.visible)
236
+ return 0;
237
+ if (this._isLeaf) {
238
+ if (this._renderable === undefined)
239
+ return 0;
240
+ // A leaf whose content cannot measure itself has no width of its own to
241
+ // report, so it reports the offer — unbounded included, which is where
242
+ // `withBoundedWidth` says so rather than inventing a number.
243
+ return isMeasurable(this._renderable)
244
+ ? Measurement.get(options, this._renderable).maximum
245
+ : options.maxWidth;
246
+ }
247
+ const visible = this._children.filter((c) => c.visible);
248
+ if (visible.length === 0)
249
+ return 0;
250
+ const widths = visible.map((c) => c._naturalWidth(options));
251
+ if (this._splitDirection === "row")
252
+ return this._rowBudgetFor(visible, widths);
253
+ // Accumulated, not spread: a column split holds as many children as a caller
254
+ // made, and `Math.max(...widths)` passes one argument per child, so a
255
+ // generated dashboard deep enough overruns the engine's argument limit and
256
+ // throws out of `measure()`.
257
+ let widest = 0;
258
+ for (const width of widths)
259
+ widest = Math.max(widest, width);
260
+ return widest;
261
+ }
262
+ /**
263
+ * The budget at which `_distributeSpace` gives every child of a row at least
264
+ * the width it wants — the inverse of the ratio rule above, and the reason
265
+ * this is not the sum of those widths.
266
+ *
267
+ * [LAW:one-source-of-truth] A growing child receives `floor(budget * ratio /
268
+ * totalRatio)`, so to receive `want` cells it needs the budget to reach
269
+ * `want * totalRatio / ratio`, and one budget serves them all: the row needs
270
+ * the largest demand, never their total. Summed instead, a 1:1 split of
271
+ * "left" and "right" reported 9 and then rendered "right" into the 4 cells
272
+ * that `floor(9/2)` actually hands it.
273
+ *
274
+ * The panes that do not grow are paid first here for the same reason they
275
+ * are paid first there — and it is the reason this divides by no zero: a
276
+ * `ratio` of 0 never reaches the division, because a pane that does not grow
277
+ * demands nothing of a budget for growing.
278
+ */
279
+ _rowBudgetFor(children, widths) {
280
+ const growing = children.filter((c) => c.size === undefined && c.ratio !== 0);
281
+ const totalRatio = growing.reduce((sum, c) => sum + c.ratio, 0);
282
+ let pinned = 0;
283
+ let budget = 0;
284
+ for (const [i, child] of children.entries()) {
285
+ if (child.size !== undefined) {
286
+ pinned += child.size;
287
+ }
288
+ else if (child.ratio === 0) {
289
+ pinned += child.minimumSize;
290
+ }
291
+ else {
292
+ const want = Math.max(child.minimumSize, widths[i]);
293
+ budget = Math.max(budget, Math.ceil(want * totalRatio / child.ratio));
294
+ }
295
+ }
296
+ return pinned + budget;
297
+ }
298
+ measure(rawOptions) {
299
+ // `minimumSize` is what this layout asks for and the ceiling is what it was
300
+ // offered; the ceiling wins. Unclamped, a layout measured into no width
301
+ // reported the range 1..0 — a floor above its own ceiling, which the parent
302
+ // that asked cannot divide.
303
+ //
304
+ // The ceiling alone was not the answer either: a layout that reported the
305
+ // whole offer as its maximum told every parent it wanted all of it, so
306
+ // `Panel` in fit mode drew its frame at the full console width around a
307
+ // layout of two short panes, and an unbounded offer came back unbounded.
308
+ const parsed = withCellWidth(rawOptions);
309
+ const maximum = Math.min(this._naturalWidth(parsed), parsed.maxWidth);
310
+ return { minimum: Math.min(this.minimumSize, maximum), maximum };
311
+ }
312
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Live — animates a portion of the terminal by continuously re-rendering.
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.
7
+ * - **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.
11
+ */
12
+ import { Console } from "../core/console.js";
13
+ import { type Renderable } from "../core/protocol.js";
14
+ export interface LiveOptions {
15
+ refreshPerSecond?: number;
16
+ autoRefresh?: boolean;
17
+ transient?: boolean;
18
+ console?: Console;
19
+ verticalOverflow?: "crop" | "ellipsis" | "visible";
20
+ /** When true, Live enters the alternate screen buffer on start() and
21
+ * restores the original on stop(). Refresh uses cursor-home instead of
22
+ * cursor-up-and-clear, eliminating flicker for full-screen layouts. */
23
+ altScreen?: boolean;
24
+ }
25
+ export declare class Live {
26
+ private _renderable;
27
+ private _console;
28
+ private _refreshPerSecond;
29
+ private _autoRefresh;
30
+ private _transient;
31
+ private _verticalOverflow;
32
+ private _altScreen;
33
+ private _timer;
34
+ private _lastLineCount;
35
+ private _started;
36
+ private _firstRefresh;
37
+ constructor(renderable?: Renderable, options?: LiveOptions);
38
+ get console(): Console;
39
+ get renderable(): Renderable | undefined;
40
+ start(): void;
41
+ stop(): void;
42
+ update(renderable?: Renderable, options?: {
43
+ refresh?: boolean;
44
+ }): void;
45
+ refresh(): void;
46
+ private _overflow;
47
+ private _erase;
48
+ private _release;
49
+ private _writeCursorControl;
50
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Live — animates a portion of the terminal by continuously re-rendering.
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.
7
+ * - **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.
11
+ */
12
+ import { Console } from "../core/console.js";
13
+ import { Segment } from "../core/segment.js";
14
+ import { segmentsToString } from "../core/render.js";
15
+ import { fitHeight } from "../core/protocol.js";
16
+ export class Live {
17
+ _renderable;
18
+ _console;
19
+ _refreshPerSecond;
20
+ _autoRefresh;
21
+ _transient;
22
+ _verticalOverflow;
23
+ _altScreen;
24
+ _timer;
25
+ _lastLineCount;
26
+ _started;
27
+ _firstRefresh;
28
+ constructor(renderable, options) {
29
+ this._renderable = renderable;
30
+ this._console = options?.console ?? new Console({ forceTerminal: true });
31
+ this._refreshPerSecond = options?.refreshPerSecond ?? 4;
32
+ this._autoRefresh = options?.autoRefresh !== false;
33
+ this._transient = options?.transient ?? false;
34
+ this._verticalOverflow = options?.verticalOverflow ?? "ellipsis";
35
+ this._altScreen = options?.altScreen ?? false;
36
+ this._lastLineCount = 0;
37
+ this._started = false;
38
+ this._firstRefresh = true;
39
+ }
40
+ get console() {
41
+ return this._console;
42
+ }
43
+ get renderable() {
44
+ return this._renderable;
45
+ }
46
+ start() {
47
+ if (this._started)
48
+ 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);
56
+ if (this._autoRefresh) {
57
+ const interval = Math.floor(1000 / this._refreshPerSecond);
58
+ // No caller is on the stack to run `stop()` for a frame that throws
59
+ // here, and in Node the throw ends the process — so the terminal is
60
+ // handed back first, and the error then goes on to wherever an uncaught
61
+ // one goes.
62
+ this._timer = setInterval(() => {
63
+ try {
64
+ this.refresh();
65
+ }
66
+ catch (error) {
67
+ this._release();
68
+ throw error;
69
+ }
70
+ }, interval);
71
+ }
72
+ }
73
+ stop() {
74
+ if (!this._started)
75
+ return;
76
+ // [LAW:no-silent-failure] A final frame that throws still propagates, and
77
+ // the terminal is handed back on the way out.
78
+ try {
79
+ if (this._transient) {
80
+ this._console.file.write(this._erase());
81
+ this._lastLineCount = 0;
82
+ }
83
+ else {
84
+ this.refresh();
85
+ }
86
+ }
87
+ finally {
88
+ this._release();
89
+ }
90
+ }
91
+ update(renderable, options) {
92
+ if (renderable !== undefined) {
93
+ this._renderable = renderable;
94
+ }
95
+ if (options?.refresh) {
96
+ this.refresh();
97
+ }
98
+ }
99
+ refresh() {
100
+ // A frame is drawn only while this Live holds the terminal, as in Rich:
101
+ // before `start()` or after the hand-back it would land on a screen that
102
+ // belongs to someone else — the main buffer, under the user's own output.
103
+ if (!this._started || !this._renderable)
104
+ return;
105
+ // [LAW:dataflow-not-control-flow] Both modes render under the terminal's
106
+ // rows and differ only in what those rows are: the alternate screen is a
107
+ // region the frame stands in, and an inline frame keeps its natural height
108
+ // under them as a ceiling. Live set the budget, so Live shapes what comes
109
+ // back — its overflow policy, then `fitHeight`, which pads only a region.
110
+ const options = this._console.options;
111
+ const height = { rows: options.height.rows, exact: this._altScreen };
112
+ 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
+ }
145
+ // Lines past `rows`: dropped, with the last kept row replaced by an ellipsis,
146
+ // or left for the terminal to scroll.
147
+ _overflow(lines, rows) {
148
+ if (this._verticalOverflow === "visible" || lines.length <= rows)
149
+ return lines;
150
+ const kept = lines.slice(0, rows);
151
+ if (this._verticalOverflow === "ellipsis" && kept.length > 0) {
152
+ kept[kept.length - 1] = [new Segment("...")];
153
+ }
154
+ return kept;
155
+ }
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
+ // [LAW:single-enforcer] The one way the terminal is handed back — the timer
161
+ // stopped, the cursor shown, the alternate screen left — whether `stop()`
162
+ // 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");
174
+ }
175
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Markdown — renders Markdown content to the terminal.
3
+ * Uses built-in parsing (no external dependency).
4
+ */
5
+ import { Segment } from "../core/segment.js";
6
+ import { Style } from "../core/style.js";
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
+ export interface MarkdownOptions {
9
+ codeTheme?: string;
10
+ inlineCodeStyle?: string | Style;
11
+ hyperlinks?: boolean;
12
+ justify?: "left" | "center" | "right" | "full";
13
+ }
14
+ export declare class Markdown implements Renderable, Measurable {
15
+ readonly markdown: string;
16
+ readonly inlineCodeStyle: string | Style;
17
+ readonly hyperlinks: boolean;
18
+ constructor(markdown: string, options?: MarkdownOptions);
19
+ render(rawOptions: RenderOptions): Iterable<Segment>;
20
+ measure(options: RenderOptions): {
21
+ minimum: number;
22
+ maximum: number;
23
+ };
24
+ }