@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.
- package/README.md +7 -0
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
|
@@ -5,9 +5,10 @@ import { cellCount } from "../core/cells.js";
|
|
|
5
5
|
import { ColorDepth, ColorRgba, ColorSpec, resolveTerminal } from "../core/color.js";
|
|
6
6
|
import { Segment } from "../core/segment.js";
|
|
7
7
|
import { Style } from "../core/style.js";
|
|
8
|
-
import { drawable, getStyle } from "../core/protocol.js";
|
|
8
|
+
import { drawable, getStyle, render } from "../core/protocol.js";
|
|
9
9
|
import { Effected } from "./effect.js";
|
|
10
10
|
import { EFFECT_CURVES, atShares, shimmer } from "./effects.js";
|
|
11
|
+
import { State } from "../core/state.js";
|
|
11
12
|
const BAR = "━";
|
|
12
13
|
const ASCII_BAR = "-";
|
|
13
14
|
const HALF_BAR_RIGHT = "╸";
|
|
@@ -22,27 +23,49 @@ const PULSE_REACH = 10;
|
|
|
22
23
|
/** Rich's pulse back where the back style names no colour. */
|
|
23
24
|
const PULSE_BACK = ColorSpec.fromRgba(new ColorRgba(0, 0, 0));
|
|
24
25
|
export class ProgressBar {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// Rich's ProgressBar is updated after it is built: `bar.completed = n`.
|
|
27
|
+
_total;
|
|
28
|
+
_completed;
|
|
29
|
+
_finished;
|
|
27
30
|
width;
|
|
28
31
|
pulse;
|
|
29
32
|
style;
|
|
30
33
|
completeStyle;
|
|
31
34
|
finishedStyle;
|
|
32
35
|
constructor(options) {
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
36
|
+
this._total = new State(options?.total ?? 100);
|
|
37
|
+
this._completed = new State(options?.completed ?? 0);
|
|
35
38
|
this.width = options?.width;
|
|
36
|
-
this.pulse = options?.pulse;
|
|
39
|
+
this.pulse = options?.pulse ?? false;
|
|
40
|
+
this._finished = new State(options?.finished);
|
|
37
41
|
this.style = options?.style ?? "bar.back";
|
|
38
42
|
this.completeStyle = options?.completeStyle ?? "bar.complete";
|
|
39
43
|
this.finishedStyle = options?.finishedStyle ?? "bar.finished";
|
|
40
44
|
}
|
|
45
|
+
get total() {
|
|
46
|
+
return this._total.get();
|
|
47
|
+
}
|
|
48
|
+
set total(value) {
|
|
49
|
+
this._total.set(value);
|
|
50
|
+
}
|
|
51
|
+
get completed() {
|
|
52
|
+
return this._completed.get();
|
|
53
|
+
}
|
|
54
|
+
set completed(value) {
|
|
55
|
+
this._completed.set(value);
|
|
56
|
+
}
|
|
57
|
+
/** Whether the work is done, as given; unset, `completed` reaching `total` at the moment the bar is drawn. */
|
|
58
|
+
get finished() {
|
|
59
|
+
return this._finished.get();
|
|
60
|
+
}
|
|
61
|
+
set finished(value) {
|
|
62
|
+
this._finished.set(value);
|
|
63
|
+
}
|
|
41
64
|
*render(options) {
|
|
42
65
|
// Never wider than offered, as Rich's `min(self.width or max_width, max_width)`.
|
|
43
66
|
// [LAW:parse-dont-validate] parsed as a cell count, so a negative width draws nothing, as Python's `"━" * -2` is "".
|
|
44
67
|
const width = cellCount(Math.min(this.width || options.maxWidth, options.maxWidth));
|
|
45
|
-
if (this.pulse
|
|
68
|
+
if (!this.pulse) {
|
|
46
69
|
yield* this.track(options, width, this.completeHalves(width), getStyle(options, this.style));
|
|
47
70
|
return;
|
|
48
71
|
}
|
|
@@ -56,10 +79,10 @@ export class ProgressBar {
|
|
|
56
79
|
const style = getStyle(options, this.style);
|
|
57
80
|
const ink = style.color?.isDefault === false ? style.color : ColorSpec.fromRgba((style.color ?? PULSE_BACK).getTruecolor(theme, true));
|
|
58
81
|
const back = style.add(Style.fromColor(ink));
|
|
59
|
-
// [LAW:no-ambient-temporal-coupling] The moment is the
|
|
82
|
+
// [LAW:no-ambient-temporal-coupling] The moment is the frame's; the bar
|
|
60
83
|
// only says what it looks like then. An empty track, lit by the shimmer.
|
|
61
84
|
const track = { render: (o) => this.track(o, width, 0, back) };
|
|
62
|
-
yield* new Effected(track, this.pulseEffect(options, back, depth, theme), {
|
|
85
|
+
yield* render(new Effected(track, this.pulseEffect(options, back, depth, theme), { key: "progress-bar", theme }), options);
|
|
63
86
|
}
|
|
64
87
|
/**
|
|
65
88
|
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
@@ -82,7 +105,7 @@ export class ProgressBar {
|
|
|
82
105
|
*track(options, width, completeHalves, back) {
|
|
83
106
|
const barCount = Math.floor(completeHalves / 2);
|
|
84
107
|
const halfBarCount = completeHalves % 2;
|
|
85
|
-
const isFinished = this.completed >= this.total;
|
|
108
|
+
const isFinished = this.finished ?? this.completed >= this.total;
|
|
86
109
|
const fill = getStyle(options, isFinished ? this.finishedStyle : this.completeStyle);
|
|
87
110
|
const fillStyle = fill.isNull ? undefined : fill;
|
|
88
111
|
const backStyle = back.isNull ? undefined : back;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { Console } from "../core/console.js";
|
|
25
25
|
import { renderMarkup } from "../core/markup.js";
|
|
26
26
|
import { RichText } from "../core/text.js";
|
|
27
|
+
import { pattern } from "../core/pattern.js";
|
|
27
28
|
/** An answer the prompt refused, and the markup it prints before asking again — Rich's `InvalidResponse`. */
|
|
28
29
|
class InvalidResponse {
|
|
29
30
|
message;
|
|
@@ -104,10 +105,10 @@ function responseKind(convert, invalid, options) {
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
/** Python's `int()` over a string: a sign, then digits, any two of which may have one `_` between them. */
|
|
107
|
-
const INTEGER = /^[+-]?\d(?:_?\d)
|
|
108
|
+
const INTEGER = pattern(/^[+-]?\d(?:_?\d)*$/);
|
|
108
109
|
const DIGITS = String.raw `\d(?:_?\d)*`;
|
|
109
110
|
/** Python's `float()` over a string: a decimal with an optional exponent, or `inf`, `infinity` or `nan`, in any case. */
|
|
110
|
-
const FLOAT = new RegExp(String.raw `^[+-]?(?:(?:${DIGITS}(?:\.(?:${DIGITS})?)?|\.${DIGITS})(?:e[+-]?${DIGITS})?|inf(?:inity)?|nan)$`, "i");
|
|
111
|
+
const FLOAT = pattern(new RegExp(String.raw `^[+-]?(?:(?:${DIGITS}(?:\.(?:${DIGITS})?)?|\.${DIGITS})(?:e[+-]?${DIGITS})?|inf(?:inity)?|nan)$`, "i"));
|
|
111
112
|
/**
|
|
112
113
|
* The integer an answer spells, as Python's `int()` reads it, or none. One a
|
|
113
114
|
* `number` cannot hold exactly is none: Python's int is unbounded and a
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitespace as Python reads it — `str.isspace`, `str.strip`, and `\s` in a
|
|
3
|
+
* `str` pattern of `re`, which are one set. markdown-it-py, the markdown-it
|
|
4
|
+
* Rich runs, reads whitespace through all three, and that set is not
|
|
5
|
+
* JavaScript's: `\s` and `trim` take U+FEFF, which Python does not, and leave
|
|
6
|
+
* U+001C–U+001F and U+0085, which Python takes.
|
|
7
|
+
*/
|
|
8
|
+
/** The set as a regex character class, every member of it a single UTF-16 unit, for composing into a pattern where markdown-it-py's has `\s`. */
|
|
9
|
+
export declare const PY_WHITESPACE: string;
|
|
10
|
+
/** Python's `str.strip`, a scan in from each end, so a run of interior whitespace costs one pass, as it does there. */
|
|
11
|
+
export declare function pyStrip(text: string): string;
|
|
12
|
+
/** Python's `str.rstrip`, a scan in from the end — not a `+$` pattern, which retries from every cell of an interior run. */
|
|
13
|
+
export declare function pyRstrip(text: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitespace as Python reads it — `str.isspace`, `str.strip`, and `\s` in a
|
|
3
|
+
* `str` pattern of `re`, which are one set. markdown-it-py, the markdown-it
|
|
4
|
+
* Rich runs, reads whitespace through all three, and that set is not
|
|
5
|
+
* JavaScript's: `\s` and `trim` take U+FEFF, which Python does not, and leave
|
|
6
|
+
* U+001C–U+001F and U+0085, which Python takes.
|
|
7
|
+
*/
|
|
8
|
+
import { pattern } from "../core/pattern.js";
|
|
9
|
+
/** The set as a regex character class, every member of it a single UTF-16 unit, for composing into a pattern where markdown-it-py's has `\s`. */
|
|
10
|
+
export const PY_WHITESPACE = String.raw `[\t-\r\x1c-\x20\x85\xa0 -
]`;
|
|
11
|
+
const PY_SPACE = pattern(new RegExp(`^${PY_WHITESPACE}$`));
|
|
12
|
+
/** Python's `str.strip`, a scan in from each end, so a run of interior whitespace costs one pass, as it does there. */
|
|
13
|
+
export function pyStrip(text) {
|
|
14
|
+
let start = 0;
|
|
15
|
+
while (start < text.length && PY_SPACE.test(text[start]))
|
|
16
|
+
start++;
|
|
17
|
+
return pyRstrip(text.slice(start));
|
|
18
|
+
}
|
|
19
|
+
/** Python's `str.rstrip`, a scan in from the end — not a `+$` pattern, which retries from every cell of an interior run. */
|
|
20
|
+
export function pyRstrip(text) {
|
|
21
|
+
let end = text.length;
|
|
22
|
+
while (end > 0 && PY_SPACE.test(text[end - 1]))
|
|
23
|
+
end--;
|
|
24
|
+
return text.slice(0, end);
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll — a slot of fixed size over content wider or taller than it, the
|
|
3
|
+
* content's offset in the slot moving with time: a long line panning as a
|
|
4
|
+
* marquee, a log tail scrolling, a panel sliding into its place.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:one-type-per-behavior] A marquee, a scroll and a slide are one window
|
|
7
|
+
* and differ only in the motion handed in: the offset along each axis is an
|
|
8
|
+
* `Animation<number>`, so a marquee is a looping `Phase` scaled to the span
|
|
9
|
+
* it crosses and a slide-in is a `tween` to 0, and when the window next
|
|
10
|
+
* changes is `Animation.all`'s answer over the two. Nothing here knows which
|
|
11
|
+
* one it is drawing.
|
|
12
|
+
*
|
|
13
|
+
* The slot is what the window is. It measures as its width whatever the
|
|
14
|
+
* content, and draws exactly its rows of its width at every offset — cropped
|
|
15
|
+
* only to a narrower room it is given — so what sits beside it never moves.
|
|
16
|
+
* An offset before or past the content shows blank cells rather than stopping
|
|
17
|
+
* at an edge: that is what lets a marquee enter from the right and a slide-in
|
|
18
|
+
* arrive from outside the slot.
|
|
19
|
+
*
|
|
20
|
+
* The content is drawn at its own natural width and no height, its own
|
|
21
|
+
* length, because the content wider and taller than the slot is the thing
|
|
22
|
+
* being windowed. Content that should wrap to the slot instead, or that has
|
|
23
|
+
* no natural width because it fills whatever it is given, is
|
|
24
|
+
* `new Constrain(content, width)`, composed rather than asked for.
|
|
25
|
+
*
|
|
26
|
+
* [LAW:single-enforcer] A row is cut at the window's edges by
|
|
27
|
+
* `Segment.divide`, the one place a line is cut at a column: a wide glyph a
|
|
28
|
+
* cut falls in is blanked, never halved, and each cell's anchor still names
|
|
29
|
+
* its column in the content, so hit-testing finds the owner through a moving
|
|
30
|
+
* window.
|
|
31
|
+
*
|
|
32
|
+
* [LAW:no-ambient-temporal-coupling] Pure in `t`, nothing kept between
|
|
33
|
+
* frames; `at` hands over the frame alone, and placed, the scroll stamps
|
|
34
|
+
* the hold it is in at the frame's time (`placedRender`).
|
|
35
|
+
*/
|
|
36
|
+
import { Animation } from "../core/animation.js";
|
|
37
|
+
import type { Renderable } from "../core/protocol.js";
|
|
38
|
+
/** The slot a scroll draws, and how far the content is moved in it. */
|
|
39
|
+
export interface ScrollOptions {
|
|
40
|
+
/** The slot's width in cells. */
|
|
41
|
+
readonly width: number;
|
|
42
|
+
/** The slot's height in rows. */
|
|
43
|
+
readonly height: number;
|
|
44
|
+
/** Columns of content left of the slot's first, rounded to the nearest cell: negative moves the content right. Absent, 0. */
|
|
45
|
+
readonly col?: Animation<number>;
|
|
46
|
+
/** Rows of content above the slot's first, rounded to the nearest row: negative moves the content down. Absent, 0. */
|
|
47
|
+
readonly row?: Animation<number>;
|
|
48
|
+
}
|
|
49
|
+
/** `content` seen through a slot of `width` by `height` cells, moved in it by `col` and `row` at each moment. */
|
|
50
|
+
export declare function scroll(content: Renderable, options: ScrollOptions): Animation<Renderable>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll — a slot of fixed size over content wider or taller than it, the
|
|
3
|
+
* content's offset in the slot moving with time: a long line panning as a
|
|
4
|
+
* marquee, a log tail scrolling, a panel sliding into its place.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:one-type-per-behavior] A marquee, a scroll and a slide are one window
|
|
7
|
+
* and differ only in the motion handed in: the offset along each axis is an
|
|
8
|
+
* `Animation<number>`, so a marquee is a looping `Phase` scaled to the span
|
|
9
|
+
* it crosses and a slide-in is a `tween` to 0, and when the window next
|
|
10
|
+
* changes is `Animation.all`'s answer over the two. Nothing here knows which
|
|
11
|
+
* one it is drawing.
|
|
12
|
+
*
|
|
13
|
+
* The slot is what the window is. It measures as its width whatever the
|
|
14
|
+
* content, and draws exactly its rows of its width at every offset — cropped
|
|
15
|
+
* only to a narrower room it is given — so what sits beside it never moves.
|
|
16
|
+
* An offset before or past the content shows blank cells rather than stopping
|
|
17
|
+
* at an edge: that is what lets a marquee enter from the right and a slide-in
|
|
18
|
+
* arrive from outside the slot.
|
|
19
|
+
*
|
|
20
|
+
* The content is drawn at its own natural width and no height, its own
|
|
21
|
+
* length, because the content wider and taller than the slot is the thing
|
|
22
|
+
* being windowed. Content that should wrap to the slot instead, or that has
|
|
23
|
+
* no natural width because it fills whatever it is given, is
|
|
24
|
+
* `new Constrain(content, width)`, composed rather than asked for.
|
|
25
|
+
*
|
|
26
|
+
* [LAW:single-enforcer] A row is cut at the window's edges by
|
|
27
|
+
* `Segment.divide`, the one place a line is cut at a column: a wide glyph a
|
|
28
|
+
* cut falls in is blanked, never halved, and each cell's anchor still names
|
|
29
|
+
* its column in the content, so hit-testing finds the owner through a moving
|
|
30
|
+
* window.
|
|
31
|
+
*
|
|
32
|
+
* [LAW:no-ambient-temporal-coupling] Pure in `t`, nothing kept between
|
|
33
|
+
* frames; `at` hands over the frame alone, and placed, the scroll stamps
|
|
34
|
+
* the hold it is in at the frame's time (`placedRender`).
|
|
35
|
+
*/
|
|
36
|
+
import { Animation } from "../core/animation.js";
|
|
37
|
+
import { Measurement } from "../core/measure.js";
|
|
38
|
+
import { render, withBoundedWidth } from "../core/protocol.js";
|
|
39
|
+
import { Segment } from "../core/segment.js";
|
|
40
|
+
const NO_OFFSET = Animation.constant(0);
|
|
41
|
+
/** `content` seen through a slot of `width` by `height` cells, moved in it by `col` and `row` at each moment. */
|
|
42
|
+
export function scroll(content, options) {
|
|
43
|
+
const { width, height, col = NO_OFFSET, row = NO_OFFSET } = options;
|
|
44
|
+
for (const [name, n] of [["width", width], ["height", height]]) {
|
|
45
|
+
if (!(Number.isInteger(n) && n > 0))
|
|
46
|
+
throw new RangeError(`scroll: ${name} must be a positive whole number of cells, got ${n}`);
|
|
47
|
+
}
|
|
48
|
+
return Animation.map(Animation.all([col, row]), ([c, r]) => new ScrollFrame(content, width, height, cells("col", c), cells("row", r)));
|
|
49
|
+
}
|
|
50
|
+
/** `offset` rounded to the nearest cell. [LAW:no-silent-failure] One that is not finite has no cell to move to, and says so rather than drawing a blank slot. */
|
|
51
|
+
function cells(name, offset) {
|
|
52
|
+
if (!Number.isFinite(offset))
|
|
53
|
+
throw new RangeError(`scroll: ${name} must be a finite number of cells, got ${offset}`);
|
|
54
|
+
return Math.round(offset);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The width `content` is drawn at: its own natural one. [LAW:no-silent-failure]
|
|
58
|
+
* Content that takes whatever width it is offered — a `Rule`, a renderable
|
|
59
|
+
* with no `measure` — has none, and is refused by name rather than drawn at
|
|
60
|
+
* an unbounded width it cannot fill.
|
|
61
|
+
*/
|
|
62
|
+
function naturalWidth(content, options) {
|
|
63
|
+
const natural = Measurement.get({ ...options, maxWidth: Number.POSITIVE_INFINITY }, content).maximum;
|
|
64
|
+
if (!Number.isFinite(natural)) {
|
|
65
|
+
throw new RangeError("scroll: the content has no natural width — it fills whatever width it is given; give it one with new Constrain(content, width)");
|
|
66
|
+
}
|
|
67
|
+
return natural;
|
|
68
|
+
}
|
|
69
|
+
/** One frame of a scroll: `content`'s cells from column `col` and row `row`, `width` by `height` of them, blank where it has none. */
|
|
70
|
+
class ScrollFrame {
|
|
71
|
+
content;
|
|
72
|
+
width;
|
|
73
|
+
height;
|
|
74
|
+
col;
|
|
75
|
+
row;
|
|
76
|
+
constructor(content, width, height, col, row) {
|
|
77
|
+
this.content = content;
|
|
78
|
+
this.width = width;
|
|
79
|
+
this.height = height;
|
|
80
|
+
this.col = col;
|
|
81
|
+
this.row = row;
|
|
82
|
+
}
|
|
83
|
+
*render(rawOptions) {
|
|
84
|
+
const options = withBoundedWidth(rawOptions, this);
|
|
85
|
+
// The slot is cropped to the room it is given, as a `Panel` of a declared width is.
|
|
86
|
+
const width = Math.min(this.width, options.maxWidth);
|
|
87
|
+
const drawn = [...render(this.content, { ...options, maxWidth: naturalWidth(this.content, options), height: undefined })];
|
|
88
|
+
const lines = Segment.splitLines(drawn);
|
|
89
|
+
const blank = Math.min(width, Math.max(0, -this.col));
|
|
90
|
+
const start = Math.max(0, this.col);
|
|
91
|
+
// [LAW:one-source-of-truth] What the window shows is cut from the whole content, so it may change
|
|
92
|
+
// when anything in the content does — a row above the slot entering pushes every row down, and run
|
|
93
|
+
// backwards, leaving it pulls them up. The cut throws away the cells that hold rode on, so it is
|
|
94
|
+
// told to the frame (`RenderOptions.onHold`).
|
|
95
|
+
options.onHold?.({ lastChange: Segment.lastChange(drawn), nextChange: Segment.nextChange(drawn) });
|
|
96
|
+
for (let r = 0; r < this.height; r++) {
|
|
97
|
+
const line = lines[this.row + r] ?? [];
|
|
98
|
+
const shown = Segment.divide(line, [start, start + width - blank])[1] ?? [];
|
|
99
|
+
yield* Segment.adjustLineLength([new Segment(" ".repeat(blank)), ...shown], width);
|
|
100
|
+
yield Segment.line();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
measure() {
|
|
104
|
+
return { minimum: this.width, maximum: this.width };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the interval; the beat, the width every frame draws at and which frame `t`
|
|
9
9
|
* lands on are the frames primitive's, so a spinner is one more sequence on
|
|
10
10
|
* it and not a second way of turning frames. Custom frames are a `FrameSequence`
|
|
11
|
-
*
|
|
11
|
+
* placed directly. [LAW:one-source-of-truth]
|
|
12
12
|
*
|
|
13
13
|
* It is built once and asked for each frame, as Rich's `Spinner` is built
|
|
14
14
|
* once and has `render(time)` called on it: the name is parsed and the
|
|
@@ -19,41 +19,44 @@
|
|
|
19
19
|
* the beat, the width, each frame padded to it — is made there too, so a
|
|
20
20
|
* drawn frame costs only finding which one `t` lands on.
|
|
21
21
|
*
|
|
22
|
-
* [LAW:no-ambient-temporal-coupling]
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
28
|
-
* time, as `App` and `Task.time` hand it, where Rich's spinner counts from
|
|
22
|
+
* [LAW:no-ambient-temporal-coupling] Placed, a spinner draws at the time of
|
|
23
|
+
* the frame it is drawn in, and nothing here reads a clock. Whoever draws
|
|
24
|
+
* frames hands each one its time (`core/clock.ts`) — a `Progress` the
|
|
25
|
+
* moment its tasks are drawn at, a `Live` the frame's time on its clock — as
|
|
26
|
+
* Rich's `Spinner.render` is handed `console.get_time()` or `task.get_time()`. `t` is the clock's
|
|
27
|
+
* time, as a host writes it on the frame, where Rich's spinner counts from
|
|
29
28
|
* its first draw: here nothing is kept between frames, so the same `t` draws
|
|
30
29
|
* the same bytes at one redraw a second or sixty, two spinners on one clock
|
|
31
30
|
* agree at every `t`, a `Progress` on a hand-moved clock turns its spinners
|
|
32
31
|
* on that clock, and a draw costs the same at `t = 0` and `t = 10⁶`.
|
|
33
32
|
*
|
|
34
|
-
* `speed`
|
|
35
|
-
*
|
|
36
|
-
* the interval
|
|
37
|
-
* the time
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
33
|
+
* `speed` is a `Timeline` (`core/timeline.ts`) playing from `t = 0` at
|
|
34
|
+
* that rate, as Rich scales the time, `t × speed` into the beat: the frames
|
|
35
|
+
* are held for the interval the data gives, and the spinner is asked for the
|
|
36
|
+
* frame at the time the timeline maps the host's to. [LAW:one-source-of-truth]
|
|
37
|
+
* When the frame next changes (`Style.hold`) is a time on the host's
|
|
38
|
+
* timeline, and the sequence answers on its own, so the timeline carries the
|
|
39
|
+
* answer back. `at` hands over the frame alone, as every animation's does;
|
|
40
|
+
* placed, a spinner draws at the frame's time and stamps its cells with that
|
|
41
|
+
* answer for the frames the output draws. Backwards, the frames count down from the last at `t = 0`,
|
|
42
|
+
* a negative time landing in the last frame of the pass before, where Rich's
|
|
43
|
+
* `int()` truncation holds frame 0 for a first interval and then counts
|
|
44
|
+
* down: a departure, made so a negative speed is the one timeline run
|
|
45
|
+
* backwards and not a second rule about time. A speed of zero would hold a
|
|
46
|
+
* frame forever, which a paused timeline is and a rate is not, and the
|
|
47
|
+
* timeline refuses it.
|
|
47
48
|
*/
|
|
49
|
+
import { Animation } from "../core/animation.js";
|
|
50
|
+
import { Segment } from "../core/segment.js";
|
|
48
51
|
import { Style } from "../core/style.js";
|
|
49
52
|
import { RichText } from "../core/text.js";
|
|
50
|
-
import type { Renderable, Measurable } from "../core/protocol.js";
|
|
53
|
+
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
51
54
|
export interface SpinnerOptions {
|
|
52
55
|
/** Multiplies the spinner's rate; negative runs it backwards; zero is refused. Defaults to 1. */
|
|
53
56
|
speed?: number;
|
|
54
57
|
style?: string | Style;
|
|
55
58
|
}
|
|
56
|
-
export declare class Spinner {
|
|
59
|
+
export declare class Spinner implements Animation<Renderable & Measurable> {
|
|
57
60
|
readonly name: string;
|
|
58
61
|
readonly speed: number;
|
|
59
62
|
readonly style: string | Style;
|
|
@@ -64,10 +67,19 @@ export declare class Spinner {
|
|
|
64
67
|
* whatever the console says — and a `RichText` changed in place draws as
|
|
65
68
|
* changed, as the `Text` Rich keeps does. [LAW:single-enforcer]
|
|
66
69
|
*/
|
|
67
|
-
text: string | RichText;
|
|
70
|
+
get text(): string | RichText;
|
|
71
|
+
set text(value: string | RichText);
|
|
72
|
+
private readonly _text;
|
|
68
73
|
private readonly _data;
|
|
69
74
|
private readonly _named;
|
|
70
75
|
private readonly _ascii;
|
|
76
|
+
/**
|
|
77
|
+
* The spinner drawn at the frame's time, its cells saying the hold it is in
|
|
78
|
+
* on the host's clock (`stampFrame`) — exactly, for the frames the output
|
|
79
|
+
* draws, where `nextChange` must answer for both. Under reduced motion it
|
|
80
|
+
* is the spinner reduced, which never turns (`shownAt`).
|
|
81
|
+
*/
|
|
82
|
+
private readonly _placed;
|
|
71
83
|
constructor(name?: string, text?: string | RichText, options?: SpinnerOptions);
|
|
72
84
|
get frames(): readonly string[];
|
|
73
85
|
/** Milliseconds each frame is held, as the spinner data gives it. */
|
|
@@ -77,8 +89,29 @@ export declare class Spinner {
|
|
|
77
89
|
* the line wrapped to the width and ended, as the reference's `Text` is —
|
|
78
90
|
* printed, stacked in a `Group`, or laid out in a `Progress` cell alike.
|
|
79
91
|
* The frames this output can draw are chosen where the output is known.
|
|
92
|
+
* The frame says nothing of when it turns — `t` may be a time a timeline
|
|
93
|
+
* mapped — so placed, the spinner itself stamps it.
|
|
80
94
|
*/
|
|
81
95
|
at(t: number): Renderable & Measurable;
|
|
96
|
+
/** The spinner with its motion taken out: its first frame and the label, which never turn, so any time answers. */
|
|
97
|
+
reduced(): Animation<Renderable & Measurable>;
|
|
98
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
99
|
+
measure(options: RenderOptions): {
|
|
100
|
+
minimum: number;
|
|
101
|
+
maximum: number;
|
|
102
|
+
};
|
|
103
|
+
/** The frame `choose` picks from the frames the output can draw, and the label, saying the hold it is in if `choose` says. */
|
|
104
|
+
private _frame;
|
|
105
|
+
/**
|
|
106
|
+
* When the spinner drawn at `t` next turns, on the host's clock: the
|
|
107
|
+
* sooner of its two sequences' answers, since which one is drawn is the
|
|
108
|
+
* output's to choose and the line spinner that stands in on an ASCII-only
|
|
109
|
+
* output keeps its own interval. An early answer, on the output that draws
|
|
110
|
+
* the other one, which the contract allows; the drawn cells carry the exact one.
|
|
111
|
+
*/
|
|
112
|
+
nextChange(t: number): number;
|
|
113
|
+
/** When the spinner drawn at `t` began to be drawn: the later of its two sequences' answers, by the same argument. */
|
|
114
|
+
lastChange(t: number): number;
|
|
82
115
|
private _render;
|
|
83
116
|
/**
|
|
84
117
|
* The frame in progress and the label as one text, as Rich's
|