@promptctl/rich-js 0.18.1 → 0.20.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.
- package/README.md +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import { Style } from "../core/style.js";
|
|
6
|
+
import type { RichText } from "../core/text.js";
|
|
6
7
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
7
8
|
export type PaddingDimensions = number | [number, number] | [number, number, number, number];
|
|
8
9
|
/**
|
|
@@ -18,6 +19,25 @@ export type PaddingDimensions = number | [number, number] | [number, number, num
|
|
|
18
19
|
* border and `Table` threw outright.
|
|
19
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
|
+
}
|
|
28
|
+
/**
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
export declare function layoutPadding(outerWidth: number, leftWanted: number, rightWanted: number): PaddingGeometry;
|
|
21
41
|
export declare class Padding implements Renderable, Measurable {
|
|
22
42
|
readonly renderable: Renderable;
|
|
23
43
|
readonly top: number;
|
|
@@ -26,7 +46,7 @@ export declare class Padding implements Renderable, Measurable {
|
|
|
26
46
|
readonly left: number;
|
|
27
47
|
readonly style: string | Style;
|
|
28
48
|
readonly expand: boolean;
|
|
29
|
-
constructor(
|
|
49
|
+
constructor(content: string | RichText | Renderable, padding: PaddingDimensions, options?: {
|
|
30
50
|
style?: string | Style;
|
|
31
51
|
expand?: boolean;
|
|
32
52
|
});
|
|
@@ -36,3 +56,4 @@ export declare class Padding implements Renderable, Measurable {
|
|
|
36
56
|
maximum: number;
|
|
37
57
|
};
|
|
38
58
|
}
|
|
59
|
+
export {};
|
|
@@ -5,7 +5,8 @@ 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,
|
|
8
|
+
import { fitHeight, getStyle, insetHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
|
+
import { embed } from "./embed.js";
|
|
9
10
|
/**
|
|
10
11
|
* Parses the shapes a caller may write padding in — one number, a vertical/
|
|
11
12
|
* horizontal pair, or all four sides — into the four-sided form every
|
|
@@ -51,7 +52,7 @@ export function normalizePadding(padding) {
|
|
|
51
52
|
* `layoutPanel` gives: a squeeze should cost you the decoration, not the thing
|
|
52
53
|
* being decorated.
|
|
53
54
|
*/
|
|
54
|
-
function layoutPadding(outerWidth, leftWanted, rightWanted) {
|
|
55
|
+
export function layoutPadding(outerWidth, leftWanted, rightWanted) {
|
|
55
56
|
let budget = cellCount(outerWidth);
|
|
56
57
|
const take = (want) => {
|
|
57
58
|
const got = Math.min(want, budget);
|
|
@@ -75,9 +76,9 @@ export class Padding {
|
|
|
75
76
|
left;
|
|
76
77
|
style;
|
|
77
78
|
expand;
|
|
78
|
-
constructor(
|
|
79
|
+
constructor(content, padding, options) {
|
|
79
80
|
const [top, right, bottom, left] = normalizePadding(padding);
|
|
80
|
-
this.renderable =
|
|
81
|
+
this.renderable = embed(content);
|
|
81
82
|
this.top = top;
|
|
82
83
|
this.right = right;
|
|
83
84
|
this.bottom = bottom;
|
|
@@ -87,16 +88,25 @@ export class Padding {
|
|
|
87
88
|
}
|
|
88
89
|
*render(rawOptions) {
|
|
89
90
|
const options = withBoundedWidth(rawOptions, this);
|
|
90
|
-
|
|
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);
|
|
91
98
|
const innerOptions = {
|
|
92
99
|
...options,
|
|
93
100
|
maxWidth: geometry.contentWidth,
|
|
94
101
|
height: insetHeight(options.height, this.top + this.bottom),
|
|
95
102
|
};
|
|
96
|
-
const segments = [...this.renderable.render(innerOptions)];
|
|
97
|
-
const lines = fitHeight(Segment.splitLines(segments), innerOptions.height);
|
|
98
103
|
const resolved = getStyle(options, this.style);
|
|
99
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);
|
|
100
110
|
// Zero-length spans need no branch to suppress: the wire boundary drops
|
|
101
111
|
// empty segments, so a padding the width could not afford emits nothing.
|
|
102
112
|
const leftPad = new Segment(" ".repeat(geometry.left), style);
|
|
@@ -108,10 +118,9 @@ export class Padding {
|
|
|
108
118
|
}
|
|
109
119
|
for (const line of lines) {
|
|
110
120
|
yield leftPad;
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
yield* Segment.adjustLineLength(line, geometry.contentWidth, style, this.expand);
|
|
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);
|
|
115
124
|
yield rightPad;
|
|
116
125
|
yield Segment.line();
|
|
117
126
|
}
|
|
@@ -124,18 +133,15 @@ export class Padding {
|
|
|
124
133
|
const options = withCellWidth(rawOptions);
|
|
125
134
|
const geometry = layoutPadding(options.maxWidth, this.left, this.right);
|
|
126
135
|
const overhead = geometry.left + geometry.right;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
return { minimum: overhead, maximum: options.maxWidth };
|
|
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
|
+
};
|
|
140
146
|
}
|
|
141
147
|
}
|
|
@@ -9,8 +9,8 @@ import type { PaddingDimensions } from "./padding.js";
|
|
|
9
9
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
10
10
|
/**
|
|
11
11
|
* A lazily-resolved border accessory. Strings render inline in the
|
|
12
|
-
* border style; a `RichText` accessory
|
|
13
|
-
*
|
|
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
14
|
* accessory is a small status indicator, not an arbitrary span carrier).
|
|
15
15
|
* A function form is evaluated at render time, *after* content has been
|
|
16
16
|
* rendered for the current frame — use this when the accessory mirrors
|
|
@@ -34,14 +34,13 @@ export interface PanelOptions {
|
|
|
34
34
|
style?: string | Style;
|
|
35
35
|
borderStyle?: string | Style;
|
|
36
36
|
/**
|
|
37
|
-
* Style for the title text in the top border
|
|
38
|
-
*
|
|
39
|
-
* title should pop relative to the border.
|
|
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.
|
|
40
39
|
*/
|
|
41
40
|
titleStyle?: string | Style;
|
|
42
41
|
/**
|
|
43
|
-
* Style for the subtitle text in the bottom border
|
|
44
|
-
* `borderStyle
|
|
42
|
+
* Style for the subtitle text in the bottom border, laid over
|
|
43
|
+
* `borderStyle` as `titleStyle` is.
|
|
45
44
|
*/
|
|
46
45
|
subtitleStyle?: string | Style;
|
|
47
46
|
width?: number;
|
|
@@ -60,6 +59,8 @@ export declare class Panel implements Renderable, Measurable {
|
|
|
60
59
|
readonly subtitleStyle: string | Style | undefined;
|
|
61
60
|
readonly width: number | undefined;
|
|
62
61
|
readonly padding: [number, number, number, number];
|
|
62
|
+
private readonly _titleLabel;
|
|
63
|
+
private readonly _subtitleLabel;
|
|
63
64
|
constructor(content: string | RichText | Renderable, options?: PanelOptions);
|
|
64
65
|
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
65
66
|
/**
|
|
@@ -70,44 +71,41 @@ export declare class Panel implements Renderable, Measurable {
|
|
|
70
71
|
*/
|
|
71
72
|
private _renderContent;
|
|
72
73
|
/**
|
|
73
|
-
* One row of the panel body: frame column,
|
|
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.
|
|
74
77
|
*
|
|
75
|
-
* [LAW:single-enforcer] `adjustLineLength` is the one place
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* child is trimmed rather than allowed to eat the right-hand padding. The
|
|
80
|
-
* second fills the rest of the span, which is the trailing padding and any
|
|
81
|
-
* shortfall in one stroke.
|
|
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
82
|
*/
|
|
83
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;
|
|
84
89
|
measure(rawOptions: RenderOptions): {
|
|
85
90
|
minimum: number;
|
|
86
91
|
maximum: number;
|
|
87
92
|
};
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* [LAW:one-source-of-truth] `measure` and `_getPanelWidth` ask this same
|
|
93
|
-
* question, and each used to work it out itself — the same `layoutPanel`, the
|
|
94
|
-
* same `frameOverhead`, the same `Math.min(maxWidth, maximum + overhead)`,
|
|
95
|
-
* written twice. That is the pattern `_declaredWidth` below was extracted to
|
|
96
|
-
* stop, left standing for the fit case; changing how overhead is derived in
|
|
97
|
-
* one copy is all it would take to put `measure` and `render` back into the
|
|
98
|
-
* disagreement this epic spent itself closing.
|
|
99
|
-
*/
|
|
100
|
-
private _fitRange;
|
|
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;
|
|
101
97
|
/**
|
|
102
98
|
* The width this panel was told to be, as a count of cells.
|
|
103
99
|
*
|
|
104
|
-
* [LAW:one-source-of-truth] `measure` and `_getPanelWidth`
|
|
105
|
-
*
|
|
106
|
-
* separately, `measure` reported nine cells of content while `render` drew the
|
|
107
|
-
* declared twelve, and the parent that divided space from the range got a
|
|
108
|
-
* panel three cells wider than the share it granted.
|
|
100
|
+
* [LAW:one-source-of-truth] `measure` and `_getPanelWidth` read the field
|
|
101
|
+
* from here, so neither can count it differently from the other.
|
|
109
102
|
*/
|
|
110
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
|
+
*/
|
|
111
109
|
private _getPanelWidth;
|
|
112
110
|
private _renderTopBorder;
|
|
113
111
|
private _renderBottomBorder;
|
|
@@ -7,9 +7,9 @@ import { NULL_STYLE } from "../core/style.js";
|
|
|
7
7
|
import { ROUNDED } from "../core/box.js";
|
|
8
8
|
import { Measurement } from "../core/measure.js";
|
|
9
9
|
import { RichText } from "../core/text.js";
|
|
10
|
-
import { embed, inlineLabel } from "./embed.js";
|
|
10
|
+
import { cutLabel, drawLabel, embed, inlineLabel } from "./embed.js";
|
|
11
11
|
import { normalizePadding } from "./padding.js";
|
|
12
|
-
import { fitHeight, getStyle, insetHeight,
|
|
12
|
+
import { drawable, fitHeight, getStyle, insetHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
13
13
|
function layoutPanel(outerWidth, padding) {
|
|
14
14
|
const [, padRightWanted, , padLeftWanted] = padding;
|
|
15
15
|
// Panel's one division point, and the width reaching it is not always one
|
|
@@ -49,11 +49,58 @@ function frameOverhead(geometry) {
|
|
|
49
49
|
return geometry.left + geometry.right + geometry.padLeft + geometry.padRight;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
54
55
|
*/
|
|
55
|
-
function
|
|
56
|
-
return
|
|
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
|
+
];
|
|
57
104
|
}
|
|
58
105
|
export class Panel {
|
|
59
106
|
renderable;
|
|
@@ -68,11 +115,15 @@ export class Panel {
|
|
|
68
115
|
subtitleStyle;
|
|
69
116
|
width;
|
|
70
117
|
padding;
|
|
118
|
+
_titleLabel;
|
|
119
|
+
_subtitleLabel;
|
|
71
120
|
constructor(content, options) {
|
|
72
121
|
this.renderable = embed(content);
|
|
73
122
|
this.box = options?.box ?? ROUNDED;
|
|
74
123
|
this.title = options?.title;
|
|
75
124
|
this.subtitle = options?.subtitle;
|
|
125
|
+
this._titleLabel = inlineLabel(this.title);
|
|
126
|
+
this._subtitleLabel = inlineLabel(this.subtitle);
|
|
76
127
|
this.bottomRightAccessory = options?.bottomRightAccessory;
|
|
77
128
|
this.expand = options?.expand !== false;
|
|
78
129
|
this.style = options?.style ?? NULL_STYLE;
|
|
@@ -85,26 +136,27 @@ export class Panel {
|
|
|
85
136
|
*render(rawOptions) {
|
|
86
137
|
const options = withBoundedWidth(rawOptions, this);
|
|
87
138
|
const box = this.box.substitute(options);
|
|
88
|
-
|
|
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.
|
|
89
143
|
const style = getStyle(options, this.style);
|
|
144
|
+
const borderStyle = style.add(getStyle(options, this.borderStyle));
|
|
90
145
|
const border = borderStyle.isNull ? undefined : borderStyle;
|
|
91
146
|
const contentStyle = style.isNull ? undefined : style;
|
|
92
147
|
const geometry = layoutPanel(this._getPanelWidth(options), this.padding);
|
|
93
148
|
const [padTop, , padBottom] = this.padding;
|
|
94
|
-
const contentLines = this._renderContent(options, geometry.contentWidth);
|
|
95
|
-
// Top border (with optional title)
|
|
149
|
+
const contentLines = this._renderContent(options, geometry.contentWidth, contentStyle);
|
|
96
150
|
yield* this._renderTopBorder(options, box, geometry, border);
|
|
97
|
-
// Top padding — a padding row is a content row whose content is nothing.
|
|
98
151
|
for (let i = 0; i < padTop; i++) {
|
|
99
|
-
yield* this.
|
|
152
|
+
yield* this._renderPaddingRow(box, geometry, border, contentStyle);
|
|
100
153
|
}
|
|
101
154
|
for (const line of contentLines) {
|
|
102
155
|
yield* this._renderRow(box, geometry, line, border, contentStyle);
|
|
103
156
|
}
|
|
104
157
|
for (let i = 0; i < padBottom; i++) {
|
|
105
|
-
yield* this.
|
|
158
|
+
yield* this._renderPaddingRow(box, geometry, border, contentStyle);
|
|
106
159
|
}
|
|
107
|
-
// Bottom border (with optional subtitle)
|
|
108
160
|
yield* this._renderBottomBorder(options, box, geometry, border);
|
|
109
161
|
}
|
|
110
162
|
/**
|
|
@@ -113,137 +165,107 @@ export class Panel {
|
|
|
113
165
|
* but the render still happens, because a `bottomRightAccessory` thunk
|
|
114
166
|
* fires at every width and reads state this render populates.
|
|
115
167
|
*/
|
|
116
|
-
_renderContent(options, contentWidth) {
|
|
168
|
+
_renderContent(options, contentWidth, style) {
|
|
117
169
|
// Two border rows and the vertical padding are the panel's own. Handed on
|
|
118
170
|
// as a region, the rest is the panel's to shape, which is what stretches
|
|
119
171
|
// its frame down a pane and keeps its bottom border inside one.
|
|
120
172
|
const [padTop, , padBottom] = this.padding;
|
|
121
173
|
const height = insetHeight(options.height, 2 + padTop + padBottom);
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
124
178
|
return contentWidth === 0 ? [] : lines;
|
|
125
179
|
}
|
|
126
180
|
/**
|
|
127
|
-
* One row of the panel body: frame column,
|
|
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.
|
|
128
184
|
*
|
|
129
|
-
* [LAW:single-enforcer] `adjustLineLength` is the one place
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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.
|
|
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.
|
|
136
189
|
*/
|
|
137
190
|
*_renderRow(box, geometry, line, border, contentStyle) {
|
|
138
191
|
const frame = box.getContentChars("row");
|
|
139
192
|
yield new Segment(frame.left.repeat(geometry.left), border);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
yield
|
|
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);
|
|
143
207
|
yield new Segment(frame.right.repeat(geometry.right), border);
|
|
144
208
|
yield Segment.line();
|
|
145
209
|
}
|
|
146
210
|
measure(rawOptions) {
|
|
147
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.
|
|
148
217
|
const declared = this._declaredWidth;
|
|
149
218
|
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
219
|
const width = Math.min(options.maxWidth, declared);
|
|
157
220
|
return { minimum: width, maximum: width };
|
|
158
221
|
}
|
|
159
|
-
|
|
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 };
|
|
160
226
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
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) {
|
|
227
|
+
/** The content plus its own frame, both read off the division it will render against. */
|
|
228
|
+
_contentRange(options) {
|
|
174
229
|
const geometry = layoutPanel(options.maxWidth, this.padding);
|
|
175
230
|
const overhead = frameOverhead(geometry);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 };
|
|
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;
|
|
192
240
|
}
|
|
193
241
|
/**
|
|
194
242
|
* The width this panel was told to be, as a count of cells.
|
|
195
243
|
*
|
|
196
|
-
* [LAW:one-source-of-truth] `measure` and `_getPanelWidth`
|
|
197
|
-
*
|
|
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.
|
|
244
|
+
* [LAW:one-source-of-truth] `measure` and `_getPanelWidth` read the field
|
|
245
|
+
* from here, so neither can count it differently from the other.
|
|
201
246
|
*/
|
|
202
247
|
get _declaredWidth() {
|
|
203
248
|
return this.width === undefined ? undefined : cellCount(this.width);
|
|
204
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
|
+
*/
|
|
205
255
|
_getPanelWidth(options) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return
|
|
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));
|
|
212
262
|
}
|
|
213
263
|
*_renderTopBorder(options, box, geometry, border) {
|
|
214
|
-
const
|
|
215
|
-
|
|
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);
|
|
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));
|
|
243
266
|
yield Segment.line();
|
|
244
267
|
}
|
|
245
268
|
*_renderBottomBorder(options, box, geometry, border) {
|
|
246
|
-
const innerBorderWidth = geometry.spanWidth;
|
|
247
269
|
// Resolve the right accessory *now*. Function form evaluates after
|
|
248
270
|
// content has been rendered (Panel.render collects content segments
|
|
249
271
|
// before yielding any borders), so the thunk sees fresh widget state.
|
|
@@ -253,44 +275,18 @@ export class Panel {
|
|
|
253
275
|
: typeof accessory === "string"
|
|
254
276
|
? ` ${accessory} `
|
|
255
277
|
: ` ${accessory.plain} `;
|
|
256
|
-
|
|
278
|
+
// Cell-aware clip: the accessory never claims more than the span.
|
|
279
|
+
const accessoryText = setCellSize(accessoryDisplay, asCellCol(Math.min(cellLen(accessoryDisplay), geometry.spanWidth)));
|
|
257
280
|
const accessoryOwn = accessory instanceof RichText ? accessory.resolvedStyle(options) : NULL_STYLE;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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);
|
|
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);
|
|
294
290
|
yield Segment.line();
|
|
295
291
|
}
|
|
296
292
|
_resolveAccessory(a) {
|