@promptctl/rich-js 0.24.0 → 0.25.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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -5,7 +5,7 @@ import { Segment } from "../core/segment.js";
5
5
  import { Style } from "../core/style.js";
6
6
  import type { RichText } from "../core/text.js";
7
7
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
- export type PaddingDimensions = number | [number, number] | [number, number, number, number];
8
+ export type PaddingDimensions = number | readonly [number, number] | readonly [number, number, number, number];
9
9
  /**
10
10
  * Parses the shapes a caller may write padding in — one number, a vertical/
11
11
  * horizontal pair, or all four sides — into the four-sided form every
@@ -18,26 +18,14 @@ export type PaddingDimensions = number | [number, number] | [number, number, num
18
18
  * to reach the renderers, where `Panel` drew content rows wider than its own
19
19
  * border and `Table` threw outright.
20
20
  */
21
- export declare function normalizePadding(padding: PaddingDimensions): [number, number, number, number];
22
- /** The three spans of a padded row, summing to exactly the requested width. */
23
- interface PaddingGeometry {
24
- readonly left: number;
25
- readonly contentWidth: number;
26
- readonly right: number;
27
- }
21
+ export declare function normalizePadding(padding: PaddingDimensions): readonly [number, number, number, number];
28
22
  /**
29
- * Divides the requested width once into the three spans a padded row is made
30
- * of, in the order they are worth spending cells on.
31
- *
32
- * The three used to be derived separately and disagree where it matters:
33
- * `render` took the canvas as `Math.max(1, maxWidth - left - right)`, which
34
- * clamps *up*, so a padding of 1 inside a 2-cell request drew 3-cell content
35
- * rows beside 2-cell blank rows — an open frame the terminal then soft-wraps.
36
- * Content takes its first cell before padding takes any, for the reason
37
- * `layoutPanel` gives: a squeeze should cost you the decoration, not the thing
38
- * being decorated.
23
+ * The width a padding leaves its content: what the request holds once both
24
+ * sides are paid for, as Rich's `Padding` divides it. The sides are paid in
25
+ * full, so a request too narrow for them leaves the content no column, and it
26
+ * is drawn as nothing — Rich's `Console.render` under one column.
39
27
  */
40
- export declare function layoutPadding(outerWidth: number, leftWanted: number, rightWanted: number): PaddingGeometry;
28
+ export declare function paddedContentWidth(outerWidth: number, left: number, right: number): number;
41
29
  export declare class Padding implements Renderable, Measurable {
42
30
  readonly renderable: Renderable;
43
31
  readonly top: number;
@@ -56,4 +44,3 @@ export declare class Padding implements Renderable, Measurable {
56
44
  maximum: number;
57
45
  };
58
46
  }
59
- export {};
@@ -5,7 +5,7 @@ import { Segment } from "../core/segment.js";
5
5
  import { cellCount } from "../core/cells.js";
6
6
  import { NULL_STYLE } from "../core/style.js";
7
7
  import { Measurement } from "../core/measure.js";
