@promptctl/rich-js 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Padding — wraps a renderable with whitespace padding on all four sides.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { cellCount } from "../core/cells.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
+ import { Measurement } from "../core/measure.js";
8
+ import { fitHeight, getStyle, insetHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
+ import { embed } from "./embed.js";
10
+ /**
11
+ * Parses the shapes a caller may write padding in — one number, a vertical/
12
+ * horizontal pair, or all four sides — into the four-sided form every
13
+ * renderable indexes by, flooring each side at zero.
14
+ *
15
+ * [LAW:parse-dont-validate] This is the one crossing between the public
16
+ * padding vocabulary and the internal tuple, which is why the floor lives
17
+ * here and nowhere downstream: past this point `" ".repeat(left)` is safe in
18
+ * any renderable without asking what the caller passed. A negative side used
19
+ * to reach the renderers, where `Panel` drew content rows wider than its own
20
+ * border and `Table` threw outright.
21
+ */
22
+ export function normalizePadding(padding) {
23
+ // A padding side is a count of cells like any other, so it is parsed by the
24
+ // same rule rather than by a second copy of it: a fractional side reaches
25
+ // `" ".repeat`, which truncates, while the rows around it are measured
26
+ // arithmetically — so the padding and the border disagree and the frame
27
+ // breaks. `cellCount`'s header carries why the comparison, not `Math.max`.
28
+ //
29
+ // `Infinity` is where a side stops being an offer and the rule needs the one
30
+ // addition. An unbounded *offer* is a question a renderable can answer, so
31
+ // `cellCount` passes it through; an unbounded *side* is a quantity and there
32
+ // is nothing to draw. Unparsed it reached `budget -= got` as
33
+ // `Infinity - Infinity`, and `new Padding(text, Infinity).measure(...)`
34
+ // handed back `{NaN, NaN}` from a public API.
35
+ const side = (n) => (Number.isFinite(n) ? cellCount(n) : 0);
36
+ const sides = typeof padding === "number"
37
+ ? [padding, padding, padding, padding]
38
+ : padding.length === 2
39
+ ? [padding[0], padding[1], padding[0], padding[1]]
40
+ : padding;
41
+ return [side(sides[0]), side(sides[1]), side(sides[2]), side(sides[3])];
42
+ }
43
+ /**
44
+ * Divides the requested width once into the three spans a padded row is made
45
+ * of, in the order they are worth spending cells on.
46
+ *
47
+ * The three used to be derived separately and disagree where it matters:
48
+ * `render` took the canvas as `Math.max(1, maxWidth - left - right)`, which
49
+ * clamps *up*, so a padding of 1 inside a 2-cell request drew 3-cell content
50
+ * rows beside 2-cell blank rows — an open frame the terminal then soft-wraps.
51
+ * Content takes its first cell before padding takes any, for the reason
52
+ * `layoutPanel` gives: a squeeze should cost you the decoration, not the thing
53
+ * being decorated.
54
+ */
55
+ export function layoutPadding(outerWidth, leftWanted, rightWanted) {
56
+ let budget = cellCount(outerWidth);
57
+ const take = (want) => {
58
+ const got = Math.min(want, budget);
59
+ budget -= got;
60
+ return got;
61
+ };
62
+ const firstContentCell = take(1);
63
+ const left = take(leftWanted);
64
+ const right = take(rightWanted);
65
+ return { left, contentWidth: firstContentCell + budget, right };
66
+ }
67
+ /** The full width of a padded row — the division, added back up. */
68
+ function rowWidth(geometry) {
69
+ return geometry.left + geometry.contentWidth + geometry.right;
70
+ }
71
+ export class Padding {
72
+ renderable;
73
+ top;
74
+ right;
75
+ bottom;
76
+ left;
77
+ style;
78
+ expand;
79
+ constructor(content, padding, options) {
80
+ const [top, right, bottom, left] = normalizePadding(padding);
81
+ this.renderable = embed(content);
82
+ this.top = top;
83
+ this.right = right;
84
+ this.bottom = bottom;
85
+ this.left = left;
86
+ this.style = options?.style ?? NULL_STYLE;
87
+ this.expand = options?.expand !== false;
88
+ }
89
+ *render(rawOptions) {
90
+ const options = withBoundedWidth(rawOptions, this);
91
+ // `expand` decides how wide the block is, never whether its rows reach
92
+ // that width: as Rich's does, a padding that does not expand narrows to
93
+ // what it measures, and every row is then padded out to it. Deciding the
94
+ // rows instead left the content rows ragged under a full-width blank row,
95
+ // and a `style` ground with holes down its right-hand side.
96
+ const width = this.expand ? options.maxWidth : Measurement.get(options, this).maximum;
97
+ const geometry = layoutPadding(width, this.left, this.right);
98
+ const innerOptions = {
99
+ ...options,
100
+ maxWidth: geometry.contentWidth,
101
+ height: insetHeight(options.height, this.top + this.bottom),
102
+ };
103
+ const resolved = getStyle(options, this.style);
104
+ const style = resolved.isNull ? undefined : resolved;
105
+ // The style is the ground the content is drawn on, not only the spaces
106
+ // around it — Rich's `render_lines(..., style=style)`. Applied to the
107
+ // padding alone, a background left a hole behind the content.
108
+ const segments = [...Segment.applyStyle(this.renderable.render(innerOptions), style)];
109
+ const lines = fitHeight(Segment.splitLines(segments), innerOptions.height);
110
+ // Zero-length spans need no branch to suppress: the wire boundary drops
111
+ // empty segments, so a padding the width could not afford emits nothing.
112
+ const leftPad = new Segment(" ".repeat(geometry.left), style);
113
+ const rightPad = new Segment(" ".repeat(geometry.right), style);
114
+ const blankLine = new Segment(" ".repeat(rowWidth(geometry)), style);
115
+ for (let i = 0; i < this.top; i++) {
116
+ yield blankLine;
117
+ yield Segment.line();
118
+ }
119
+ for (const line of lines) {
120
+ yield leftPad;
121
+ // Crops as well as pads: a child that ignored the canvas it was handed
122
+ // (a Table at its natural width) would otherwise burst the padding.
123
+ yield* Segment.adjustLineLength(line, geometry.contentWidth, style);
124
+ yield rightPad;
125
+ yield Segment.line();
126
+ }
127
+ for (let i = 0; i < this.bottom; i++) {
128
+ yield blankLine;
129
+ yield Segment.line();
130
+ }
131
+ }
132
+ measure(rawOptions) {
133
+ const options = withCellWidth(rawOptions);
134
+ const geometry = layoutPadding(options.maxWidth, this.left, this.right);
135
+ const overhead = geometry.left + geometry.right;
136
+ const measurement = Measurement.get({ ...options, maxWidth: geometry.contentWidth }, this.renderable);
137
+ // The ceiling is what was offered, and it wins over what the child asks
138
+ // for: a range whose floor sits above its own ceiling — this returned
139
+ // {8, 8} for three cells of padding inside a two-cell offer — is one no
140
+ // parent layout can divide.
141
+ const maximum = Math.min(options.maxWidth, measurement.maximum + overhead);
142
+ return {
143
+ minimum: Math.min(measurement.minimum + overhead, maximum),
144
+ maximum,
145
+ };
146
+ }
147
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Panel — a bordered box that wraps content, with optional title and subtitle.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Style } from "../core/style.js";
6
+ import { Box } from "../core/box.js";
7
+ import { RichText } from "../core/text.js";
8
+ import type { PaddingDimensions } from "./padding.js";
9
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
10
+ /**
11
+ * A lazily-resolved border accessory. Strings render inline in the
12
+ * border style; a `RichText` accessory lays ONLY its wrapping `style` over
13
+ * the border's (per-range spans within the RichText are not preserved — the
14
+ * accessory is a small status indicator, not an arbitrary span carrier).
15
+ * A function form is evaluated at render time, *after* content has been
16
+ * rendered for the current frame — use this when the accessory mirrors
17
+ * state that the wrapped renderable populates during its own `render()`
18
+ * (e.g. a widget's post-render scroll position).
19
+ */
20
+ export type BorderAccessory = string | RichText | (() => string | RichText | undefined);
21
+ export interface PanelOptions {
22
+ box?: Box;
23
+ title?: string | RichText;
24
+ subtitle?: string | RichText;
25
+ /**
26
+ * Right-aligned accessory in the bottom border, just left of the
27
+ * `bottomRight` corner. Coexists with `subtitle` — the subtitle
28
+ * remains centered in the remaining space. Padded with a leading/
29
+ * trailing space like `title`/`subtitle`, so passing `"[14/102]"`
30
+ * renders as `─ [14/102] ┘`.
31
+ */
32
+ bottomRightAccessory?: BorderAccessory;
33
+ expand?: boolean;
34
+ style?: string | Style;
35
+ borderStyle?: string | Style;
36
+ /**
37
+ * Style for the title text in the top border, laid over `borderStyle`:
38
+ * what it sets wins, and what it leaves unset the border's supplies.
39
+ */
40
+ titleStyle?: string | Style;
41
+ /**
42
+ * Style for the subtitle text in the bottom border, laid over
43
+ * `borderStyle` as `titleStyle` is.
44
+ */
45
+ subtitleStyle?: string | Style;
46
+ width?: number;
47
+ padding?: PaddingDimensions;
48
+ }
49
+ export declare class Panel implements Renderable, Measurable {
50
+ readonly renderable: Renderable;
51
+ readonly box: Box;
52
+ readonly title: string | RichText | undefined;
53
+ readonly subtitle: string | RichText | undefined;
54
+ readonly bottomRightAccessory: BorderAccessory | undefined;
55
+ readonly expand: boolean;
56
+ readonly style: string | Style;
57
+ readonly borderStyle: string | Style;
58
+ readonly titleStyle: string | Style | undefined;
59
+ readonly subtitleStyle: string | Style | undefined;
60
+ readonly width: number | undefined;
61
+ readonly padding: [number, number, number, number];
62
+ private readonly _titleLabel;
63
+ private readonly _subtitleLabel;
64
+ constructor(content: string | RichText | Renderable, options?: PanelOptions);
65
+ render(rawOptions: RenderOptions): Iterable<Segment>;
66
+ /**
67
+ * The wrapped renderable's lines, laid out on a canvas `contentWidth` cells
68
+ * wide. Below width 3 a panel is all frame and the canvas holds no lines —
69
+ * but the render still happens, because a `bottomRightAccessory` thunk
70
+ * fires at every width and reads state this render populates.
71
+ */
72
+ private _renderContent;
73
+ /**
74
+ * One row of the panel body, cut where Rich cuts it: frame column, left
75
+ * padding, the content line made exactly `contentWidth` wide, right padding,
76
+ * frame column — each its own segment, so each its own SGR run.
77
+ *
78
+ * [LAW:single-enforcer] `adjustLineLength` is the one place the content's
79
+ * width is decided: content that rendered wider than its canvas (a `Table`
80
+ * at its natural width, say) is cropped back to it, and a short line is
81
+ * filled out to it, so the right padding always starts at the same column.
82
+ */
83
+ private _renderRow;
84
+ /**
85
+ * A row of vertical padding: one blank run across the whole span, as Rich's
86
+ * `Padding` draws its blank lines, not a content row with nothing in it.
87
+ */
88
+ private _renderPaddingRow;
89
+ measure(rawOptions: RenderOptions): {
90
+ minimum: number;
91
+ maximum: number;
92
+ };
93
+ /** The content plus its own frame, both read off the division it will render against. */
94
+ private _contentRange;
95
+ /** The cells the title's label takes, its padding included; none without a title. */
96
+ private _labelWidth;
97
+ /**
98
+ * The width this panel was told to be, as a count of cells.
99
+ *
100
+ * [LAW:one-source-of-truth] `measure` and `_getPanelWidth` read the field
101
+ * from here, so neither can count it differently from the other.
102
+ */
103
+ private get _declaredWidth();
104
+ /**
105
+ * As Rich's `__rich_console__` sizes it: a declared width is a ceiling that
106
+ * `expand` fills and a fitted panel fits its content inside, and the title
107
+ * widens either.
108
+ */
109
+ private _getPanelWidth;
110
+ private _renderTopBorder;
111
+ private _renderBottomBorder;
112
+ private _resolveAccessory;
113
+ static fit(content: string | RichText | Renderable, options?: Omit<PanelOptions, "expand">): Panel;
114
+ }
@@ -0,0 +1,303 @@
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 { cutLabel, drawLabel, embed, inlineLabel } from "./embed.js";
11
+ import { normalizePadding } from "./padding.js";
12
+ import { drawable, fitHeight, getStyle, insetHeight, 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
+ * How a title or subtitle string is read: always as markup and never
53
+ * highlighted, whatever the console says, because Rich's `Panel` reads its
54
+ * title with `Text.from_markup` rather than through the console.
55
+ */
56
+ function borderLabel(options) {
57
+ return { ...options, markup: true, highlight: false };
58
+ }
59
+ /** A title or subtitle as Rich's `Panel._title` sets it: read as a border reads it, a space either side. */
60
+ function borderTitle(label, options) {
61
+ return label.text(borderLabel(options)).pad(1);
62
+ }
63
+ /**
64
+ * The style of text set into a border — title, subtitle and accessory alike:
65
+ * its own laid over the border's, the one rule for "what colour is the title
66
+ * text in". Laid over rather than in place of it, as Rich's `stylize_before`
67
+ * does, because the border's style carries the panel's own: a title that
68
+ * replaced it cut a hole in the panel's background on the top row.
69
+ */
70
+ function borderTextStyle(border, own) {
71
+ return own.isNull ? border : (border ?? NULL_STYLE).add(own);
72
+ }
73
+ /**
74
+ * A border row: the opening corner, the rule across `span` cells with `label`
75
+ * centred in it, and `close` — the closing corner, or nothing when the caller
76
+ * sets something between the rule and its corner.
77
+ *
78
+ * Cut as Rich cuts it. An unlabelled rule is one run, corners included. A
79
+ * label is set into the `span - 2` cells between one rule cell either side,
80
+ * truncated to them as Rich's `align_text` truncates it — marked only when the
81
+ * label's own overflow is `"ellipsis"` — and centred with the rule filling the
82
+ * rest, so the row leaves as corner-and-rule, rule, label, rule,
83
+ * rule-and-corner. Rich draws no label in a span of two cells or fewer
84
+ * (`width <= 4`), where it would sit on the corners.
85
+ */
86
+ function borderRow(options, edge, geometry, span, label, labelStyle, border, close) {
87
+ const corner = edge.left.repeat(geometry.left);
88
+ if (label === undefined || span <= 2) {
89
+ return [new Segment(corner + edge.horizontal.repeat(span) + close, border)];
90
+ }
91
+ const canvas = span - 2;
92
+ const text = borderTitle(label, options);
93
+ cutLabel(text, canvas, text.overflow === "ellipsis" ? drawable(options, "\u2026", ".") : "");
94
+ const fitted = drawLabel(text, borderLabel(options), labelStyle);
95
+ const excess = canvas - Segment.getLineLength(fitted);
96
+ const before = Math.floor(excess / 2);
97
+ return [
98
+ new Segment(corner + edge.horizontal, border),
99
+ new Segment(edge.horizontal.repeat(before), border),
100
+ ...fitted,
101
+ new Segment(edge.horizontal.repeat(excess - before), border),
102
+ new Segment(edge.horizontal + close, border),
103
+ ];
104
+ }
105
+ export class Panel {
106
+ renderable;
107
+ box;
108
+ title;
109
+ subtitle;
110
+ bottomRightAccessory;
111
+ expand;
112
+ style;
113
+ borderStyle;
114
+ titleStyle;
115
+ subtitleStyle;
116
+ width;
117
+ padding;
118
+ _titleLabel;
119
+ _subtitleLabel;
120
+ constructor(content, options) {
121
+ this.renderable = embed(content);
122
+ this.box = options?.box ?? ROUNDED;
123
+ this.title = options?.title;
124
+ this.subtitle = options?.subtitle;
125
+ this._titleLabel = inlineLabel(this.title);
126
+ this._subtitleLabel = inlineLabel(this.subtitle);
127
+ this.bottomRightAccessory = options?.bottomRightAccessory;
128
+ this.expand = options?.expand !== false;
129
+ this.style = options?.style ?? NULL_STYLE;
130
+ this.borderStyle = options?.borderStyle ?? NULL_STYLE;
131
+ this.titleStyle = options?.titleStyle;
132
+ this.subtitleStyle = options?.subtitleStyle;
133
+ this.width = options?.width;
134
+ this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
135
+ }
136
+ *render(rawOptions) {
137
+ const options = withBoundedWidth(rawOptions, this);
138
+ const box = this.box.substitute(options);
139
+ // [LAW:one-source-of-truth] `style` is the ground the whole panel is drawn
140
+ // on, as Rich's is: the frame's style is the border style laid over it,
141
+ // and the content is rendered on top of it. Applied to the padding alone,
142
+ // "white on dark_blue" drew a blue ring with a hole where the text was.
143
+ const style = getStyle(options, this.style);
144
+ const borderStyle = style.add(getStyle(options, this.borderStyle));
145
+ const border = borderStyle.isNull ? undefined : borderStyle;
146
+ const contentStyle = style.isNull ? undefined : style;
147
+ const geometry = layoutPanel(this._getPanelWidth(options), this.padding);
148
+ const [padTop, , padBottom] = this.padding;
149
+ const contentLines = this._renderContent(options, geometry.contentWidth, contentStyle);
150
+ yield* this._renderTopBorder(options, box, geometry, border);
151
+ for (let i = 0; i < padTop; i++) {
152
+ yield* this._renderPaddingRow(box, geometry, border, contentStyle);
153
+ }
154
+ for (const line of contentLines) {
155
+ yield* this._renderRow(box, geometry, line, border, contentStyle);
156
+ }
157
+ for (let i = 0; i < padBottom; i++) {
158
+ yield* this._renderPaddingRow(box, geometry, border, contentStyle);
159
+ }
160
+ yield* this._renderBottomBorder(options, box, geometry, border);
161
+ }
162
+ /**
163
+ * The wrapped renderable's lines, laid out on a canvas `contentWidth` cells
164
+ * wide. Below width 3 a panel is all frame and the canvas holds no lines —
165
+ * but the render still happens, because a `bottomRightAccessory` thunk
166
+ * fires at every width and reads state this render populates.
167
+ */
168
+ _renderContent(options, contentWidth, style) {
169
+ // Two border rows and the vertical padding are the panel's own. Handed on
170
+ // as a region, the rest is the panel's to shape, which is what stretches
171
+ // its frame down a pane and keeps its bottom border inside one.
172
+ const [padTop, , padBottom] = this.padding;
173
+ const height = insetHeight(options.height, 2 + padTop + padBottom);
174
+ // No highlighter: what the panel holds is drawn plain, as Rich's `Panel`
175
+ // hands its body `highlight=False`.
176
+ const innerOptions = { ...options, highlight: false, maxWidth: contentWidth, height };
177
+ const lines = fitHeight(Segment.splitLines([...Segment.applyStyle(this.renderable.render(innerOptions), style)]), height);
178
+ return contentWidth === 0 ? [] : lines;
179
+ }
180
+ /**
181
+ * One row of the panel body, cut where Rich cuts it: frame column, left
182
+ * padding, the content line made exactly `contentWidth` wide, right padding,
183
+ * frame column — each its own segment, so each its own SGR run.
184
+ *
185
+ * [LAW:single-enforcer] `adjustLineLength` is the one place the content's
186
+ * width is decided: content that rendered wider than its canvas (a `Table`
187
+ * at its natural width, say) is cropped back to it, and a short line is
188
+ * filled out to it, so the right padding always starts at the same column.
189
+ */
190
+ *_renderRow(box, geometry, line, border, contentStyle) {
191
+ const frame = box.getContentChars("row");
192
+ yield new Segment(frame.left.repeat(geometry.left), border);
193
+ yield new Segment(" ".repeat(geometry.padLeft), contentStyle);
194
+ yield* Segment.adjustLineLength(line, geometry.contentWidth, contentStyle);
195
+ yield new Segment(" ".repeat(geometry.padRight), contentStyle);
196
+ yield new Segment(frame.right.repeat(geometry.right), border);
197
+ yield Segment.line();
198
+ }
199
+ /**
200
+ * A row of vertical padding: one blank run across the whole span, as Rich's
201
+ * `Padding` draws its blank lines, not a content row with nothing in it.
202
+ */
203
+ *_renderPaddingRow(box, geometry, border, contentStyle) {
204
+ const frame = box.getContentChars("row");
205
+ yield new Segment(frame.left.repeat(geometry.left), border);
206
+ yield new Segment(" ".repeat(geometry.spanWidth), contentStyle);
207
+ yield new Segment(frame.right.repeat(geometry.right), border);
208
+ yield Segment.line();
209
+ }
210
+ measure(rawOptions) {
211
+ const options = withCellWidth(rawOptions);
212
+ // Rich's `__rich_measure__`, which is not its render's arithmetic: a
213
+ // declared width is the measurement whatever the panel then draws inside
214
+ // it, and a title counts as content beside the body, with the padding and
215
+ // frame around it, where the render holds it between one rule cell either
216
+ // side. A parent sizing from it sizes as Rich's parent does.
217
+ const declared = this._declaredWidth;
218
+ if (declared !== undefined) {
219
+ const width = Math.min(options.maxWidth, declared);
220
+ return { minimum: width, maximum: width };
221
+ }
222
+ const content = this._contentRange(options);
223
+ const overhead = frameOverhead(layoutPanel(options.maxWidth, this.padding));
224
+ const maximum = Math.min(options.maxWidth, Math.max(content.maximum, this._labelWidth(options) + overhead));
225
+ return { minimum: Math.min(content.minimum, maximum), maximum };
226
+ }
227
+ /** The content plus its own frame, both read off the division it will render against. */
228
+ _contentRange(options) {
229
+ const geometry = layoutPanel(options.maxWidth, this.padding);
230
+ const overhead = frameOverhead(geometry);
231
+ const measurement = Measurement.get({ ...options, maxWidth: geometry.contentWidth }, this.renderable);
232
+ return {
233
+ minimum: measurement.minimum + overhead,
234
+ maximum: Math.min(options.maxWidth, measurement.maximum + overhead),
235
+ };
236
+ }
237
+ /** The cells the title's label takes, its padding included; none without a title. */
238
+ _labelWidth(options) {
239
+ return this._titleLabel === undefined ? 0 : borderTitle(this._titleLabel, options).cellLength;
240
+ }
241
+ /**
242
+ * The width this panel was told to be, as a count of cells.
243
+ *
244
+ * [LAW:one-source-of-truth] `measure` and `_getPanelWidth` read the field
245
+ * from here, so neither can count it differently from the other.
246
+ */
247
+ get _declaredWidth() {
248
+ return this.width === undefined ? undefined : cellCount(this.width);
249
+ }
250
+ /**
251
+ * As Rich's `__rich_console__` sizes it: a declared width is a ceiling that
252
+ * `expand` fills and a fitted panel fits its content inside, and the title
253
+ * widens either.
254
+ */
255
+ _getPanelWidth(options) {
256
+ const ceiling = Math.min(options.maxWidth, this._declaredWidth ?? options.maxWidth);
257
+ const body = this.expand ? ceiling : this._contentRange({ ...options, maxWidth: ceiling }).maximum;
258
+ // The title holds its label whole between one rule cell either side —
259
+ // past a declared width too, up to the width the panel was offered.
260
+ const floor = this._titleLabel === undefined ? 0 : this._labelWidth(options) + 4;
261
+ return Math.min(options.maxWidth, Math.max(body, floor));
262
+ }
263
+ *_renderTopBorder(options, box, geometry, border) {
264
+ const titleStyle = borderTextStyle(border, getStyle(options, this.titleStyle ?? NULL_STYLE));
265
+ yield* borderRow(options, box.top, geometry, geometry.spanWidth, this._titleLabel, titleStyle, border, box.top.right.repeat(geometry.right));
266
+ yield Segment.line();
267
+ }
268
+ *_renderBottomBorder(options, box, geometry, border) {
269
+ // Resolve the right accessory *now*. Function form evaluates after
270
+ // content has been rendered (Panel.render collects content segments
271
+ // before yielding any borders), so the thunk sees fresh widget state.
272
+ const accessory = this._resolveAccessory(this.bottomRightAccessory);
273
+ const accessoryDisplay = accessory === undefined
274
+ ? ""
275
+ : typeof accessory === "string"
276
+ ? ` ${accessory} `
277
+ : ` ${accessory.plain} `;
278
+ // Cell-aware clip: the accessory never claims more than the span.
279
+ const accessoryText = setCellSize(accessoryDisplay, asCellCol(Math.min(cellLen(accessoryDisplay), geometry.spanWidth)));
280
+ const accessoryOwn = accessory instanceof RichText ? accessory.resolvedStyle(options) : NULL_STYLE;
281
+ const subtitleStyle = borderTextStyle(border, getStyle(options, this.subtitleStyle ?? NULL_STYLE));
282
+ // The accessory hugs the bottom-right corner; the rule and subtitle are
283
+ // laid out, as Rich lays them, across the span it leaves. With no
284
+ // accessory the corner closes the rule's last run, as Rich's does.
285
+ const corner = box.bottom.right.repeat(geometry.right);
286
+ const [close, afterAccessory] = accessoryText === "" ? [corner, ""] : ["", corner];
287
+ yield* borderRow(options, box.bottom, geometry, geometry.spanWidth - cellLen(accessoryText), this._subtitleLabel, subtitleStyle, border, close);
288
+ yield new Segment(accessoryText, borderTextStyle(border, accessoryOwn));
289
+ yield new Segment(afterAccessory, border);
290
+ yield Segment.line();
291
+ }
292
+ _resolveAccessory(a) {
293
+ if (a === undefined)
294
+ return undefined;
295
+ if (typeof a === "function")
296
+ return a();
297
+ return a;
298
+ }
299
+ // --- Static factory ---
300
+ static fit(content, options) {
301
+ return new Panel(content, { ...options, expand: false });
302
+ }
303
+ }