@promptctl/rich-js 0.19.0 → 0.21.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 +3 -2
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +10 -0
- package/dist/core/color.d.ts +1 -0
- package/dist/core/color.js +14 -3
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/paint.js +13 -2
- package/dist/core/pretty.d.ts +23 -2
- package/dist/core/pretty.js +54 -18
- package/dist/core/protocol.d.ts +2 -0
- package/dist/core/protocol.js +3 -2
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +159 -76
- package/dist/core/strip.js +367 -215
- package/dist/core/style.js +10 -7
- package/dist/host/app.d.ts +68 -2
- package/dist/host/app.js +125 -15
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +87 -0
- package/dist/host/input.js +517 -0
- package/dist/host/terminal-host.d.ts +12 -0
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +7 -4
- package/dist/node/terminal-host.d.ts +7 -0
- package/dist/node/terminal-host.js +7 -0
- package/dist/renderables/effect.d.ts +89 -16
- package/dist/renderables/effect.js +115 -42
- package/dist/renderables/effects.d.ts +260 -0
- package/dist/renderables/effects.js +596 -0
- package/dist/renderables/embed.d.ts +11 -5
- package/dist/renderables/embed.js +13 -6
- package/dist/renderables/flexStrip.d.ts +1 -1
- package/dist/renderables/flexStrip.js +10 -9
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/progress.d.ts +6 -0
- package/dist/renderables/progress.js +11 -4
- package/dist/renderables/progressBar.d.ts +22 -2
- package/dist/renderables/progressBar.js +52 -7
- package/dist/renderables/rule.d.ts +2 -2
- package/dist/renderables/rule.js +12 -10
- package/dist/renderables/table.js +5 -7
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +59 -3
- package/dist/widgets/dropdown.js +3 -3
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/hit.js +2 -4
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +22 -7
- package/dist/widgets/widget-app.js +22 -12
- package/package.json +26 -18
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
19
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
20
|
-
import type
|
|
20
|
+
import { type Joiner, type StyledRenderable } from "../core/strip.js";
|
|
21
21
|
export type FlexAlign = "left" | "center" | "right" | "justify";
|
|
22
22
|
export interface FlexStripOptions<T extends StyledRenderable> {
|
|
23
23
|
/** Joiner used between items within a line, and at line endpoints. */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* FlexStrip reuses it verbatim. There is no second concept of "join."
|
|
17
17
|
*/
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
|
+
import { drawItem, drawJoin } from "../core/strip.js";
|
|
19
20
|
function renderToBlock(r, options) {
|
|
20
21
|
const segments = [...r.render(options)];
|
|
21
22
|
let width = 0;
|
|
@@ -63,10 +64,10 @@ export class FlexStrip {
|
|
|
63
64
|
const joiner = this.joiner;
|
|
64
65
|
// Pre-render every item and every joiner form we might need. Doing this
|
|
65
66
|
// up-front turns packing into a pure data computation over widths.
|
|
66
|
-
const itemBlocks = items.map((it) => renderToBlock(it, options));
|
|
67
|
-
const startCap = (i) => joiner ? renderToBlock(joiner
|
|
68
|
-
const endCap = (i) => joiner ? renderToBlock(joiner
|
|
69
|
-
const midJoin = (i, j) => joiner ? renderToBlock(joiner
|
|
67
|
+
const itemBlocks = items.map((it) => renderToBlock(drawItem(it), options));
|
|
68
|
+
const startCap = (i) => joiner ? renderToBlock(drawJoin(joiner, null, items[i], { index: i }), options) : EMPTY_BLOCK;
|
|
69
|
+
const endCap = (i) => joiner ? renderToBlock(drawJoin(joiner, items[i], null, { index: i + 1 }), options) : EMPTY_BLOCK;
|
|
70
|
+
const midJoin = (i, j) => joiner ? renderToBlock(drawJoin(joiner, items[i], items[j], { index: j }), options) : EMPTY_BLOCK;
|
|
70
71
|
const lines = [];
|
|
71
72
|
let line = null;
|
|
72
73
|
const gapBlock = gap > 0
|
|
@@ -177,23 +178,23 @@ export class FlexStrip {
|
|
|
177
178
|
for (let i = 0; i < items.length; i++) {
|
|
178
179
|
const itemWidth = renderToBlock(items[i], options).width;
|
|
179
180
|
const sc = this.joiner
|
|
180
|
-
? renderToBlock(this.joiner
|
|
181
|
+
? renderToBlock(drawJoin(this.joiner, null, items[i], { index: i }), options).width
|
|
181
182
|
: 0;
|
|
182
183
|
const ec = this.joiner
|
|
183
|
-
? renderToBlock(this.joiner
|
|
184
|
+
? renderToBlock(drawJoin(this.joiner, items[i], null, { index: i + 1 }), options).width
|
|
184
185
|
: 0;
|
|
185
186
|
minimum = Math.max(minimum, itemWidth + sc + ec);
|
|
186
187
|
maximum += itemWidth;
|
|
187
188
|
if (i > 0) {
|
|
188
189
|
maximum += this.gap * 2;
|
|
189
190
|
if (this.joiner) {
|
|
190
|
-
maximum += renderToBlock(this.joiner
|
|
191
|
+
maximum += renderToBlock(drawJoin(this.joiner, items[i - 1], items[i], { index: i }), options).width;
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
if (this.joiner) {
|
|
195
|
-
maximum += renderToBlock(this.joiner
|
|
196
|
-
maximum += renderToBlock(this.joiner
|
|
196
|
+
maximum += renderToBlock(drawJoin(this.joiner, null, items[0], { index: 0 }), options).width;
|
|
197
|
+
maximum += renderToBlock(drawJoin(this.joiner, items[items.length - 1], null, { index: items.length }), options).width;
|
|
197
198
|
}
|
|
198
199
|
return { minimum, maximum };
|
|
199
200
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListView — items stacked down a `Viewport`, scrolled to keep the selected
|
|
3
|
+
* one in view. `moveSelection` (core/selection) is the cursor that picks it.
|
|
4
|
+
*
|
|
5
|
+
* An item can wrap, so the lines the selected one covers exist only at the
|
|
6
|
+
* width it is drawn at, which a render is handed and whoever builds the frame
|
|
7
|
+
* is not. So the items are drawn here, at the viewport's content width and
|
|
8
|
+
* with no height — the lines the viewport scrolls — and the viewport is handed
|
|
9
|
+
* those lines and the selected item's range.
|
|
10
|
+
* [LAW:single-enforcer] Where the window sits is the viewport's call; this
|
|
11
|
+
* only says which lines the selection is.
|
|
12
|
+
*
|
|
13
|
+
* The viewport is handed in rather than made here: the scroll position lives
|
|
14
|
+
* in it, and a frame rebuilt on every key would otherwise start each one at
|
|
15
|
+
* the top. The pieces stay apart — a cursor, a viewport, a list drawn through
|
|
16
|
+
* it — so a consumer can take one without the others.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
20
|
+
import type { Viewport } from "./viewport.js";
|
|
21
|
+
export interface ListViewOptions {
|
|
22
|
+
/** Blank lines between one item and the next. Absent, none. */
|
|
23
|
+
gap?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Lines kept in view above and below the selected item, where the list has
|
|
26
|
+
* them: the neighbour a cursor is about to move onto. Absent, none.
|
|
27
|
+
*/
|
|
28
|
+
context?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class ListView implements Renderable, Measurable {
|
|
31
|
+
readonly viewport: Viewport;
|
|
32
|
+
readonly items: readonly Renderable[];
|
|
33
|
+
readonly selected: number;
|
|
34
|
+
readonly gap: number;
|
|
35
|
+
readonly context: number;
|
|
36
|
+
constructor(viewport: Viewport, items: readonly Renderable[], selected: number, options?: ListViewOptions);
|
|
37
|
+
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
38
|
+
measure(rawOptions: RenderOptions): {
|
|
39
|
+
minimum: number;
|
|
40
|
+
maximum: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListView — items stacked down a `Viewport`, scrolled to keep the selected
|
|
3
|
+
* one in view. `moveSelection` (core/selection) is the cursor that picks it.
|
|
4
|
+
*
|
|
5
|
+
* An item can wrap, so the lines the selected one covers exist only at the
|
|
6
|
+
* width it is drawn at, which a render is handed and whoever builds the frame
|
|
7
|
+
* is not. So the items are drawn here, at the viewport's content width and
|
|
8
|
+
* with no height — the lines the viewport scrolls — and the viewport is handed
|
|
9
|
+
* those lines and the selected item's range.
|
|
10
|
+
* [LAW:single-enforcer] Where the window sits is the viewport's call; this
|
|
11
|
+
* only says which lines the selection is.
|
|
12
|
+
*
|
|
13
|
+
* The viewport is handed in rather than made here: the scroll position lives
|
|
14
|
+
* in it, and a frame rebuilt on every key would otherwise start each one at
|
|
15
|
+
* the top. The pieces stay apart — a cursor, a viewport, a list drawn through
|
|
16
|
+
* it — so a consumer can take one without the others.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import { Measurement, measureRenderables } from "../core/measure.js";
|
|
20
|
+
import { withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
21
|
+
function lineCount(name, value) {
|
|
22
|
+
if (!Number.isInteger(value) || value < 0)
|
|
23
|
+
throw new RangeError(`ListView: ${name} ${value} is not a whole number of lines`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export class ListView {
|
|
27
|
+
viewport;
|
|
28
|
+
items;
|
|
29
|
+
selected;
|
|
30
|
+
gap;
|
|
31
|
+
context;
|
|
32
|
+
constructor(viewport, items, selected, options = {}) {
|
|
33
|
+
this.viewport = viewport;
|
|
34
|
+
this.items = items;
|
|
35
|
+
this.selected = selected;
|
|
36
|
+
this.gap = lineCount("gap", options.gap ?? 0);
|
|
37
|
+
this.context = lineCount("context", options.context ?? 0);
|
|
38
|
+
// An empty list's selection is 0: there is nothing for it to name.
|
|
39
|
+
if (!Number.isInteger(selected) || selected < 0 || selected >= Math.max(1, items.length)) {
|
|
40
|
+
throw new RangeError(`ListView: selected ${selected} is not an index of ${items.length} items`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
*render(rawOptions) {
|
|
44
|
+
const bounded = withBoundedWidth(rawOptions, this);
|
|
45
|
+
const { height: _height, ...rest } = bounded;
|
|
46
|
+
const options = { ...rest, maxWidth: this.viewport.contentWidth(rest.maxWidth) };
|
|
47
|
+
const gap = Array.from({ length: this.gap }, () => []);
|
|
48
|
+
const drawn = this.items.map((item) => Segment.splitLines(item.render(options)));
|
|
49
|
+
// The gap sits between items, so every item before the selected one
|
|
50
|
+
// brings its lines and one gap.
|
|
51
|
+
const start = drawn.slice(0, this.selected).reduce((total, item) => total + item.length + this.gap, 0);
|
|
52
|
+
const end = start + (drawn[this.selected]?.length ?? 0);
|
|
53
|
+
const lines = drawn.flatMap((item, i) => (i === 0 ? item : [...gap, ...item]));
|
|
54
|
+
this.viewport.content = {
|
|
55
|
+
*render() {
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
yield* line;
|
|
58
|
+
yield Segment.line();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
// A range reaching past either end is clamped by the viewport, which holds
|
|
63
|
+
// its offset to the lines that exist. Followed, not ensured: the wheel can
|
|
64
|
+
// scroll away from the selection until the selection moves.
|
|
65
|
+
this.viewport.follow(start - this.context, end + this.context);
|
|
66
|
+
yield* this.viewport.render(bounded);
|
|
67
|
+
}
|
|
68
|
+
measure(rawOptions) {
|
|
69
|
+
// Stacked, the items are as wide as the widest of them, plus whatever
|
|
70
|
+
// gutter the viewport's scrollbar takes. `withCellWidth` and not
|
|
71
|
+
// `withBoundedWidth`: the bounded parse asks this very method.
|
|
72
|
+
const options = withCellWidth(rawOptions);
|
|
73
|
+
const contentWidth = this.viewport.contentWidth(options.maxWidth);
|
|
74
|
+
const gutter = this.viewport.gutter;
|
|
75
|
+
const items = measureRenderables({ ...options, maxWidth: contentWidth }, this.items);
|
|
76
|
+
return new Measurement(items.minimum + gutter, items.maximum + gutter).withMaximum(options.maxWidth);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -29,6 +29,11 @@ export interface Task {
|
|
|
29
29
|
visible: boolean;
|
|
30
30
|
/** When the task started, in seconds on its `Progress`'s clock; undefined until it has. */
|
|
31
31
|
startTime: number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Rich's `Task.get_time()`: the moment the frame being drawn is drawn at, in
|
|
34
|
+
* seconds on its `Progress`'s clock, so a column animates without reading one.
|
|
35
|
+
*/
|
|
36
|
+
time: number;
|
|
32
37
|
/**
|
|
33
38
|
* Rich's `Task.elapsed`: seconds since the task started, as of the frame
|
|
34
39
|
* being drawn — `Progress` reads its clock once a frame and hands columns
|
|
@@ -77,6 +82,7 @@ export declare class BarColumn implements ProgressColumn {
|
|
|
77
82
|
readonly barWidth: number;
|
|
78
83
|
readonly tableColumn: ColumnOptions;
|
|
79
84
|
constructor(barWidth?: number);
|
|
85
|
+
/** Rich's `BarColumn`: a task not yet started, or with no total to fill toward, pulses. */
|
|
80
86
|
render(task: Task): ProgressBar;
|
|
81
87
|
}
|
|
82
88
|
/**
|
|
@@ -82,11 +82,14 @@ export class BarColumn {
|
|
|
82
82
|
constructor(barWidth) {
|
|
83
83
|
this.barWidth = barWidth ?? 40;
|
|
84
84
|
}
|
|
85
|
+
/** Rich's `BarColumn`: a task not yet started, or with no total to fill toward, pulses. */
|
|
85
86
|
render(task) {
|
|
87
|
+
const pulsing = task.startTime === undefined || task.total === undefined;
|
|
86
88
|
return new ProgressBar({
|
|
87
89
|
total: task.total ?? 100,
|
|
88
90
|
completed: task.completed,
|
|
89
91
|
width: this.barWidth,
|
|
92
|
+
pulse: pulsing ? { t: task.time } : undefined,
|
|
90
93
|
});
|
|
91
94
|
}
|
|
92
95
|
}
|
|
@@ -283,10 +286,13 @@ export class Progress {
|
|
|
283
286
|
const task = this._tasks.get(taskId);
|
|
284
287
|
if (!task)
|
|
285
288
|
return;
|
|
286
|
-
// Rich's order
|
|
287
|
-
//
|
|
289
|
+
// Rich's order: `advance` is added, then a given `completed` overwrites it,
|
|
290
|
+
// so `completed` wins when both come. Checked whole before any field is
|
|
291
|
+
// written, so a refused update leaves the task as it was.
|
|
288
292
|
const advance = finiteCount("advance", options.advance ?? 0);
|
|
289
|
-
const completed =
|
|
293
|
+
const completed = options.completed === undefined
|
|
294
|
+
? finiteCount("completed + advance", task.completed + advance)
|
|
295
|
+
: finiteCount("completed", options.completed);
|
|
290
296
|
const now = this._clock.now();
|
|
291
297
|
recordSample(task.samples, now, completed - task.completed);
|
|
292
298
|
task.completed = completed;
|
|
@@ -326,7 +332,7 @@ export class Progress {
|
|
|
326
332
|
table.addColumn(undefined, col.tableColumn);
|
|
327
333
|
}
|
|
328
334
|
// [LAW:effects-at-boundaries] One read of the clock a frame, handed to
|
|
329
|
-
// every column as the task's `elapsed`.
|
|
335
|
+
// every column as the task's `time` and `elapsed`.
|
|
330
336
|
const now = this._clock.now();
|
|
331
337
|
for (const { samples, ...state } of this._tasks.values()) {
|
|
332
338
|
if (!state.visible)
|
|
@@ -335,6 +341,7 @@ export class Progress {
|
|
|
335
341
|
const started = startTime !== undefined;
|
|
336
342
|
const task = {
|
|
337
343
|
...state,
|
|
344
|
+
time: now,
|
|
338
345
|
elapsed: started ? now - startTime : undefined,
|
|
339
346
|
speed: started ? sampleSpeed(samples) : undefined,
|
|
340
347
|
};
|
|
@@ -8,7 +8,15 @@ export interface ProgressBarOptions {
|
|
|
8
8
|
total?: number;
|
|
9
9
|
completed?: number;
|
|
10
10
|
width?: number;
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Rich's pulse, for work whose end is not known, drawn as it is `t` seconds
|
|
13
|
+
* into whoever draws the frames' clock: the whole bar in its back style,
|
|
14
|
+
* with the library's `shimmer` carrying `bar.pulse` light along it. Without
|
|
15
|
+
* it, the bar shows `completed` of `total`.
|
|
16
|
+
*/
|
|
17
|
+
pulse?: {
|
|
18
|
+
readonly t: number;
|
|
19
|
+
};
|
|
12
20
|
style?: string | Style;
|
|
13
21
|
completeStyle?: string | Style;
|
|
14
22
|
finishedStyle?: string | Style;
|
|
@@ -17,12 +25,24 @@ export declare class ProgressBar implements Renderable, Measurable {
|
|
|
17
25
|
total: number;
|
|
18
26
|
completed: number;
|
|
19
27
|
readonly width: number | undefined;
|
|
20
|
-
readonly pulse:
|
|
28
|
+
readonly pulse: {
|
|
29
|
+
readonly t: number;
|
|
30
|
+
} | undefined;
|
|
21
31
|
readonly style: string | Style;
|
|
22
32
|
readonly completeStyle: string | Style;
|
|
23
33
|
readonly finishedStyle: string | Style;
|
|
24
34
|
constructor(options?: ProgressBarOptions);
|
|
25
35
|
render(options: RenderOptions): Iterable<Segment>;
|
|
36
|
+
/**
|
|
37
|
+
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
38
|
+
* `depth`, toward `bar.pulse`. A bar has no words to keep legible, so its
|
|
39
|
+
* back takes the whole of the light.
|
|
40
|
+
*/
|
|
41
|
+
private pulseEffect;
|
|
42
|
+
/** Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and a total of zero is a bar already full. */
|
|
43
|
+
private completeHalves;
|
|
44
|
+
/** The bar `width` cells wide with `completeHalves` half cells filled, the rest in `back`. */
|
|
45
|
+
private track;
|
|
26
46
|
/** A width given is the bar's exact width, as Rich measures it; one left open fills what it is offered, down to 4. */
|
|
27
47
|
measure(options: RenderOptions): {
|
|
28
48
|
minimum: number;
|
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
* ProgressBar — a visual progress bar rendered with block characters.
|
|
3
3
|
*/
|
|
4
4
|
import { cellCount } from "../core/cells.js";
|
|
5
|
+
import { ColorDepth, ColorRgba, ColorSpec, resolveTerminal } from "../core/color.js";
|
|
5
6
|
import { Segment } from "../core/segment.js";
|
|
6
|
-
import {
|
|
7
|
+
import { Style } from "../core/style.js";
|
|
7
8
|
import { drawable, getStyle } from "../core/protocol.js";
|
|
9
|
+
import { Effected } from "./effect.js";
|
|
10
|
+
import { EFFECT_CURVES, atShares, shimmer } from "./effects.js";
|
|
8
11
|
const BAR = "━";
|
|
9
12
|
const ASCII_BAR = "-";
|
|
10
13
|
const HALF_BAR_RIGHT = "╸";
|
|
11
14
|
const HALF_BAR_LEFT = "╺";
|
|
15
|
+
/**
|
|
16
|
+
* The pulse's shimmer: the signed-off ease and swing, at a period that carries
|
|
17
|
+
* the light along a bar at about Rich's pace, and a band reaching half Rich's
|
|
18
|
+
* `PULSE_SIZE` of 20 cells either side of its centre.
|
|
19
|
+
*/
|
|
20
|
+
const PULSE_CURVE = { ...EFFECT_CURVES.shimmer, seconds: 5 };
|
|
21
|
+
const PULSE_REACH = 10;
|
|
22
|
+
/** Rich's pulse back where the back style names no colour. */
|
|
23
|
+
const PULSE_BACK = ColorSpec.fromRgba(new ColorRgba(0, 0, 0));
|
|
12
24
|
export class ProgressBar {
|
|
13
25
|
total;
|
|
14
26
|
completed;
|
|
@@ -21,8 +33,8 @@ export class ProgressBar {
|
|
|
21
33
|
this.total = options?.total ?? 100;
|
|
22
34
|
this.completed = options?.completed ?? 0;
|
|
23
35
|
this.width = options?.width;
|
|
24
|
-
this.pulse = options?.pulse
|
|
25
|
-
this.style = options?.style ??
|
|
36
|
+
this.pulse = options?.pulse;
|
|
37
|
+
this.style = options?.style ?? "bar.back";
|
|
26
38
|
this.completeStyle = options?.completeStyle ?? "bar.complete";
|
|
27
39
|
this.finishedStyle = options?.finishedStyle ?? "bar.finished";
|
|
28
40
|
}
|
|
@@ -30,15 +42,48 @@ export class ProgressBar {
|
|
|
30
42
|
// Never wider than offered, as Rich's `min(self.width or max_width, max_width)`.
|
|
31
43
|
// [LAW:parse-dont-validate] parsed as a cell count, so a negative width draws nothing, as Python's `"━" * -2` is "".
|
|
32
44
|
const width = cellCount(Math.min(this.width || options.maxWidth, options.maxWidth));
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
if (this.pulse === undefined) {
|
|
46
|
+
yield* this.track(options, width, this.completeHalves(width), getStyle(options, this.style));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
50
|
+
// Rich's pulse resolves its colours on the terminal a depth assumes, as
|
|
51
|
+
// `get_truecolor` does with no theme; so does this.
|
|
52
|
+
const theme = resolveTerminal(undefined, depth);
|
|
53
|
+
// Rich writes the pulse's back as a colour of its own, black where the
|
|
54
|
+
// back style names none, so the light always has the bar's colour to fall
|
|
55
|
+
// on: one the terminal supplies takes no light.
|
|
56
|
+
const style = getStyle(options, this.style);
|
|
57
|
+
const ink = style.color?.isDefault === false ? style.color : ColorSpec.fromRgba((style.color ?? PULSE_BACK).getTruecolor(theme, true));
|
|
58
|
+
const back = style.add(Style.fromColor(ink));
|
|
59
|
+
// [LAW:no-ambient-temporal-coupling] The moment is the caller's; the bar
|
|
60
|
+
// only says what it looks like then. An empty track, lit by the shimmer.
|
|
61
|
+
const track = { render: (o) => this.track(o, width, 0, back) };
|
|
62
|
+
yield* new Effected(track, this.pulseEffect(options, back, width, depth, theme), { t: this.pulse.t, key: "progress-bar", theme }).render(options);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
66
|
+
* `depth`, toward `bar.pulse`. A bar has no words to keep legible, so its
|
|
67
|
+
* back takes the whole of the light.
|
|
68
|
+
*/
|
|
69
|
+
pulseEffect(options, back, width, depth, theme) {
|
|
70
|
+
const ink = back.drawnColors(depth).color ?? ColorSpec.default();
|
|
71
|
+
const glow = getStyle(options, "bar.pulse").drawnColors(depth).color ?? ColorSpec.default();
|
|
72
|
+
const loop = shimmer(PULSE_CURVE, width, PULSE_REACH, glow.getTruecolor(theme, true), 0);
|
|
73
|
+
const whole = new Map([[ink.getTruecolor(theme, true).hex, 1]]);
|
|
74
|
+
return atShares(loop, () => whole);
|
|
75
|
+
}
|
|
76
|
+
/** Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and a total of zero is a bar already full. */
|
|
77
|
+
completeHalves(width) {
|
|
35
78
|
const completed = Math.min(this.total, Math.max(0, this.completed));
|
|
36
|
-
|
|
79
|
+
return this.total ? Math.trunc((width * 2 * completed) / this.total) : width * 2;
|
|
80
|
+
}
|
|
81
|
+
/** The bar `width` cells wide with `completeHalves` half cells filled, the rest in `back`. */
|
|
82
|
+
*track(options, width, completeHalves, back) {
|
|
37
83
|
const barCount = Math.floor(completeHalves / 2);
|
|
38
84
|
const halfBarCount = completeHalves % 2;
|
|
39
85
|
const isFinished = this.completed >= this.total;
|
|
40
86
|
const fill = getStyle(options, isFinished ? this.finishedStyle : this.completeStyle);
|
|
41
|
-
const back = getStyle(options, this.style);
|
|
42
87
|
const fillStyle = fill.isNull ? undefined : fill;
|
|
43
88
|
const backStyle = back.isNull ? undefined : back;
|
|
44
89
|
const bar = drawable(options, BAR, ASCII_BAR);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Rule — a horizontal divider line, optionally with a centered title.
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
|
-
import type
|
|
6
|
-
import { Style } from "../core/style.js";
|
|
5
|
+
import { type RichText } from "../core/text.js";
|
|
6
|
+
import type { Style } from "../core/style.js";
|
|
7
7
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
8
8
|
export type RuleAlign = "left" | "center" | "right";
|
|
9
9
|
export interface RuleOptions {
|
package/dist/renderables/rule.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { asCellCol, cellLen, setCellSize } from "../core/cells.js";
|
|
5
5
|
import { Segment } from "../core/segment.js";
|
|
6
|
-
import {
|
|
7
|
-
import { drawable
|
|
6
|
+
import { resolveStyle } from "../core/text.js";
|
|
7
|
+
import { drawable } from "../core/protocol.js";
|
|
8
8
|
import { cutLabel, drawLabel, inlineLabel } from "./embed.js";
|
|
9
9
|
const ASCII_RULE_CHAR = "-";
|
|
10
10
|
const DEFAULT_RULE_CHAR = "\u2500"; // ─
|
|
@@ -29,12 +29,14 @@ export class Rule {
|
|
|
29
29
|
this._label = inlineLabel(title);
|
|
30
30
|
this.characters = chars;
|
|
31
31
|
this.align = align ?? "center";
|
|
32
|
-
this.style = options?.style ??
|
|
32
|
+
this.style = options?.style ?? "rule.line";
|
|
33
33
|
}
|
|
34
34
|
*render(options) {
|
|
35
35
|
const maxWidth = options.maxWidth;
|
|
36
36
|
const ruleChar = drawable(options, this.characters, ASCII_RULE_CHAR);
|
|
37
|
-
|
|
37
|
+
// [LAW:single-enforcer] Resolved as Rich resolves the span it appends the
|
|
38
|
+
// line in: a name the theme lacks draws unstyled rather than throwing.
|
|
39
|
+
const style = resolveStyle(options, this.style);
|
|
38
40
|
const ruleStyle = style.isNull ? undefined : style;
|
|
39
41
|
const line = (width) => new Segment(repeatToWidth(ruleChar, width), ruleStyle);
|
|
40
42
|
// Rich's `required_space`: a centred title keeps a rule cell and a space
|
|
@@ -46,20 +48,20 @@ export class Rule {
|
|
|
46
48
|
yield Segment.line();
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
// Rich's `render_str(title, style="rule.text")`: the title is read in its
|
|
52
|
+
// own style and set beside the line, never in the line's style.
|
|
53
|
+
const text = this._label.text(options, "rule.text");
|
|
50
54
|
cutLabel(text, room, drawable(options, "\u2026", "."));
|
|
51
|
-
const title = drawLabel(text, options,
|
|
55
|
+
const title = drawLabel(text, options, undefined);
|
|
52
56
|
const titleWidth = Segment.getLineLength(title);
|
|
53
57
|
// As Rich lays the line out: the gaps of a centred title are the rule's,
|
|
54
58
|
// styled with it; an aligned title's one gap is unstyled.
|
|
55
59
|
switch (this.align) {
|
|
56
60
|
case "center": {
|
|
57
61
|
const left = Math.floor((maxWidth - titleWidth) / 2) - 1;
|
|
58
|
-
yield
|
|
59
|
-
yield new Segment(" ", ruleStyle);
|
|
62
|
+
yield new Segment(`${repeatToWidth(ruleChar, left)} `, ruleStyle);
|
|
60
63
|
yield* title;
|
|
61
|
-
yield new Segment(
|
|
62
|
-
yield line(maxWidth - left - titleWidth - 2);
|
|
64
|
+
yield new Segment(` ${repeatToWidth(ruleChar, maxWidth - left - titleWidth - 2)}`, ruleStyle);
|
|
63
65
|
break;
|
|
64
66
|
}
|
|
65
67
|
case "left":
|
|
@@ -924,12 +924,11 @@ export class Table {
|
|
|
924
924
|
}
|
|
925
925
|
*_renderTitle(options, text, tableWidth, ownStyle, justify) {
|
|
926
926
|
// The table owns the canvas; the caller's text still says how it meets the
|
|
927
|
-
// edge. A `RichText`'s own `justify`
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
// on. [LAW:one-source-of-truth]
|
|
927
|
+
// edge. A `RichText`'s own `justify` outranks `titleJustify`, as a `Text`'s
|
|
928
|
+
// does in the reference (rich-table-qj6i.kpp). Its own `overflow` does too,
|
|
929
|
+
// but for `"ignore"`: that title would have no edge and run straight
|
|
930
|
+
// through the frame. Every other method, and `noWrap`, cuts within the
|
|
931
|
+
// bound, so those stay theirs. `text` hands back a copy to clear it on.
|
|
933
932
|
//
|
|
934
933
|
// Never highlighted, and markup as the console says: Rich draws a title
|
|
935
934
|
// and caption through `render_str(highlight=False)`.
|
|
@@ -941,7 +940,6 @@ export class Table {
|
|
|
941
940
|
// the reference renders a `Text` title without the title style
|
|
942
941
|
// (rich-table-qj6i).
|
|
943
942
|
const source = text.text({ ...options, highlight: false }, getStyle(options, ownStyle));
|
|
944
|
-
source.justify = undefined;
|
|
945
943
|
if (source.overflow === "ignore")
|
|
946
944
|
source.overflow = undefined;
|
|
947
945
|
//
|
|
@@ -71,6 +71,7 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
|
|
|
71
71
|
private _offset;
|
|
72
72
|
private _extent;
|
|
73
73
|
private _moves;
|
|
74
|
+
private _followed;
|
|
74
75
|
constructor(content: Renderable, options?: ViewportOptions);
|
|
75
76
|
/**
|
|
76
77
|
* The first content line the last render showed. A move requested since is
|
|
@@ -94,11 +95,24 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
|
|
|
94
95
|
* the viewport shows its first line at the top.
|
|
95
96
|
*/
|
|
96
97
|
ensureVisible(start: number, end: number): void;
|
|
98
|
+
/**
|
|
99
|
+
* `ensureVisible(start, end)`, when the range differs from the one the last
|
|
100
|
+
* `follow` named. A view rebuilt every frame calls this in every frame with
|
|
101
|
+
* its selection's lines, and the selection is brought into view when it
|
|
102
|
+
* moves — not on every frame, which would undo on its very render each
|
|
103
|
+
* scroll the mouse wheel queues. A followed range the last render showed
|
|
104
|
+
* whole stays shown when the row count changes: the same lines can fall off
|
|
105
|
+
* the bottom of a viewport that got shorter. One the wheel had scrolled
|
|
106
|
+
* away from stays away.
|
|
107
|
+
*/
|
|
108
|
+
follow(start: number, end: number): void;
|
|
97
109
|
/**
|
|
98
110
|
* The width its content renders at, out of `width`: what the scrollbar's
|
|
99
111
|
* gutter leaves. Lines handed to `ensureVisible` are lines at this width.
|
|
100
112
|
*/
|
|
101
113
|
contentWidth(width: number): number;
|
|
114
|
+
/** The cells the scrollbar's gutter takes beside the content. */
|
|
115
|
+
get gutter(): number;
|
|
102
116
|
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
103
117
|
/**
|
|
104
118
|
* [LAW:one-source-of-truth] Where the queued moves leave the offset against
|
|
@@ -112,3 +126,19 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
|
|
|
112
126
|
maximum: number;
|
|
113
127
|
};
|
|
114
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* One viewport per thing a pane shows, named by a key. The same viewport comes
|
|
131
|
+
* back while the key holds, so its scroll position does too; a new key gets a
|
|
132
|
+
* fresh one, so a new subject opens at its top rather than wherever the last
|
|
133
|
+
* one was left. A frame is rebuilt on every key press and a scroll position is
|
|
134
|
+
* a viewport's, so this is what outlives the frame for a pane whose subject
|
|
135
|
+
* changes under it.
|
|
136
|
+
*/
|
|
137
|
+
export declare class ViewportPerKey {
|
|
138
|
+
private readonly options;
|
|
139
|
+
private held;
|
|
140
|
+
constructor(options?: ViewportOptions);
|
|
141
|
+
/** The viewport for `key`: the held one while the key holds, a fresh one otherwise. */
|
|
142
|
+
of(key: string): Viewport;
|
|
143
|
+
private fresh;
|
|
144
|
+
}
|
|
@@ -58,6 +58,8 @@ export class Viewport {
|
|
|
58
58
|
// Nothing rendered, nothing shown: no move can take until the first render.
|
|
59
59
|
_extent = { rows: 0, lines: 0 };
|
|
60
60
|
_moves = [];
|
|
61
|
+
// No range followed yet: NaN equals nothing, so the first `follow` moves.
|
|
62
|
+
_followed = { start: NaN, end: NaN, rows: NaN };
|
|
61
63
|
constructor(content, options = {}) {
|
|
62
64
|
this.content = content;
|
|
63
65
|
this.rows = options.rows;
|
|
@@ -96,20 +98,47 @@ export class Viewport {
|
|
|
96
98
|
ensureVisible(start, end) {
|
|
97
99
|
this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* `ensureVisible(start, end)`, when the range differs from the one the last
|
|
103
|
+
* `follow` named. A view rebuilt every frame calls this in every frame with
|
|
104
|
+
* its selection's lines, and the selection is brought into view when it
|
|
105
|
+
* moves — not on every frame, which would undo on its very render each
|
|
106
|
+
* scroll the mouse wheel queues. A followed range the last render showed
|
|
107
|
+
* whole stays shown when the row count changes: the same lines can fall off
|
|
108
|
+
* the bottom of a viewport that got shorter. One the wheel had scrolled
|
|
109
|
+
* away from stays away.
|
|
110
|
+
*/
|
|
111
|
+
follow(start, end) {
|
|
112
|
+
if (start === this._followed.start && end === this._followed.end)
|
|
113
|
+
return;
|
|
114
|
+
this._followed = { start, end, rows: NaN };
|
|
115
|
+
this.ensureVisible(start, end);
|
|
116
|
+
}
|
|
99
117
|
/**
|
|
100
118
|
* The width its content renders at, out of `width`: what the scrollbar's
|
|
101
119
|
* gutter leaves. Lines handed to `ensureVisible` are lines at this width.
|
|
102
120
|
*/
|
|
103
121
|
contentWidth(width) {
|
|
104
|
-
return cellCount(width -
|
|
122
|
+
return cellCount(width - this.gutter);
|
|
123
|
+
}
|
|
124
|
+
/** The cells the scrollbar's gutter takes beside the content. */
|
|
125
|
+
get gutter() {
|
|
126
|
+
return gutterWidth(this.scrollbar);
|
|
105
127
|
}
|
|
106
128
|
*render(rawOptions) {
|
|
107
129
|
const { height, ...options } = withBoundedWidth(rawOptions, this);
|
|
108
|
-
const gutter =
|
|
130
|
+
const gutter = this.gutter;
|
|
109
131
|
const contentWidth = this.contentWidth(options.maxWidth);
|
|
110
132
|
const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
|
|
111
133
|
const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
|
|
134
|
+
const followed = this._followed;
|
|
135
|
+
const wasShown = followed.start >= this._offset && followed.end <= this._offset + this._extent.rows;
|
|
112
136
|
this._extent = extent;
|
|
137
|
+
if (!Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown) {
|
|
138
|
+
this.ensureVisible(followed.start, followed.end);
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isNaN(followed.start))
|
|
141
|
+
this._followed = { ...followed, rows: extent.rows };
|
|
113
142
|
this._offset = this.resolve(extent);
|
|
114
143
|
this._moves = [];
|
|
115
144
|
const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
|
|
@@ -153,12 +182,39 @@ export class Viewport {
|
|
|
153
182
|
// `withBoundedWidth`: the bounded parse asks this very method for the
|
|
154
183
|
// natural width.
|
|
155
184
|
const options = withCellWidth(rawOptions);
|
|
156
|
-
const gutter =
|
|
185
|
+
const gutter = this.gutter;
|
|
157
186
|
const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
|
|
158
187
|
const content = Measurement.get(inner, this.content);
|
|
159
188
|
return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
|
|
160
189
|
}
|
|
161
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* One viewport per thing a pane shows, named by a key. The same viewport comes
|
|
193
|
+
* back while the key holds, so its scroll position does too; a new key gets a
|
|
194
|
+
* fresh one, so a new subject opens at its top rather than wherever the last
|
|
195
|
+
* one was left. A frame is rebuilt on every key press and a scroll position is
|
|
196
|
+
* a viewport's, so this is what outlives the frame for a pane whose subject
|
|
197
|
+
* changes under it.
|
|
198
|
+
*/
|
|
199
|
+
export class ViewportPerKey {
|
|
200
|
+
options;
|
|
201
|
+
held;
|
|
202
|
+
constructor(options = {}) {
|
|
203
|
+
this.options = options;
|
|
204
|
+
this.held = { key: "", viewport: this.fresh() };
|
|
205
|
+
}
|
|
206
|
+
/** The viewport for `key`: the held one while the key holds, a fresh one otherwise. */
|
|
207
|
+
of(key) {
|
|
208
|
+
if (key !== this.held.key)
|
|
209
|
+
this.held = { key, viewport: this.fresh() };
|
|
210
|
+
return this.held.viewport;
|
|
211
|
+
}
|
|
212
|
+
fresh() {
|
|
213
|
+
// Empty until a view hands it content: the content is built where the frame is.
|
|
214
|
+
return new Viewport(EMPTY, this.options);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const EMPTY = { *render() { } };
|
|
162
218
|
const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
|
|
163
219
|
/** `scrollbar` in its own styles, drawn in ASCII. */
|
|
164
220
|
function asciiScrollbar({ thumb, track }) {
|