8
- import { fitHeight, getStyle, insetHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
8
+ import { fitHeight, getStyle, insetHeight, render, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
9
  import { embed } from "./embed.js";
10
10
  /**
11
11
  * Parses the shapes a caller may write padding in — one number, a vertical/
@@ -41,32 +41,13 @@ export function normalizePadding(padding) {
41
41
  return [side(sides[0]), side(sides[1]), side(sides[2]), side(sides[3])];
42
42
  }
43
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.
44
+ * The width a padding leaves its content: what the request holds once both
45
+ * sides are paid for, as Rich's `Padding` divides it. The sides are paid in
46
+ * full, so a request too narrow for them leaves the content no column, and it
47
+ * is drawn as nothing — Rich's `Console.render` under one column.
54
48
  */
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;
49
+ export function paddedContentWidth(outerWidth, left, right) {
50
+ return cellCount(cellCount(outerWidth) - left - right);
70
51
  }
71
52
  export class Padding {
72
53
  renderable;
@@ -94,10 +75,10 @@ export class Padding {
94
75
  // rows instead left the content rows ragged under a full-width blank row,
95
76
  // and a `style` ground with holes down its right-hand side.
96
77
  const width = this.expand ? options.maxWidth : Measurement.get(options, this).maximum;
97
- const geometry = layoutPadding(width, this.left, this.right);
78
+ const contentWidth = paddedContentWidth(width, this.left, this.right);
98
79
  const innerOptions = {
99
80
  ...options,
100
- maxWidth: geometry.contentWidth,
81
+ maxWidth: contentWidth,
101
82
  height: insetHeight(options.height, this.top + this.bottom),
102
83
  };
103
84
  const resolved = getStyle(options, this.style);
@@ -105,35 +86,33 @@ export class Padding {
105
86
  // The style is the ground the content is drawn on, not only the spaces
106
87
  // around it — Rich's `render_lines(..., style=style)`. Applied to the
107
88
  // 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
- }
89
+ const drawn = Segment.splitLines(Segment.applyStyle(render(this.renderable, innerOptions), style));
90
+ const lines = fitHeight(drawn, innerOptions.height);
91
+ const leftPad = new Segment(" ".repeat(this.left), style);
92
+ const rightPad = new Segment(" ".repeat(this.right), style);
93
+ for (let i = 0; i < this.top; i++)
94
+ yield* row([]);
95
+ // Every row is the block's width: content rows pad and crop the content to
96
+ // its canvas — a child that ignored the canvas it was handed (a Table at
97
+ // its natural width) would otherwise burst the padding — and the row to
98
+ // the block, so the rows a region asks for under sides the width cannot
99
+ // hold are cut at its edge rather than drawn past it.
119
100
  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();
101
+ yield* row([leftPad, ...Segment.adjustLineLength(line, contentWidth, style), rightPad]);
126
102
  }
127
- for (let i = 0; i < this.bottom; i++) {
128
- yield blankLine;
103
+ for (let i = 0; i < this.bottom; i++)
104
+ yield* row([]);
105
+ function* row(cells) {
106
+ yield* Segment.adjustLineLength(cells, width, style);
129
107
  yield Segment.line();
130
108
  }
131
109
  }
132
110
  measure(rawOptions) {
133
111
  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);
112
+ const overhead = this.left + this.right;
113
+ // Content left no column measures as nothing, so the padding asks for the
114
+ // whole offer — Rich's `Padding.__rich_measure__`.
115
+ const measurement = Measurement.get({ ...options, maxWidth: paddedContentWidth(options.maxWidth, this.left, this.right) }, this.renderable);
137
116
  // The ceiling is what was offered, and it wins over what the child asks
138
117
  // for: a range whose floor sits above its own ceiling — this returned
139
118
  // {8, 8} for three cells of padding inside a two-cell offer — is one no
@@ -8,16 +8,18 @@ import { RichText } from "../core/text.js";
8
8
  import type { PaddingDimensions } from "./padding.js";
9
9
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
10
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).
11
+ * A border accessory. Strings render inline in the border style; a
12
+ * `RichText` accessory lays ONLY its wrapping `style` over the border's
13
+ * (per-range spans within the RichText are not preserved — the accessory is a
14
+ * small status indicator, not an arbitrary span carrier).
15
+ *
16
+ * [LAW:no-ambient-temporal-coupling] A function form is handed the rows the
17
+ * body drew this frame, so an accessory that mirrors the content's layout (a
18
+ * widget's scroll position) reads it off the frame it labels, not off state
19
+ * the content kept from an earlier one. A width that leaves the content no column does not render it,
20
+ * as Rich's does not, and the rows it is handed are then blank.
19
21
  */
20
- export type BorderAccessory = string | RichText | (() => string | RichText | undefined);
22
+ export type BorderAccessory = string | RichText | ((body: readonly (readonly Segment[])[]) => string | RichText | undefined);
21
23
  export interface PanelOptions {
22
24
  box?: Box;
23
25
  title?: string | RichText;
@@ -58,39 +60,35 @@ export declare class Panel implements Renderable, Measurable {
58
60
  readonly titleStyle: string | Style | undefined;
59
61
  readonly subtitleStyle: string | Style | undefined;
60
62
  readonly width: number | undefined;
61
- readonly padding: [number, number, number, number];
62
63
  private readonly _titleLabel;
63
64
  private readonly _subtitleLabel;
64
- constructor(content: string | RichText | Renderable, options?: PanelOptions);
65
- render(rawOptions: RenderOptions): Iterable<Segment>;
66
65
  /**
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.
66
+ * What the frame is drawn around: the content inside its padding, on the
67
+ * panel's ground. Rich's `Panel` draws its body as this same `Padding`, so
68
+ * how a squeezed width divides between padding and content is answered once,
69
+ * by `Padding`, for both.
71
70
  */
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;
71
+ private readonly _body;
72
+ /** The padding inside the frame, top, right, bottom, left — the body's, read back. */
73
+ get padding(): readonly [number, number, number, number];
74
+ constructor(content: string | RichText | Renderable, options?: PanelOptions);
75
+ render(rawOptions: RenderOptions): Iterable<Segment>;
84
76
  /**
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.
77
+ * The body's rows, held to the region as Rich's `render_lines` holds them —
78
+ * padded below, cropped from the bottom — and each exactly `spanWidth`
79
+ * cells: `Padding` makes every row its block's width, cropping content that
80
+ * ignored its canvas. A span of no cells renders nothing, as Rich's
81
+ * `Console.render` renders nothing under one column, so outside a region it
82
+ * draws no rows, padding rows included, and inside one the frame stands
83
+ * around empty rows. The render happens before either border is drawn,
84
+ * because a `bottomRightAccessory` function is handed the rows it drew.
87
85
  */
88
- private _renderPaddingRow;
86
+ private _renderBody;
89
87
  measure(rawOptions: RenderOptions): {
90
88
  minimum: number;
91
89
  maximum: number;
92
90
  };
93
- /** The content plus its own frame, both read off the division it will render against. */
91
+ /** The body plus the frame around it, both read off the division it will render against. */
94
92
  private _contentRange;
95
93
  /** The cells the title's label takes, its padding included; none without a title. */
96
94
  private _labelWidth;
@@ -8,45 +8,22 @@ import { ROUNDED } from "../core/box.js";
8
8
  import { Measurement } from "../core/measure.js";
9
9
  import { RichText } from "../core/text.js";
10
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;
11
+ import { Padding } from "./padding.js";
12
+ import { drawable, fitHeight, getStyle, insetHeight, render, withBoundedWidth, withCellWidth } from "../core/protocol.js";
13
+ function layoutPanel(outerWidth) {
15
14
  // Panel's one division point, and the width reaching it is not always one
16
15
  // this file produced: `_getPanelWidth` derives the fit-mode width from
17
16
  // `Measurement.get` on an arbitrary `Measurable`, so a renderable reporting a
18
17
  // negative maximum arrives here unparsed and reaches `repeat` as a negative
19
18
  // 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
- };
19
+ const width = cellCount(outerWidth);
20
+ const left = Math.min(1, width);
21
+ const right = Math.min(1, width - left);
22
+ return { left, right, spanWidth: width - left - right };
40
23
  }
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;
24
+ /** The cells a panel's frame takes from the width it is given. */
25
+ function frameWidth(geometry) {
26
+ return geometry.left + geometry.right;
50
27
  }
51
28
  /**
52
29
  * How a title or subtitle string is read: always as markup and never
@@ -114,9 +91,20 @@ export class Panel {
114
91
  titleStyle;
115
92
  subtitleStyle;
116
93
  width;
117
- padding;
118
94
  _titleLabel;
119
95
  _subtitleLabel;
96
+ /**
97
+ * What the frame is drawn around: the content inside its padding, on the
98
+ * panel's ground. Rich's `Panel` draws its body as this same `Padding`, so
99
+ * how a squeezed width divides between padding and content is answered once,
100
+ * by `Padding`, for both.
101
+ */
102
+ _body;
103
+ /** The padding inside the frame, top, right, bottom, left — the body's, read back. */
104
+ get padding() {
105
+ const body = this._body;
106
+ return [body.top, body.right, body.bottom, body.left];
107
+ }
120
108
  constructor(content, options) {
121
109
  this.renderable = embed(content);
122
110
  this.box = options?.box ?? ROUNDED;
@@ -131,7 +119,7 @@ export class Panel {
131
119
  this.titleStyle = options?.titleStyle;
132
120
  this.subtitleStyle = options?.subtitleStyle;
133
121
  this.width = options?.width;
134
- this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
122
+ this._body = new Padding(this.renderable, options?.padding ?? [0, 1, 0, 1], { style: this.style });
135
123
  }
136
124
  *render(rawOptions) {
137
125
  const options = withBoundedWidth(rawOptions, this);
@@ -140,72 +128,44 @@ export class Panel {
140
128
  // on, as Rich's is: the frame's style is the border style laid over it,
141
129
  // and the content is rendered on top of it. Applied to the padding alone,
142
130
  // "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));
131
+ const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
145
132
  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);
133
+ const geometry = layoutPanel(this._getPanelWidth(options));
134
+ const bodyLines = this._renderBody(options, geometry.spanWidth);
150
135
  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
136
  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();
137
+ for (const line of bodyLines) {
138
+ yield new Segment(frame.left.repeat(geometry.left), border);
139
+ yield* line;
140
+ yield new Segment(frame.right.repeat(geometry.right), border);
141
+ yield Segment.line();
142
+ }
143
+ yield* this._renderBottomBorder(options, box, geometry, border, bodyLines);
198
144
  }
199
145
  /**
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.
146
+ * The body's rows, held to the region as Rich's `render_lines` holds them —
147
+ * padded below, cropped from the bottom — and each exactly `spanWidth`
148
+ * cells: `Padding` makes every row its block's width, cropping content that
149
+ * ignored its canvas. A span of no cells renders nothing, as Rich's
150
+ * `Console.render` renders nothing under one column, so outside a region it
151
+ * draws no rows, padding rows included, and inside one the frame stands
152
+ * around empty rows. The render happens before either border is drawn,
153
+ * because a `bottomRightAccessory` function is handed the rows it drew.
202
154
  */
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();
155
+ _renderBody(options, spanWidth) {
156
+ // Two border rows are the panel's own, and the vertical padding is the
157
+ // body's. Handed on as a region, the rest is the panel's to shape, which
158
+ // is what stretches its frame down a pane and keeps its bottom border
159
+ // inside one. No highlighter: what the panel holds is drawn plain, as
160
+ // Rich's `Panel` hands its body `highlight=False`.
161
+ const bodyOptions = {
162
+ ...options,
163
+ highlight: false,
164
+ maxWidth: spanWidth,
165
+ height: insetHeight(options.height, 2),
166
+ };
167
+ const lines = Segment.splitLines(render(this._body, bodyOptions));
168
+ return fitHeight(lines, bodyOptions.height);
209
169
  }
210
170
  measure(rawOptions) {
211
171
  const options = withCellWidth(rawOptions);
@@ -220,18 +180,17 @@ export class Panel {
220
180
  return { minimum: width, maximum: width };
221
181
  }
222
182
  const content = this._contentRange(options);
223
- const overhead = frameOverhead(layoutPanel(options.maxWidth, this.padding));
183
+ const overhead = frameWidth(layoutPanel(options.maxWidth)) + this._body.left + this._body.right;
224
184
  const maximum = Math.min(options.maxWidth, Math.max(content.maximum, this._labelWidth(options) + overhead));
225
185
  return { minimum: Math.min(content.minimum, maximum), maximum };
226
186
  }
227
- /** The content plus its own frame, both read off the division it will render against. */
187
+ /** The body plus the frame around it, both read off the division it will render against. */
228
188
  _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);
189
+ const geometry = layoutPanel(options.maxWidth);
190
+ const measurement = Measurement.get({ ...options, maxWidth: geometry.spanWidth }, this._body);
232
191
  return {
233
- minimum: measurement.minimum + overhead,
234
- maximum: Math.min(options.maxWidth, measurement.maximum + overhead),
192
+ minimum: measurement.minimum + frameWidth(geometry),
193
+ maximum: measurement.maximum + frameWidth(geometry),
235
194
  };
236
195
  }
237
196
  /** The cells the title's label takes, its padding included; none without a title. */
@@ -265,11 +224,8 @@ export class Panel {
265
224
  yield* borderRow(options, box.top, geometry, geometry.spanWidth, this._titleLabel, titleStyle, border, box.top.right.repeat(geometry.right));
266
225
  yield Segment.line();
267
226
  }
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);
227
+ *_renderBottomBorder(options, box, geometry, border, body) {
228
+ const accessory = this._resolveAccessory(this.bottomRightAccessory, body);
273
229
  const accessoryDisplay = accessory === undefined
274
230
  ? ""
275
231
  : typeof accessory === "string"
@@ -289,11 +245,11 @@ export class Panel {
289
245
  yield new Segment(afterAccessory, border);
290
246
  yield Segment.line();
291
247
  }
292
- _resolveAccessory(a) {
248
+ _resolveAccessory(a, body) {
293
249
  if (a === undefined)
294
250
  return undefined;
295
251
  if (typeof a === "function")
296
- return a();
252
+ return a(body);
297
253
  return a;
298
254
  }
299
255
  // --- Static factory ---