@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,307 @@
1
+ /**
2
+ * Panel — a bordered box that wraps content, with optional title and subtitle.
3
+ */
4
+ import { cellLen, setCellSize, asCellCol, cellCount } from "../core/cells.js";
5
+ import { Segment } from "../core/segment.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
+ import { ROUNDED } from "../core/box.js";
8
+ import { Measurement } from "../core/measure.js";
9
+ import { RichText } from "../core/text.js";
10
+ import { embed, inlineLabel } from "./embed.js";
11
+ import { normalizePadding } from "./padding.js";
12
+ import { fitHeight, getStyle, insetHeight, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
13
+ function layoutPanel(outerWidth, padding) {
14
+ const [, padRightWanted, , padLeftWanted] = padding;
15
+ // Panel's one division point, and the width reaching it is not always one
16
+ // this file produced: `_getPanelWidth` derives the fit-mode width from
17
+ // `Measurement.get` on an arbitrary `Measurable`, so a renderable reporting a
18
+ // negative maximum arrives here unparsed and reaches `repeat` as a negative
19
+ // count.
20
+ let budget = cellCount(outerWidth);
21
+ const take = (want) => {
22
+ const got = Math.min(want, budget);
23
+ budget -= got;
24
+ return got;
25
+ };
26
+ const left = take(1);
27
+ const right = take(1);
28
+ const firstContentCell = take(1);
29
+ const padLeft = take(padLeftWanted);
30
+ const padRight = take(padRightWanted);
31
+ const contentWidth = firstContentCell + budget;
32
+ return {
33
+ left,
34
+ right,
35
+ padLeft,
36
+ contentWidth,
37
+ padRight,
38
+ spanWidth: padLeft + contentWidth + padRight,
39
+ };
40
+ }
41
+ /**
42
+ * Every cell of a panel that is not content canvas. Read off the geometry
43
+ * rather than recomputed as `2 + padLeft + padRight`, because the two differ
44
+ * exactly where this panel is squeezed: a width that cannot afford its right
45
+ * frame column or its padding does not spend cells on them, and a measurement
46
+ * that assumed it did would report a minimum larger than its own maximum.
47
+ */
48
+ function frameOverhead(geometry) {
49
+ return geometry.left + geometry.right + geometry.padLeft + geometry.padRight;
50
+ }
51
+ /**
52
+ * The style of text set into a border: its own when one was given, the
53
+ * border's otherwise — the one rule for "what colour is the title text in".
54
+ */
55
+ function borderTextStyle(options, own, border) {
56
+ return own === undefined ? border : getStyle(options, own);
57
+ }
58
+ export class Panel {
59
+ renderable;
60
+ box;
61
+ title;
62
+ subtitle;
63
+ bottomRightAccessory;
64
+ expand;
65
+ style;
66
+ borderStyle;
67
+ titleStyle;
68
+ subtitleStyle;
69
+ width;
70
+ padding;
71
+ constructor(content, options) {
72
+ this.renderable = embed(content);
73
+ this.box = options?.box ?? ROUNDED;
74
+ this.title = options?.title;
75
+ this.subtitle = options?.subtitle;
76
+ this.bottomRightAccessory = options?.bottomRightAccessory;
77
+ this.expand = options?.expand !== false;
78
+ this.style = options?.style ?? NULL_STYLE;
79
+ this.borderStyle = options?.borderStyle ?? NULL_STYLE;
80
+ this.titleStyle = options?.titleStyle;
81
+ this.subtitleStyle = options?.subtitleStyle;
82
+ this.width = options?.width;
83
+ this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
84
+ }
85
+ *render(rawOptions) {
86
+ const options = withBoundedWidth(rawOptions, this);
87
+ const box = this.box.substitute(options);
88
+ const borderStyle = getStyle(options, this.borderStyle);
89
+ const style = getStyle(options, this.style);
90
+ const border = borderStyle.isNull ? undefined : borderStyle;
91
+ const contentStyle = style.isNull ? undefined : style;
92
+ const geometry = layoutPanel(this._getPanelWidth(options), this.padding);
93
+ const [padTop, , padBottom] = this.padding;
94
+ const contentLines = this._renderContent(options, geometry.contentWidth);
95
+ // Top border (with optional title)
96
+ yield* this._renderTopBorder(options, box, geometry, border);
97
+ // Top padding — a padding row is a content row whose content is nothing.
98
+ for (let i = 0; i < padTop; i++) {
99
+ yield* this._renderRow(box, geometry, [], border, contentStyle);
100
+ }
101
+ for (const line of contentLines) {
102
+ yield* this._renderRow(box, geometry, line, border, contentStyle);
103
+ }
104
+ for (let i = 0; i < padBottom; i++) {
105
+ yield* this._renderRow(box, geometry, [], border, contentStyle);
106
+ }
107
+ // Bottom border (with optional subtitle)
108
+ yield* this._renderBottomBorder(options, box, geometry, border);
109
+ }
110
+ /**
111
+ * The wrapped renderable's lines, laid out on a canvas `contentWidth` cells
112
+ * wide. Below width 3 a panel is all frame and the canvas holds no lines —
113
+ * but the render still happens, because a `bottomRightAccessory` thunk
114
+ * fires at every width and reads state this render populates.
115
+ */
116
+ _renderContent(options, contentWidth) {
117
+ // Two border rows and the vertical padding are the panel's own. Handed on
118
+ // as a region, the rest is the panel's to shape, which is what stretches
119
+ // its frame down a pane and keeps its bottom border inside one.
120
+ const [padTop, , padBottom] = this.padding;
121
+ const height = insetHeight(options.height, 2 + padTop + padBottom);
122
+ const innerOptions = { ...options, maxWidth: contentWidth, height };
123
+ const lines = fitHeight(Segment.splitLines([...this.renderable.render(innerOptions)]), height);
124
+ return contentWidth === 0 ? [] : lines;
125
+ }
126
+ /**
127
+ * One row of the panel body: frame column, span, frame column.
128
+ *
129
+ * [LAW:single-enforcer] `adjustLineLength` is the one place a width is
130
+ * decided here, and it runs twice against two different widths. The first
131
+ * pass crops content that rendered wider than the canvas it was handed (a
132
+ * `Table` at its natural width, say) back to the canvas, so an oversized
133
+ * child is trimmed rather than allowed to eat the right-hand padding. The
134
+ * second fills the rest of the span, which is the trailing padding and any
135
+ * shortfall in one stroke.
136
+ */
137
+ *_renderRow(box, geometry, line, border, contentStyle) {
138
+ const frame = box.getContentChars("row");
139
+ yield new Segment(frame.left.repeat(geometry.left), border);
140
+ const content = Segment.adjustLineLength(line, geometry.contentWidth, contentStyle, false);
141
+ const span = [new Segment(" ".repeat(geometry.padLeft), contentStyle), ...content];
142
+ yield* Segment.adjustLineLength(span, geometry.spanWidth, contentStyle);
143
+ yield new Segment(frame.right.repeat(geometry.right), border);
144
+ yield Segment.line();
145
+ }
146
+ measure(rawOptions) {
147
+ const options = withCellWidth(rawOptions);
148
+ const declared = this._declaredWidth;
149
+ if (declared !== undefined) {
150
+ // A declared width is not a ceiling on the content, it is the answer:
151
+ // `_getPanelWidth` returns it before measuring anything, so the panel
152
+ // draws at this width whatever the content wants. Reported with a
153
+ // content-derived floor instead, `{width: 20}` around "hi" offered 100
154
+ // cells answered 6..20 and then drew 20 every time, and a parent
155
+ // dividing space from the floor under-provisioned it.
156
+ const width = Math.min(options.maxWidth, declared);
157
+ return { minimum: width, maximum: width };
158
+ }
159
+ return this._fitRange(options);
160
+ }
161
+ /**
162
+ * The width this panel wants when nothing declared one for it: its content
163
+ * plus its own frame, both read off the division it will render against.
164
+ *
165
+ * [LAW:one-source-of-truth] `measure` and `_getPanelWidth` ask this same
166
+ * question, and each used to work it out itself — the same `layoutPanel`, the
167
+ * same `frameOverhead`, the same `Math.min(maxWidth, maximum + overhead)`,
168
+ * written twice. That is the pattern `_declaredWidth` below was extracted to
169
+ * stop, left standing for the fit case; changing how overhead is derived in
170
+ * one copy is all it would take to put `measure` and `render` back into the
171
+ * disagreement this epic spent itself closing.
172
+ */
173
+ _fitRange(options) {
174
+ const geometry = layoutPanel(options.maxWidth, this.padding);
175
+ const overhead = frameOverhead(geometry);
176
+ if (isMeasurable(this.renderable)) {
177
+ const innerOptions = {
178
+ ...options,
179
+ maxWidth: geometry.contentWidth,
180
+ };
181
+ const measurement = Measurement.get(innerOptions, this.renderable);
182
+ const maximum = Math.min(options.maxWidth, measurement.maximum + overhead);
183
+ return {
184
+ minimum: Math.min(measurement.minimum + overhead, maximum),
185
+ maximum,
186
+ };
187
+ }
188
+ // Content that cannot measure itself leaves the panel with nothing to want,
189
+ // so it wants the offer — unbounded included, which `withBoundedWidth`
190
+ // reports rather than turning into a number nobody can defend.
191
+ return { minimum: Math.min(overhead, options.maxWidth), maximum: options.maxWidth };
192
+ }
193
+ /**
194
+ * The width this panel was told to be, as a count of cells.
195
+ *
196
+ * [LAW:one-source-of-truth] `measure` and `_getPanelWidth` answer the same
197
+ * question about the same field, so they read it from here. Answered
198
+ * separately, `measure` reported nine cells of content while `render` drew the
199
+ * declared twelve, and the parent that divided space from the range got a
200
+ * panel three cells wider than the share it granted.
201
+ */
202
+ get _declaredWidth() {
203
+ return this.width === undefined ? undefined : cellCount(this.width);
204
+ }
205
+ _getPanelWidth(options) {
206
+ const declared = this._declaredWidth;
207
+ if (declared !== undefined)
208
+ return Math.min(declared, options.maxWidth);
209
+ if (this.expand)
210
+ return options.maxWidth;
211
+ return this._fitRange(options).maximum;
212
+ }
213
+ *_renderTopBorder(options, box, geometry, border) {
214
+ const innerBorderWidth = geometry.spanWidth;
215
+ const titleSeg = borderTextStyle(options, this.titleStyle, border);
216
+ const title = inlineLabel(this.title, options, titleSeg);
217
+ const titleWidth = Segment.getLineLength(title);
218
+ if (titleWidth === 0) {
219
+ yield new Segment(box.top.left.repeat(geometry.left), border);
220
+ yield new Segment(box.top.horizontal.repeat(innerBorderWidth), border);
221
+ yield new Segment(box.top.right.repeat(geometry.right), border);
222
+ yield Segment.line();
223
+ return;
224
+ }
225
+ yield new Segment(box.top.left.repeat(geometry.left), border);
226
+ if (titleWidth >= innerBorderWidth) {
227
+ // Title fills the border. [LAW:one-source-of-truth] adjustLineLength is
228
+ // cell-aware — a plain .slice would miscount wide chars and break border
229
+ // alignment.
230
+ yield* Segment.adjustLineLength(title, innerBorderWidth, titleSeg);
231
+ }
232
+ else {
233
+ // Center the title in the top border
234
+ const leftRuleWidth = Math.floor((innerBorderWidth - titleWidth) / 2);
235
+ const rightRuleWidth = innerBorderWidth - titleWidth - leftRuleWidth;
236
+ if (leftRuleWidth > 0)
237
+ yield new Segment(box.top.horizontal.repeat(leftRuleWidth), border);
238
+ yield* title;
239
+ if (rightRuleWidth > 0)
240
+ yield new Segment(box.top.horizontal.repeat(rightRuleWidth), border);
241
+ }
242
+ yield new Segment(box.top.right.repeat(geometry.right), border);
243
+ yield Segment.line();
244
+ }
245
+ *_renderBottomBorder(options, box, geometry, border) {
246
+ const innerBorderWidth = geometry.spanWidth;
247
+ // Resolve the right accessory *now*. Function form evaluates after
248
+ // content has been rendered (Panel.render collects content segments
249
+ // before yielding any borders), so the thunk sees fresh widget state.
250
+ const accessory = this._resolveAccessory(this.bottomRightAccessory);
251
+ const accessoryDisplay = accessory === undefined
252
+ ? ""
253
+ : typeof accessory === "string"
254
+ ? ` ${accessory} `
255
+ : ` ${accessory.plain} `;
256
+ const accessoryWidth = cellLen(accessoryDisplay);
257
+ const accessoryOwn = accessory instanceof RichText ? accessory.resolvedStyle(options) : NULL_STYLE;
258
+ const accessoryStyle = accessoryOwn.isNull ? border : accessoryOwn;
259
+ yield new Segment(box.bottom.left.repeat(geometry.left), border);
260
+ // Space available for the centered subtitle / rule fill — the accessory
261
+ // (if any) hugs the right edge and the subtitle treats the remainder
262
+ // as its centering canvas.
263
+ const centerWidth = Math.max(0, innerBorderWidth - accessoryWidth);
264
+ const subtitleSeg = borderTextStyle(options, this.subtitleStyle, border);
265
+ const subtitle = inlineLabel(this.subtitle, options, subtitleSeg);
266
+ const subtitleWidth = Segment.getLineLength(subtitle);
267
+ if (subtitleWidth === 0) {
268
+ if (centerWidth > 0)
269
+ yield new Segment(box.bottom.horizontal.repeat(centerWidth), border);
270
+ }
271
+ else {
272
+ if (subtitleWidth >= centerWidth) {
273
+ // Cell-aware clip — see _renderTopBorder.
274
+ yield* Segment.adjustLineLength(subtitle, centerWidth, subtitleSeg);
275
+ }
276
+ else {
277
+ const leftRuleWidth = Math.floor((centerWidth - subtitleWidth) / 2);
278
+ const rightRuleWidth = centerWidth - subtitleWidth - leftRuleWidth;
279
+ if (leftRuleWidth > 0)
280
+ yield new Segment(box.bottom.horizontal.repeat(leftRuleWidth), border);
281
+ yield* subtitle;
282
+ if (rightRuleWidth > 0)
283
+ yield new Segment(box.bottom.horizontal.repeat(rightRuleWidth), border);
284
+ }
285
+ }
286
+ if (accessoryWidth > 0) {
287
+ // Cell-aware clip — see _renderTopBorder.
288
+ const fit = accessoryWidth > innerBorderWidth
289
+ ? setCellSize(accessoryDisplay, asCellCol(innerBorderWidth))
290
+ : accessoryDisplay;
291
+ yield new Segment(fit, accessoryStyle);
292
+ }
293
+ yield new Segment(box.bottom.right.repeat(geometry.right), border);
294
+ yield Segment.line();
295
+ }
296
+ _resolveAccessory(a) {
297
+ if (a === undefined)
298
+ return undefined;
299
+ if (typeof a === "function")
300
+ return a();
301
+ return a;
302
+ }
303
+ // --- Static factory ---
304
+ static fit(content, options) {
305
+ return new Panel(content, { ...options, expand: false });
306
+ }
307
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Progress — displays continuously updated progress bars.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Console } from "../core/console.js";
6
+ import type { Renderable, RenderOptions } from "../core/protocol.js";
7
+ export interface TaskOptions {
8
+ total?: number;
9
+ start?: boolean;
10
+ visible?: boolean;
11
+ }
12
+ export interface TaskUpdateOptions {
13
+ completed?: number;
14
+ advance?: number;
15
+ description?: string;
16
+ visible?: boolean;
17
+ refresh?: boolean;
18
+ }
19
+ interface Task {
20
+ id: number;
21
+ description: string;
22
+ total: number | undefined;
23
+ completed: number;
24
+ started: boolean;
25
+ visible: boolean;
26
+ startTime: number;
27
+ elapsed: number;
28
+ }
29
+ export interface ProgressColumn extends Renderable {
30
+ render(options: RenderOptions, task?: Task): Iterable<Segment>;
31
+ }
32
+ export declare class TextColumn implements ProgressColumn {
33
+ readonly format: string;
34
+ constructor(format?: string);
35
+ render(options: RenderOptions, task?: Task): Iterable<Segment>;
36
+ }
37
+ export declare class BarColumn implements ProgressColumn {
38
+ readonly barWidth: number;
39
+ constructor(barWidth?: number);
40
+ render(_options: RenderOptions, task?: Task): Iterable<Segment>;
41
+ }
42
+ export declare class TaskProgressColumn implements ProgressColumn {
43
+ render(options: RenderOptions, task?: Task): Iterable<Segment>;
44
+ }
45
+ export declare class TimeRemainingColumn implements ProgressColumn {
46
+ render(options: RenderOptions, task?: Task): Iterable<Segment>;
47
+ }
48
+ export declare class TimeElapsedColumn implements ProgressColumn {
49
+ render(options: RenderOptions, task?: Task): Iterable<Segment>;
50
+ }
51
+ export declare class SpinnerColumn implements ProgressColumn {
52
+ private _spinner;
53
+ constructor(spinnerName?: string);
54
+ render(options: RenderOptions, _task?: Task): Iterable<Segment>;
55
+ }
56
+ export declare class MofNCompleteColumn implements ProgressColumn {
57
+ render(_options: RenderOptions, task?: Task): Iterable<Segment>;
58
+ }
59
+ export interface ProgressOptions {
60
+ refreshPerSecond?: number;
61
+ autoRefresh?: boolean;
62
+ transient?: boolean;
63
+ expand?: boolean;
64
+ console?: Console;
65
+ }
66
+ export declare class Progress implements Renderable {
67
+ private _columns;
68
+ private _tasks;
69
+ private _nextId;
70
+ private _live;
71
+ private _console;
72
+ readonly expand: boolean;
73
+ constructor(...columns: (ProgressColumn | ProgressOptions)[]);
74
+ get console(): Console;
75
+ get finished(): boolean;
76
+ static getDefaultColumns(): ProgressColumn[];
77
+ addTask(description: string, options?: TaskOptions): number;
78
+ updateTask(taskId: number, options: TaskUpdateOptions): void;
79
+ startTask(taskId: number): void;
80
+ start(): void;
81
+ stop(): void;
82
+ refresh(): void;
83
+ render(options: RenderOptions): Iterable<Segment>;
84
+ }
85
+ export declare function track<T>(iterable: Iterable<T>, options?: {
86
+ description?: string;
87
+ total?: number;
88
+ console?: Console;
89
+ }): Iterable<T>;
90
+ export {};
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Progress — displays continuously updated progress bars.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { RichText } from "../core/text.js";
6
+ import { Console } from "../core/console.js";
7
+ import { escape as escapeMarkup } from "../core/markup.js";
8
+ import { embeddedText } from "./embed.js";
9
+ import { ProgressBar } from "./progressBar.js";
10
+ import { Spinner } from "./spinner.js";
11
+ import { Live } from "./live.js";
12
+ import { Table } from "./table.js";
13
+ import { getStyle } from "../core/protocol.js";
14
+ export class TextColumn {
15
+ format;
16
+ constructor(format) {
17
+ this.format = format ?? "{task.description}";
18
+ }
19
+ *render(options, task) {
20
+ // [LAW:single-enforcer] Format strings flow through the markup parser so
21
+ // tags like `[progress.description]` become Style spans, not literal text.
22
+ // Task descriptions are escaped first to prevent injection of stray tags.
23
+ const description = escapeMarkup(task?.description ?? "");
24
+ // Use a callback so `$&`/`$1`/`$$` in the task description aren't
25
+ // reinterpreted by String.replace as replacement patterns.
26
+ const formatted = this.format.replace(/\{task\.description\}/g, () => description);
27
+ // [LAW:single-enforcer] A column is a row fragment, the same as any
28
+ // embedded text.
29
+ yield* embeddedText(formatted).render(options);
30
+ }
31
+ }
32
+ export class BarColumn {
33
+ barWidth;
34
+ constructor(barWidth) {
35
+ this.barWidth = barWidth ?? 40;
36
+ }
37
+ *render(_options, task) {
38
+ const bar = new ProgressBar({
39
+ total: task?.total ?? 100,
40
+ completed: task?.completed ?? 0,
41
+ width: this.barWidth,
42
+ });
43
+ yield* bar.render(_options);
44
+ }
45
+ }
46
+ export class TaskProgressColumn {
47
+ *render(options, task) {
48
+ const percent = task && task.total
49
+ ? Math.min(100, Math.round((task.completed / task.total) * 100))
50
+ : 0;
51
+ yield new Segment(`${percent}%`, getStyle(options, "progress.percentage"));
52
+ }
53
+ }
54
+ export class TimeRemainingColumn {
55
+ *render(options, task) {
56
+ const style = getStyle(options, "progress.remaining");
57
+ if (!task || !task.total || !task.started || task.completed <= 0) {
58
+ yield new Segment("-:--:--", style);
59
+ return;
60
+ }
61
+ const elapsed = (Date.now() - task.startTime) / 1000;
62
+ const rate = task.completed / elapsed;
63
+ const remaining = (task.total - task.completed) / rate;
64
+ yield new Segment(formatTime(remaining), style);
65
+ }
66
+ }
67
+ export class TimeElapsedColumn {
68
+ *render(options, task) {
69
+ const style = getStyle(options, "progress.elapsed");
70
+ if (!task || !task.started) {
71
+ yield new Segment("0:00:00", style);
72
+ return;
73
+ }
74
+ const elapsed = (Date.now() - task.startTime) / 1000;
75
+ yield new Segment(formatTime(elapsed), style);
76
+ }
77
+ }
78
+ export class SpinnerColumn {
79
+ _spinner;
80
+ constructor(spinnerName) {
81
+ this._spinner = new Spinner(spinnerName);
82
+ }
83
+ *render(options, _task) {
84
+ yield* this._spinner.render(options);
85
+ }
86
+ }
87
+ export class MofNCompleteColumn {
88
+ *render(_options, task) {
89
+ const completed = task?.completed ?? 0;
90
+ const total = task?.total ?? "?";
91
+ yield new Segment(`${completed}/${total}`);
92
+ }
93
+ }
94
+ function formatTime(seconds) {
95
+ const h = Math.floor(seconds / 3600);
96
+ const m = Math.floor((seconds % 3600) / 60);
97
+ const s = Math.floor(seconds % 60);
98
+ return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
99
+ }
100
+ export class Progress {
101
+ _columns;
102
+ _tasks;
103
+ _nextId;
104
+ _live;
105
+ _console;
106
+ expand;
107
+ constructor(...columns) {
108
+ // Last arg might be options
109
+ let opts = {};
110
+ const cols = [];
111
+ for (const arg of columns) {
112
+ if ("render" in arg) {
113
+ cols.push(arg);
114
+ }
115
+ else {
116
+ opts = arg;
117
+ }
118
+ }
119
+ if (cols.length === 0) {
120
+ cols.push(new TextColumn("[progress.description]{task.description}"), new BarColumn(), new TaskProgressColumn(), new TimeRemainingColumn());
121
+ }
122
+ this._columns = cols;
123
+ this._tasks = new Map();
124
+ this._nextId = 1;
125
+ this._console = opts.console ?? new Console({ forceTerminal: true });
126
+ this.expand = opts.expand ?? false;
127
+ this._live = new Live(this, {
128
+ console: this._console,
129
+ refreshPerSecond: opts.refreshPerSecond ?? 10,
130
+ autoRefresh: opts.autoRefresh,
131
+ transient: opts.transient,
132
+ });
133
+ }
134
+ get console() {
135
+ return this._console;
136
+ }
137
+ get finished() {
138
+ for (const task of this._tasks.values()) {
139
+ if (!task.started || (task.total !== undefined && task.completed < task.total)) {
140
+ return false;
141
+ }
142
+ }
143
+ return this._tasks.size > 0;
144
+ }
145
+ static getDefaultColumns() {
146
+ return [
147
+ new TextColumn("[progress.description]{task.description}"),
148
+ new BarColumn(),
149
+ new TaskProgressColumn(),
150
+ new TimeRemainingColumn(),
151
+ ];
152
+ }
153
+ addTask(description, options) {
154
+ const id = this._nextId++;
155
+ const task = {
156
+ id,
157
+ description,
158
+ total: options?.total,
159
+ completed: 0,
160
+ started: options?.start !== false,
161
+ visible: options?.visible !== false,
162
+ startTime: Date.now(),
163
+ elapsed: 0,
164
+ };
165
+ this._tasks.set(id, task);
166
+ return id;
167
+ }
168
+ updateTask(taskId, options) {
169
+ const task = this._tasks.get(taskId);
170
+ if (!task)
171
+ return;
172
+ if (options.completed !== undefined)
173
+ task.completed = options.completed;
174
+ if (options.advance !== undefined)
175
+ task.completed += options.advance;
176
+ if (options.description !== undefined)
177
+ task.description = options.description;
178
+ if (options.visible !== undefined)
179
+ task.visible = options.visible;
180
+ if (options.refresh) {
181
+ this._live.update(this, { refresh: true });
182
+ }
183
+ }
184
+ startTask(taskId) {
185
+ const task = this._tasks.get(taskId);
186
+ if (task) {
187
+ task.started = true;
188
+ task.startTime = Date.now();
189
+ }
190
+ }
191
+ start() {
192
+ this._live.start();
193
+ }
194
+ stop() {
195
+ this._live.stop();
196
+ }
197
+ refresh() {
198
+ this._live.refresh();
199
+ }
200
+ *render(options) {
201
+ const table = Table.grid({ expand: this.expand });
202
+ for (const _col of this._columns) {
203
+ table.addColumn();
204
+ }
205
+ for (const task of this._tasks.values()) {
206
+ if (!task.visible)
207
+ continue;
208
+ const cells = this._columns.map((col) => {
209
+ const segs = [...col.render(options, task)];
210
+ // Build a RichText that preserves segment styles as spans
211
+ const text = new RichText("", { end: "" });
212
+ for (const seg of segs) {
213
+ text.append(seg.text, seg.style);
214
+ }
215
+ return text;
216
+ });
217
+ table.addRow(...cells);
218
+ }
219
+ yield* table.render(options);
220
+ }
221
+ }
222
+ // --- track ---
223
+ export function* track(iterable, options) {
224
+ const items = [...iterable];
225
+ const total = options?.total ?? items.length;
226
+ const progress = new Progress({ console: options?.console });
227
+ const taskId = progress.addTask(options?.description ?? "Working...", { total });
228
+ progress.start();
229
+ try {
230
+ for (let i = 0; i < items.length; i++) {
231
+ yield items[i];
232
+ progress.updateTask(taskId, { completed: i + 1 });
233
+ }
234
+ }
235
+ finally {
236
+ progress.stop();
237
+ }
238
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ProgressBar — a visual progress bar rendered with block characters.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Style } from "../core/style.js";
6
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
+ export interface ProgressBarOptions {
8
+ total?: number;
9
+ completed?: number;
10
+ width?: number;
11
+ pulse?: boolean;
12
+ style?: string | Style;
13
+ completeStyle?: string | Style;
14
+ finishedStyle?: string | Style;
15
+ }
16
+ export declare class ProgressBar implements Renderable, Measurable {
17
+ total: number;
18
+ completed: number;
19
+ readonly width: number | undefined;
20
+ readonly pulse: boolean;
21
+ readonly style: string | Style;
22
+ readonly completeStyle: string | Style;
23
+ readonly finishedStyle: string | Style;
24
+ constructor(options?: ProgressBarOptions);
25
+ get percentComplete(): number;
26
+ render(options: RenderOptions): Iterable<Segment>;
27
+ measure(_options: RenderOptions): {
28
+ minimum: number;
29
+ maximum: number;
30
+ };
31
+ }