@promptctl/rich-js 0.23.0 → 0.24.1
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/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/animation.d.ts +78 -0
- package/dist/core/animation.js +107 -0
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +17 -11
- package/dist/core/cells.js +100 -25
- package/dist/core/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/console.js +5 -5
- package/dist/core/easing.d.ts +138 -7
- package/dist/core/easing.js +355 -17
- package/dist/core/json.js +2 -1
- package/dist/core/measure.js +2 -2
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +2 -2
- package/dist/core/protocol.d.ts +73 -19
- package/dist/core/protocol.js +56 -15
- package/dist/core/render.js +2 -1
- package/dist/core/segment.d.ts +28 -1
- package/dist/core/segment.js +60 -44
- package/dist/core/strip.d.ts +107 -29
- package/dist/core/strip.js +242 -131
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +49 -9
- package/dist/core/text.js +26 -15
- package/dist/core/timeline.d.ts +84 -0
- package/dist/core/timeline.js +144 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +69 -0
- package/dist/host/app.js +4 -6
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +15 -5
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/align.d.ts +3 -2
- package/dist/renderables/align.js +3 -0
- package/dist/renderables/columns.d.ts +3 -2
- package/dist/renderables/columns.js +7 -2
- package/dist/renderables/constrain.d.ts +3 -2
- package/dist/renderables/constrain.js +5 -1
- package/dist/renderables/effect.d.ts +83 -30
- package/dist/renderables/effect.js +155 -57
- package/dist/renderables/effects.d.ts +88 -39
- package/dist/renderables/effects.js +444 -105
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.d.ts +3 -2
- package/dist/renderables/flexStrip.js +6 -1
- package/dist/renderables/frames.d.ts +116 -0
- package/dist/renderables/frames.js +133 -0
- package/dist/renderables/group.d.ts +3 -2
- package/dist/renderables/group.js +5 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +3 -2
- package/dist/renderables/layout.js +15 -2
- package/dist/renderables/list-view.d.ts +3 -2
- package/dist/renderables/list-view.js +12 -5
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +69 -35
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1421 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +7 -6
- package/dist/renderables/markdown.js +363 -499
- package/dist/renderables/padding.d.ts +8 -20
- package/dist/renderables/padding.js +32 -50
- package/dist/renderables/panel.d.ts +26 -26
- package/dist/renderables/panel.js +67 -103
- package/dist/renderables/progress.d.ts +5 -3
- package/dist/renderables/progress.js +14 -3
- package/dist/renderables/progressBar.js +4 -4
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +24 -0
- package/dist/renderables/spinner.d.ts +73 -27
- package/dist/renderables/spinner.js +110 -51
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +19 -11
- package/dist/renderables/table.js +140 -156
- package/dist/renderables/traceback.d.ts +1 -1
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +6 -5
- package/dist/renderables/tree.js +11 -7
- package/dist/renderables/viewport.d.ts +13 -5
- package/dist/renderables/viewport.js +41 -20
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +8 -3
- package/dist/themes/ramp.d.ts +18 -3
- package/dist/themes/ramp.js +23 -2
- package/dist/widgets/focus-manager.d.ts +21 -14
- package/dist/widgets/focus-manager.js +28 -29
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/static-item.d.ts +4 -3
- package/dist/widgets/static-item.js +9 -7
- package/dist/widgets/text-input.d.ts +28 -18
- package/dist/widgets/text-input.js +144 -101
- package/dist/widgets/widget-app.d.ts +19 -3
- package/dist/widgets/widget-app.js +39 -21
- package/dist/widgets/widget-base.js +0 -3
- package/package.json +2 -5
package/dist/core/style.js
CHANGED
|
@@ -94,7 +94,26 @@ export class Style {
|
|
|
94
94
|
* unequal, because they came from different places.
|
|
95
95
|
*/
|
|
96
96
|
anchor;
|
|
97
|
+
/**
|
|
98
|
+
* When what this style's cells show next changes, in seconds on the
|
|
99
|
+
* timeline of whoever drew the frame — `t` itself for a cell drawn by
|
|
100
|
+
* something that moves every frame, and `Infinity`, the default, for one
|
|
101
|
+
* that never changes. It draws nothing. Whatever draws for a handed time
|
|
102
|
+
* stamps its cells with it (`Frames`, `Effected`), the stamp rides up
|
|
103
|
+
* through every container as the anchor does (`./anchor.ts`), and
|
|
104
|
+
* whoever holds the composed frame reads the soonest off it
|
|
105
|
+
* (`Segment.nextChange`), so a host knows when to draw again without
|
|
106
|
+
* asking each container. Two styles laid together change at the sooner.
|
|
107
|
+
*/
|
|
108
|
+
nextChange;
|
|
97
109
|
constructor(options = {}) {
|
|
110
|
+
const nextChange = options.nextChange ?? Number.POSITIVE_INFINITY;
|
|
111
|
+
// [LAW:parse-dont-validate] A time or never, settled where the style is
|
|
112
|
+
// made: NaN and -Infinity fail here, so no reader of a frame meets one.
|
|
113
|
+
if (!(nextChange > Number.NEGATIVE_INFINITY)) {
|
|
114
|
+
throw new RangeError(`Style nextChange must be a time in seconds or Infinity, got ${nextChange}`);
|
|
115
|
+
}
|
|
116
|
+
this.nextChange = nextChange;
|
|
98
117
|
this.color = resolveColor(options.color);
|
|
99
118
|
this.bgcolor = resolveBackground(options.bgcolor);
|
|
100
119
|
this.bold = options.bold;
|
|
@@ -110,7 +129,8 @@ export class Style {
|
|
|
110
129
|
this.frame = options.frame;
|
|
111
130
|
this.encircle = options.encircle;
|
|
112
131
|
this.overline = options.overline;
|
|
113
|
-
|
|
132
|
+
// As the reference's `self._link`, read only for truth: an empty URL is no link.
|
|
133
|
+
this.link = options.link || undefined;
|
|
114
134
|
this.meta = options.meta;
|
|
115
135
|
this.anchor = options.anchor;
|
|
116
136
|
}
|
|
@@ -132,7 +152,8 @@ export class Style {
|
|
|
132
152
|
this.overline === undefined &&
|
|
133
153
|
this.link === undefined &&
|
|
134
154
|
this.meta === undefined &&
|
|
135
|
-
this.anchor === undefined
|
|
155
|
+
this.anchor === undefined &&
|
|
156
|
+
this.nextChange === Number.POSITIVE_INFINITY);
|
|
136
157
|
}
|
|
137
158
|
get transparentBackground() {
|
|
138
159
|
return this.bgcolor === undefined || this.bgcolor.isDefault;
|
|
@@ -204,6 +225,8 @@ export class Style {
|
|
|
204
225
|
? { ...this.meta, ...other.meta }
|
|
205
226
|
: other.meta ?? this.meta,
|
|
206
227
|
anchor: other.anchor ?? this.anchor,
|
|
228
|
+
// Cells under both change when either does: the sooner.
|
|
229
|
+
nextChange: Math.min(this.nextChange, other.nextChange),
|
|
207
230
|
});
|
|
208
231
|
}
|
|
209
232
|
equals(other) {
|
|
@@ -225,7 +248,8 @@ export class Style {
|
|
|
225
248
|
this.encircle === other.encircle &&
|
|
226
249
|
this.overline === other.overline &&
|
|
227
250
|
this.link === other.link &&
|
|
228
|
-
anchorsEqual(this.anchor, other.anchor)
|
|
251
|
+
anchorsEqual(this.anchor, other.anchor) &&
|
|
252
|
+
this.nextChange === other.nextChange);
|
|
229
253
|
}
|
|
230
254
|
/**
|
|
231
255
|
* The colours this style puts on screen at `colorSystem`: each flattened
|
|
@@ -273,7 +297,11 @@ export class Style {
|
|
|
273
297
|
].join(";");
|
|
274
298
|
}
|
|
275
299
|
/**
|
|
276
|
-
*
|
|
300
|
+
* The look as a canonical string that round-trips through `Style.parse()`:
|
|
301
|
+
* colours, attributes and the link, which is all a style string can spell.
|
|
302
|
+
* What a style carries beyond its look — `meta`, `anchor`, `nextChange` —
|
|
303
|
+
* has no spelling and is not here, so two styles that differ only there
|
|
304
|
+
* read alike; `equals` tells them apart.
|
|
277
305
|
*/
|
|
278
306
|
toString() {
|
|
279
307
|
if (this.isNull)
|
|
@@ -668,16 +696,28 @@ export const DEFAULT_STYLES = {
|
|
|
668
696
|
"syntax.line_number.highlight": Style.parse("bold grey93 on grey27"),
|
|
669
697
|
// Markdown
|
|
670
698
|
"markdown.h1": Style.parse("bold underline"),
|
|
671
|
-
"markdown.h2": Style.parse("
|
|
672
|
-
"markdown.h3": Style.parse("bold
|
|
673
|
-
"markdown.h4": Style.parse("
|
|
699
|
+
"markdown.h2": Style.parse("underline magenta"),
|
|
700
|
+
"markdown.h3": Style.parse("bold magenta"),
|
|
701
|
+
"markdown.h4": Style.parse("italic magenta"),
|
|
702
|
+
"markdown.h5": Style.parse("italic"),
|
|
703
|
+
"markdown.h6": Style.parse("dim"),
|
|
674
704
|
// A fixed cyan, not the theme's: `cyan` is dark enough on some themes to
|
|
675
705
|
// sink into grey11. Bold, as Rich's is: at 16 colours a renderable draws it
|
|
676
706
|
// without its pair (`groundedStyle`), and bold is what still marks code.
|
|
677
707
|
"markdown.code": Style.parse("bold dark_turquoise on grey11"),
|
|
678
|
-
"markdown.
|
|
708
|
+
"markdown.em": Style.parse("italic"),
|
|
709
|
+
"markdown.strong": Style.parse("bold"),
|
|
710
|
+
"markdown.s": Style.parse("strike"),
|
|
711
|
+
"markdown.kbd": Style.parse("bold bright_yellow"),
|
|
712
|
+
"markdown.block_quote": Style.parse("magenta"),
|
|
713
|
+
"markdown.hr": Style.parse("dim"),
|
|
714
|
+
"markdown.item": NULL_STYLE,
|
|
715
|
+
"markdown.item.bullet": Style.parse("bold"),
|
|
716
|
+
"markdown.item.number": Style.parse("cyan"),
|
|
679
717
|
"markdown.link": Style.parse("bright_blue"),
|
|
680
|
-
"markdown.link_url": Style.parse("blue"),
|
|
718
|
+
"markdown.link_url": Style.parse("underline blue"),
|
|
719
|
+
"markdown.table.border": Style.parse("cyan"),
|
|
720
|
+
"markdown.table.header": Style.parse("not bold cyan"),
|
|
681
721
|
// Progress
|
|
682
722
|
"progress.description": NULL_STYLE,
|
|
683
723
|
"progress.percentage": Style.parse("magenta"),
|
package/dist/core/text.js
CHANGED
|
@@ -9,10 +9,20 @@ import { stripOscTerminators } from "./osc8.js";
|
|
|
9
9
|
import { drawable, getStyle, withBoundedWidth } from "./protocol.js";
|
|
10
10
|
// Every C0 control and DEL except \t and \n. Rich's `Text` strips only
|
|
11
11
|
// `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR); the rest of the set is the
|
|
12
|
-
// port's extension, so no
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
12
|
+
// port's extension, so no byte of content can start an escape sequence or move
|
|
13
|
+
// the cursor on the wire: the C1 controls (U+0080–U+009F) go as well, since a
|
|
14
|
+
// terminal reads U+009B as CSI and U+009D as OSC (TextInput refuses them for
|
|
15
|
+
// the same reason). So do the format controls that hold state past the text,
|
|
16
|
+
// also the port's extension: the bidi embeddings and overrides with their
|
|
17
|
+
// closer PDF (U+202A–U+202E), the isolates with their closer PDI
|
|
18
|
+
// (U+2066–U+2069), and the deprecated shaping and swapping switches
|
|
19
|
+
// (U+206A–U+206F). An opener left unclosed in content reverses whatever is
|
|
20
|
+
// drawn after it on the line, and a closer whose opener is gone closes
|
|
21
|
+
// nothing of the text's own. The bidi marks (LRM, RLM, ALM) hold no state and
|
|
22
|
+
// pass, since right-to-left text needs them.
|
|
23
|
+
// [LAW:single-enforcer] Single place where control chars are sanitized: every
|
|
24
|
+
// string that becomes part of a Text's characters passes here.
|
|
25
|
+
const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u206F]/g;
|
|
16
26
|
export function stripControlChars(text) {
|
|
17
27
|
return text.replace(CONTROL_CHARS_RE, "");
|
|
18
28
|
}
|
|
@@ -73,7 +83,10 @@ function sanitizeStyleLink(style) {
|
|
|
73
83
|
* [LAW:single-enforcer] Nor does a RichText keep an anchor (`./anchor.ts`). It
|
|
74
84
|
* lays its text out anew, so a stamp naming where a cell sat in the output it
|
|
75
85
|
* came from would name the wrong cell once it wraps, slices or overlaps; the
|
|
76
|
-
* owner that renders the RichText is the one that stamps its cells.
|
|
86
|
+
* owner that renders the RichText is the one that stamps its cells. A
|
|
87
|
+
* `nextChange` stays: when a span's cells next change is true of them
|
|
88
|
+
* wherever the text lays them, so a frame appended into a line still says
|
|
89
|
+
* when it turns.
|
|
77
90
|
*/
|
|
78
91
|
function admitStyle(style) {
|
|
79
92
|
if (typeof style === "string")
|
|
@@ -470,10 +483,9 @@ export class RichText {
|
|
|
470
483
|
return this;
|
|
471
484
|
}
|
|
472
485
|
pad(count, char) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this._spans = this._spans.map((s) => s.move(count));
|
|
486
|
+
const padding = stripControlChars(char ?? " ").repeat(count);
|
|
487
|
+
// Shift all spans right by what the left side gained.
|
|
488
|
+
this._spans = this._spans.map((s) => s.move(padding.length));
|
|
477
489
|
this._text = padding + this._text + padding;
|
|
478
490
|
return this;
|
|
479
491
|
}
|
|
@@ -489,14 +501,13 @@ export class RichText {
|
|
|
489
501
|
return this;
|
|
490
502
|
}
|
|
491
503
|
padLeft(count, char) {
|
|
492
|
-
const
|
|
493
|
-
this._spans = this._spans.map((s) => s.move(
|
|
494
|
-
this._text =
|
|
504
|
+
const padding = stripControlChars(char ?? " ").repeat(count);
|
|
505
|
+
this._spans = this._spans.map((s) => s.move(padding.length));
|
|
506
|
+
this._text = padding + this._text;
|
|
495
507
|
return this;
|
|
496
508
|
}
|
|
497
509
|
padRight(count, char) {
|
|
498
|
-
|
|
499
|
-
this._text += c.repeat(count);
|
|
510
|
+
this._text += stripControlChars(char ?? " ").repeat(count);
|
|
500
511
|
return this;
|
|
501
512
|
}
|
|
502
513
|
setLength(length) {
|
|
@@ -555,7 +566,7 @@ export class RichText {
|
|
|
555
566
|
// value, so width 0 and a marker wider than the width run the same walk
|
|
556
567
|
// as every other call: what is drawn plus what is kept is `cells` wide.
|
|
557
568
|
const cells = cellCount(width);
|
|
558
|
-
const marker = cellFit(options?.marker ?? "\u2026", cells);
|
|
569
|
+
const marker = cellFit(stripControlChars(options?.marker ?? "\u2026"), cells);
|
|
559
570
|
const budget = asCellCol(cells - cellLen(marker));
|
|
560
571
|
if (mode === "right") {
|
|
561
572
|
this._cropRightTo(budget);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline — the map from the host's clock to the time an animation is
|
|
3
|
+
* asked for: playback control, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Every animation is a pure function of its time (`./animation.ts`), so
|
|
6
|
+
* pausing, reversing, scrubbing and speeding one up are not things done to
|
|
7
|
+
* it but a change in which time it is asked for. A timeline is that map, as
|
|
8
|
+
* a value: an animation time at a host time, a rate — animation seconds per
|
|
9
|
+
* host second, negative running it backwards — and whether it is held. Each
|
|
10
|
+
* operation is a value in and a value out, so a paused timeline resumed at a
|
|
11
|
+
* new host time is one that maps that host time to the time it froze at, and
|
|
12
|
+
* two hosts holding equal timelines ask every animation for the same time.
|
|
13
|
+
*
|
|
14
|
+
* A paused timeline carries no host anchor: it maps every host time to the
|
|
15
|
+
* one time it holds, so there is no origin for it to be wrong about.
|
|
16
|
+
* [LAW:types-are-the-program]
|
|
17
|
+
*
|
|
18
|
+
* `run` is an animation on host time. Its `nextChange` is the host moment
|
|
19
|
+
* the mapped time reaches the animation's own answer, which is what
|
|
20
|
+
* `Spinner`'s speed scaled its holds for before this existed: an answer a
|
|
21
|
+
* host reads is on the host's timeline, and a beat over scaled time answers
|
|
22
|
+
* on the scaled one. Run backwards, the host next sees a change when the
|
|
23
|
+
* mapped time drops under where the value began — the animation's
|
|
24
|
+
* `lastChange` — so a reversed loop turns exactly at its hold boundaries,
|
|
25
|
+
* and a reversed one-shot changes at its switch and never before its start.
|
|
26
|
+
* Paused, nothing changes: never, and never did. The host moment is the
|
|
27
|
+
* first host float the map carries past the edge, searched out from the sum
|
|
28
|
+
* `origin + (edge - time) / rate` (`firstReaching`): a host float is a step
|
|
29
|
+
* of `rate` on the animation's clock, so the floats between that sum and
|
|
30
|
+
* the first one past the edge are as many as the two clocks' resolutions
|
|
31
|
+
* are apart — unbounded, far from the origin or at a slow rate — which is
|
|
32
|
+
* why it is a search and not the ulp walk a sum on one clock takes. A value
|
|
33
|
+
* moving at the mapped time changes at the host time itself.
|
|
34
|
+
*
|
|
35
|
+
* [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. The host
|
|
36
|
+
* time is an argument everywhere, and a timeline is built from one the host
|
|
37
|
+
* hands in, so the same host time maps to the same animation time at one
|
|
38
|
+
* sample a second or sixty.
|
|
39
|
+
*/
|
|
40
|
+
import type { Animation } from "./animation.js";
|
|
41
|
+
/**
|
|
42
|
+
* A map from host time to animation time. Playing, it is `time` at `origin`
|
|
43
|
+
* moving `rate` animation seconds a host second; paused, it is `time` at
|
|
44
|
+
* every host time, with the rate it resumes at.
|
|
45
|
+
*/
|
|
46
|
+
export type Timeline = {
|
|
47
|
+
readonly playing: true;
|
|
48
|
+
/** The host time, in seconds, at which the animation time is `time`. */
|
|
49
|
+
readonly origin: number;
|
|
50
|
+
/** The animation time, in seconds, at `origin`. */
|
|
51
|
+
readonly time: number;
|
|
52
|
+
/** Animation seconds per host second; negative runs the animation backwards. Never zero: a held timeline is paused. */
|
|
53
|
+
readonly rate: number;
|
|
54
|
+
} | {
|
|
55
|
+
readonly playing: false;
|
|
56
|
+
/** The animation time, in seconds, held at every host time. */
|
|
57
|
+
readonly time: number;
|
|
58
|
+
/** The rate the timeline resumes at. */
|
|
59
|
+
readonly rate: number;
|
|
60
|
+
};
|
|
61
|
+
export declare const Timeline: {
|
|
62
|
+
/** Playing from host time `origin`, at which the animation time is 0, at `rate` animation seconds a host second. */
|
|
63
|
+
play(origin: number, rate?: number): Timeline;
|
|
64
|
+
/** The animation time at host time `host`. */
|
|
65
|
+
at(timeline: Timeline, host: number): number;
|
|
66
|
+
/** `timeline` held at the animation time it maps `host` to. Pausing a paused timeline holds the same time. */
|
|
67
|
+
pause(timeline: Timeline, host: number): Timeline;
|
|
68
|
+
/** `timeline` playing from `host` at the animation time it maps `host` to, at its rate. Resuming a playing timeline is the same map. */
|
|
69
|
+
resume(timeline: Timeline, host: number): Timeline;
|
|
70
|
+
/** `timeline` with the animation at `time` at host time `host`, playing or held as it was. */
|
|
71
|
+
seek(timeline: Timeline, host: number, time: number): Timeline;
|
|
72
|
+
/** `timeline` at `rate` from host time `host` on, with no jump: the animation time at `host` is what it was. */
|
|
73
|
+
withRate(timeline: Timeline, host: number, rate: number): Timeline;
|
|
74
|
+
/**
|
|
75
|
+
* `animation` on host time: its value at the time `timeline` maps each
|
|
76
|
+
* host time to, changing at the host moment the mapped time reaches the
|
|
77
|
+
* animation's own answer — backwards, when it drops under where the value
|
|
78
|
+
* began. Paused, the value never changes and never did. The value is the
|
|
79
|
+
* animation's own, on no clock: what a drawn value's cells say about when
|
|
80
|
+
* they change is stamped by whoever draws it at a host time
|
|
81
|
+
* (`../renderables/frames.ts`, `frameAt`), never carried in from here.
|
|
82
|
+
*/
|
|
83
|
+
run<T>(timeline: Timeline, animation: Animation<T>): Animation<T>;
|
|
84
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline — the map from the host's clock to the time an animation is
|
|
3
|
+
* asked for: playback control, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Every animation is a pure function of its time (`./animation.ts`), so
|
|
6
|
+
* pausing, reversing, scrubbing and speeding one up are not things done to
|
|
7
|
+
* it but a change in which time it is asked for. A timeline is that map, as
|
|
8
|
+
* a value: an animation time at a host time, a rate — animation seconds per
|
|
9
|
+
* host second, negative running it backwards — and whether it is held. Each
|
|
10
|
+
* operation is a value in and a value out, so a paused timeline resumed at a
|
|
11
|
+
* new host time is one that maps that host time to the time it froze at, and
|
|
12
|
+
* two hosts holding equal timelines ask every animation for the same time.
|
|
13
|
+
*
|
|
14
|
+
* A paused timeline carries no host anchor: it maps every host time to the
|
|
15
|
+
* one time it holds, so there is no origin for it to be wrong about.
|
|
16
|
+
* [LAW:types-are-the-program]
|
|
17
|
+
*
|
|
18
|
+
* `run` is an animation on host time. Its `nextChange` is the host moment
|
|
19
|
+
* the mapped time reaches the animation's own answer, which is what
|
|
20
|
+
* `Spinner`'s speed scaled its holds for before this existed: an answer a
|
|
21
|
+
* host reads is on the host's timeline, and a beat over scaled time answers
|
|
22
|
+
* on the scaled one. Run backwards, the host next sees a change when the
|
|
23
|
+
* mapped time drops under where the value began — the animation's
|
|
24
|
+
* `lastChange` — so a reversed loop turns exactly at its hold boundaries,
|
|
25
|
+
* and a reversed one-shot changes at its switch and never before its start.
|
|
26
|
+
* Paused, nothing changes: never, and never did. The host moment is the
|
|
27
|
+
* first host float the map carries past the edge, searched out from the sum
|
|
28
|
+
* `origin + (edge - time) / rate` (`firstReaching`): a host float is a step
|
|
29
|
+
* of `rate` on the animation's clock, so the floats between that sum and
|
|
30
|
+
* the first one past the edge are as many as the two clocks' resolutions
|
|
31
|
+
* are apart — unbounded, far from the origin or at a slow rate — which is
|
|
32
|
+
* why it is a search and not the ulp walk a sum on one clock takes. A value
|
|
33
|
+
* moving at the mapped time changes at the host time itself.
|
|
34
|
+
*
|
|
35
|
+
* [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. The host
|
|
36
|
+
* time is an argument everywhere, and a timeline is built from one the host
|
|
37
|
+
* hands in, so the same host time maps to the same animation time at one
|
|
38
|
+
* sample a second or sixty.
|
|
39
|
+
*/
|
|
40
|
+
import { firstReaching } from "./easing.js";
|
|
41
|
+
/** Throws, naming `what`, unless `seconds` is a finite number of seconds. */
|
|
42
|
+
function finiteSeconds(what, seconds) {
|
|
43
|
+
if (!Number.isFinite(seconds))
|
|
44
|
+
throw new RangeError(`${what} must be a finite number of seconds, got ${seconds}`);
|
|
45
|
+
return seconds;
|
|
46
|
+
}
|
|
47
|
+
/** [LAW:single-enforcer] A host time is a finite number of seconds, refused once, at every operation that takes one. */
|
|
48
|
+
const hostTime = (host) => finiteSeconds("Timeline host time", host);
|
|
49
|
+
/**
|
|
50
|
+
* [LAW:parse-dont-validate] A rate is a finite number other than zero,
|
|
51
|
+
* settled where it is given: a rate of zero would be a second way to hold a
|
|
52
|
+
* timeline still, and `pause` is the one.
|
|
53
|
+
*/
|
|
54
|
+
function playbackRate(rate) {
|
|
55
|
+
if (!Number.isFinite(rate) || rate === 0)
|
|
56
|
+
throw new RangeError(`Timeline rate must be finite and not zero, got ${rate}`);
|
|
57
|
+
return rate;
|
|
58
|
+
}
|
|
59
|
+
/** `timeline` with `time` at host time `host`, at `rate`, held as it was. A paused timeline keeps no origin, and refuses a host time it cannot use all the same. */
|
|
60
|
+
function anchored(timeline, host, time, rate) {
|
|
61
|
+
const origin = hostTime(host);
|
|
62
|
+
return timeline.playing ? { playing: true, origin, time, rate } : { playing: false, time, rate };
|
|
63
|
+
}
|
|
64
|
+
/** `animation` asked at the time `playing` maps each host time to, saying on host time when it changes. */
|
|
65
|
+
function played(animation, { origin, time, rate }) {
|
|
66
|
+
const mapped = (host) => time + rate * (host - origin);
|
|
67
|
+
const forward = rate > 0;
|
|
68
|
+
// Forwards, the host next sees the animation's own next change, and the
|
|
69
|
+
// value began when the animation says it did; backwards, it next sees a
|
|
70
|
+
// change where the value began, and the value began where the animation
|
|
71
|
+
// says it ends. A hold is `[lastChange, nextChange)`, so the far side of
|
|
72
|
+
// an edge is at it going up and strictly under it coming down.
|
|
73
|
+
const ahead = forward ? (t) => animation.nextChange(t) : (t) => animation.lastChange(t);
|
|
74
|
+
const behind = forward ? (t) => animation.lastChange(t) : (t) => animation.nextChange(t);
|
|
75
|
+
const past = forward ? (t, edge) => t >= edge : (t, edge) => t < edge;
|
|
76
|
+
// The first host moment the map carries past `edge`, an animation time.
|
|
77
|
+
// An edge at infinity, or one so far off at this rate that no host float
|
|
78
|
+
// reaches it, is never reached, forwards or backwards: the sum's own sign
|
|
79
|
+
// says which, as a host moment past either end of the host's clock.
|
|
80
|
+
const hostOf = (edge) => {
|
|
81
|
+
const sum = origin + (edge - time) / rate;
|
|
82
|
+
return Number.isFinite(sum) ? firstReaching(sum, (host) => past(mapped(host), edge)) : sum;
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
at: (host) => animation.at(mapped(hostTime(host))),
|
|
86
|
+
nextChange(host) {
|
|
87
|
+
const h = hostTime(host);
|
|
88
|
+
const t = mapped(h);
|
|
89
|
+
const edge = ahead(t);
|
|
90
|
+
return edge === t ? h : Math.max(h, hostOf(edge));
|
|
91
|
+
},
|
|
92
|
+
lastChange(host) {
|
|
93
|
+
const h = hostTime(host);
|
|
94
|
+
const t = mapped(h);
|
|
95
|
+
const edge = behind(t);
|
|
96
|
+
return edge === t ? h : Math.min(h, hostOf(edge));
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export const Timeline = {
|
|
101
|
+
/** Playing from host time `origin`, at which the animation time is 0, at `rate` animation seconds a host second. */
|
|
102
|
+
play(origin, rate = 1) {
|
|
103
|
+
return { playing: true, origin: finiteSeconds("Timeline.play origin", origin), time: 0, rate: playbackRate(rate) };
|
|
104
|
+
},
|
|
105
|
+
/** The animation time at host time `host`. */
|
|
106
|
+
at(timeline, host) {
|
|
107
|
+
const h = hostTime(host);
|
|
108
|
+
return timeline.playing ? timeline.time + timeline.rate * (h - timeline.origin) : timeline.time;
|
|
109
|
+
},
|
|
110
|
+
/** `timeline` held at the animation time it maps `host` to. Pausing a paused timeline holds the same time. */
|
|
111
|
+
pause(timeline, host) {
|
|
112
|
+
return { playing: false, time: Timeline.at(timeline, host), rate: timeline.rate };
|
|
113
|
+
},
|
|
114
|
+
/** `timeline` playing from `host` at the animation time it maps `host` to, at its rate. Resuming a playing timeline is the same map. */
|
|
115
|
+
resume(timeline, host) {
|
|
116
|
+
return { playing: true, origin: hostTime(host), time: Timeline.at(timeline, host), rate: timeline.rate };
|
|
117
|
+
},
|
|
118
|
+
/** `timeline` with the animation at `time` at host time `host`, playing or held as it was. */
|
|
119
|
+
seek(timeline, host, time) {
|
|
120
|
+
return anchored(timeline, host, finiteSeconds("Timeline.seek time", time), timeline.rate);
|
|
121
|
+
},
|
|
122
|
+
/** `timeline` at `rate` from host time `host` on, with no jump: the animation time at `host` is what it was. */
|
|
123
|
+
withRate(timeline, host, rate) {
|
|
124
|
+
return anchored(timeline, host, Timeline.at(timeline, host), playbackRate(rate));
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* `animation` on host time: its value at the time `timeline` maps each
|
|
128
|
+
* host time to, changing at the host moment the mapped time reaches the
|
|
129
|
+
* animation's own answer — backwards, when it drops under where the value
|
|
130
|
+
* began. Paused, the value never changes and never did. The value is the
|
|
131
|
+
* animation's own, on no clock: what a drawn value's cells say about when
|
|
132
|
+
* they change is stamped by whoever draws it at a host time
|
|
133
|
+
* (`../renderables/frames.ts`, `frameAt`), never carried in from here.
|
|
134
|
+
*/
|
|
135
|
+
run(timeline, animation) {
|
|
136
|
+
if (timeline.playing)
|
|
137
|
+
return played(animation, timeline);
|
|
138
|
+
return {
|
|
139
|
+
at: (host) => animation.at(Timeline.at(timeline, host)),
|
|
140
|
+
nextChange: (host) => (hostTime(host), Number.POSITIVE_INFINITY),
|
|
141
|
+
lastChange: (host) => (hostTime(host), Number.NEGATIVE_INFINITY),
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tween — a value eased from one number to another over seconds, then held:
|
|
3
|
+
* the animation a progress bar's fill, a counter or a width is handed so it
|
|
4
|
+
* moves to its new value instead of jumping. The first `Animation` of a
|
|
5
|
+
* value that is never drawn as cells, so its `nextChange` is its only way of
|
|
6
|
+
* saying when to look again (`./animation.ts`).
|
|
7
|
+
*
|
|
8
|
+
* The value at `t` is the `Timing`'s ease of the way from `from` to `to`
|
|
9
|
+
* through `Phase.once(0, seconds)`: `from` at `t ≤ 0` whenever the ease
|
|
10
|
+
* starts at 0, as every built-in does but `step-start` and the steps that
|
|
11
|
+
* jump at the start of their interval, which show their first jump before
|
|
12
|
+
* the tween begins, as `ColorRamp.at` does; `to` from `seconds` on. It is
|
|
13
|
+
* the one-shot phase every effect transition reads time
|
|
14
|
+
* through, so there is no second rule about when a one-shot begins: it
|
|
15
|
+
* begins at 0 on its own clock, and starting it later is `Animation.sequence`'s
|
|
16
|
+
* or a `Timeline`'s job. [LAW:single-enforcer] An ease that overshoots is
|
|
17
|
+
* shown as it is — a count eased by `back-out` passes its target and comes
|
|
18
|
+
* back, which is what the curve is for — and lands exactly on `to`, because
|
|
19
|
+
* every built-in ease is exactly 1 at 1 and the blend is `(1 − p)·from +
|
|
20
|
+
* p·to`, exact at both ends where `from + (to − from)·p` is not.
|
|
21
|
+
*
|
|
22
|
+
* A tween keeps nothing between calls, so retargeting is not a mutation: a
|
|
23
|
+
* tween whose target changes at `t₁` is the old tween until `t₁`, then a new
|
|
24
|
+
* one from the old one's value there — `Animation.sequence(old, t₁,
|
|
25
|
+
* tween(old.at(t₁), to, timing))` — and starts from where it is because
|
|
26
|
+
* that is where it was built from. [LAW:no-ambient-temporal-coupling]
|
|
27
|
+
*
|
|
28
|
+
* Over colours it is a `ColorRamp` read over time (`../themes/ramp.ts`,
|
|
29
|
+
* `over`): the ramp is already the one place two colours are blended, in
|
|
30
|
+
* OKLCH, and a colour tween is a two-stop ramp over its seconds, not a
|
|
31
|
+
* second blender. [LAW:one-source-of-truth]
|
|
32
|
+
*
|
|
33
|
+
* Tier 2 of `src/core/`: `./easing.ts` and `./animation.ts`, nothing else.
|
|
34
|
+
* [LAW:one-way-deps]
|
|
35
|
+
*/
|
|
36
|
+
import { Animation } from "./animation.js";
|
|
37
|
+
import { type Timing } from "./easing.js";
|
|
38
|
+
/**
|
|
39
|
+
* `from` eased to `to` over `timing`, as an `Animation<number>`: moving from
|
|
40
|
+
* 0 to `timing.seconds` on its own clock, the ease of 0 before (`from`, for
|
|
41
|
+
* every ease that starts at 0), `to` after, and so
|
|
42
|
+
* seen at its end, never mid-way, by a host whose frames are further apart
|
|
43
|
+
* than it is long. `from` and `to` must be finite; the seconds are checked
|
|
44
|
+
* where every one-shot's are, in `Phase.once`. A tween to where it already
|
|
45
|
+
* is is `Animation.constant(from)`, decided here once: the blend would land
|
|
46
|
+
* one ulp off `from` at some `p`, and a value that never moves is one that
|
|
47
|
+
* never changes, so a host is told not to look again. [LAW:parse-dont-validate]
|
|
48
|
+
*/
|
|
49
|
+
export declare function tween(from: number, to: number, timing: Timing): Animation<number>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tween — a value eased from one number to another over seconds, then held:
|
|
3
|
+
* the animation a progress bar's fill, a counter or a width is handed so it
|
|
4
|
+
* moves to its new value instead of jumping. The first `Animation` of a
|
|
5
|
+
* value that is never drawn as cells, so its `nextChange` is its only way of
|
|
6
|
+
* saying when to look again (`./animation.ts`).
|
|
7
|
+
*
|
|
8
|
+
* The value at `t` is the `Timing`'s ease of the way from `from` to `to`
|
|
9
|
+
* through `Phase.once(0, seconds)`: `from` at `t ≤ 0` whenever the ease
|
|
10
|
+
* starts at 0, as every built-in does but `step-start` and the steps that
|
|
11
|
+
* jump at the start of their interval, which show their first jump before
|
|
12
|
+
* the tween begins, as `ColorRamp.at` does; `to` from `seconds` on. It is
|
|
13
|
+
* the one-shot phase every effect transition reads time
|
|
14
|
+
* through, so there is no second rule about when a one-shot begins: it
|
|
15
|
+
* begins at 0 on its own clock, and starting it later is `Animation.sequence`'s
|
|
16
|
+
* or a `Timeline`'s job. [LAW:single-enforcer] An ease that overshoots is
|
|
17
|
+
* shown as it is — a count eased by `back-out` passes its target and comes
|
|
18
|
+
* back, which is what the curve is for — and lands exactly on `to`, because
|
|
19
|
+
* every built-in ease is exactly 1 at 1 and the blend is `(1 − p)·from +
|
|
20
|
+
* p·to`, exact at both ends where `from + (to − from)·p` is not.
|
|
21
|
+
*
|
|
22
|
+
* A tween keeps nothing between calls, so retargeting is not a mutation: a
|
|
23
|
+
* tween whose target changes at `t₁` is the old tween until `t₁`, then a new
|
|
24
|
+
* one from the old one's value there — `Animation.sequence(old, t₁,
|
|
25
|
+
* tween(old.at(t₁), to, timing))` — and starts from where it is because
|
|
26
|
+
* that is where it was built from. [LAW:no-ambient-temporal-coupling]
|
|
27
|
+
*
|
|
28
|
+
* Over colours it is a `ColorRamp` read over time (`../themes/ramp.ts`,
|
|
29
|
+
* `over`): the ramp is already the one place two colours are blended, in
|
|
30
|
+
* OKLCH, and a colour tween is a two-stop ramp over its seconds, not a
|
|
31
|
+
* second blender. [LAW:one-source-of-truth]
|
|
32
|
+
*
|
|
33
|
+
* Tier 2 of `src/core/`: `./easing.ts` and `./animation.ts`, nothing else.
|
|
34
|
+
* [LAW:one-way-deps]
|
|
35
|
+
*/
|
|
36
|
+
import { Animation } from "./animation.js";
|
|
37
|
+
import { Phase } from "./easing.js";
|
|
38
|
+
/**
|
|
39
|
+
* `from` eased to `to` over `timing`, as an `Animation<number>`: moving from
|
|
40
|
+
* 0 to `timing.seconds` on its own clock, the ease of 0 before (`from`, for
|
|
41
|
+
* every ease that starts at 0), `to` after, and so
|
|
42
|
+
* seen at its end, never mid-way, by a host whose frames are further apart
|
|
43
|
+
* than it is long. `from` and `to` must be finite; the seconds are checked
|
|
44
|
+
* where every one-shot's are, in `Phase.once`. A tween to where it already
|
|
45
|
+
* is is `Animation.constant(from)`, decided here once: the blend would land
|
|
46
|
+
* one ulp off `from` at some `p`, and a value that never moves is one that
|
|
47
|
+
* never changes, so a host is told not to look again. [LAW:parse-dont-validate]
|
|
48
|
+
*/
|
|
49
|
+
export function tween(from, to, timing) {
|
|
50
|
+
for (const [name, v] of [["from", from], ["to", to]]) {
|
|
51
|
+
if (!Number.isFinite(v))
|
|
52
|
+
throw new RangeError(`tween ${name} must be finite, got ${v}`);
|
|
53
|
+
}
|
|
54
|
+
const { ease, seconds } = timing;
|
|
55
|
+
const phase = Phase.once(0, seconds);
|
|
56
|
+
if (from === to)
|
|
57
|
+
return Animation.constant(from);
|
|
58
|
+
return {
|
|
59
|
+
at(t) {
|
|
60
|
+
const p = ease(phase(t));
|
|
61
|
+
return (1 - p) * from + p * to;
|
|
62
|
+
},
|
|
63
|
+
// Before it begins the value is held until 0; while it moves, every
|
|
64
|
+
// frame; from `seconds` on, never again — and it became what it is at
|
|
65
|
+
// `seconds`, or always was before it began.
|
|
66
|
+
nextChange: (t) => (t < 0 ? 0 : t < seconds ? t : Number.POSITIVE_INFINITY),
|
|
67
|
+
lastChange: (t) => (t < 0 ? Number.NEGATIVE_INFINITY : t < seconds ? t : seconds),
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/host/app.js
CHANGED
|
@@ -34,7 +34,7 @@ import { frameRate, systemClock } from "../core/clock.js";
|
|
|
34
34
|
import { Console } from "../core/console.js";
|
|
35
35
|
import { Segment } from "../core/segment.js";
|
|
36
36
|
import { SurfacePainter } from "../core/paint.js";
|
|
37
|
-
import { fitHeight } from "../core/protocol.js";
|
|
37
|
+
import { fitHeight, render } from "../core/protocol.js";
|
|
38
38
|
import { hostEnvironment } from "./host-environment.js";
|
|
39
39
|
import { InputParser, keyName, parseKeyName } from "./input.js";
|
|
40
40
|
// [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
|
|
@@ -91,13 +91,11 @@ export class App {
|
|
|
91
91
|
mouseHandlers = new Set();
|
|
92
92
|
wheelHandlers = new Set();
|
|
93
93
|
quitKeys;
|
|
94
|
-
input
|
|
95
|
-
key: (press) => this.key(press),
|
|
96
|
-
mouse: (event) => this.mouse(event),
|
|
97
|
-
});
|
|
94
|
+
input;
|
|
98
95
|
settle = () => { };
|
|
99
96
|
constructor(options) {
|
|
100
97
|
this.host = options.host;
|
|
98
|
+
this.input = new InputParser({ key: (press) => this.key(press), mouse: (event) => this.mouse(event) }, options.host.inputChunks);
|
|
101
99
|
this.surface = options.surface;
|
|
102
100
|
this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
|
|
103
101
|
this.view = options.view;
|
|
@@ -383,7 +381,7 @@ export class App {
|
|
|
383
381
|
const height = this.painter.height(options.height.rows);
|
|
384
382
|
// [LAW:no-ambient-temporal-coupling] The frame owner reads the clock,
|
|
385
383
|
// once a frame; the view is handed the time as data.
|
|
386
|
-
const lines = Segment.splitLines(this.view(this.clock.now())
|
|
384
|
+
const lines = Segment.splitLines(render(this.view(this.clock.now()), { ...options, height }));
|
|
387
385
|
// The app set the budget, so the app shapes what comes back: no deeper
|
|
388
386
|
// than the terminal — a taller frame would scroll the rows the painter
|
|
389
387
|
// goes back over — and, as a region, exactly that deep.
|
package/dist/host/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { BrowserTerminalHost } from "./terminal-host.js";
|
|
2
|
-
export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
2
|
+
export type { TerminalHost, TerminalSize, DataHandler, InputChunks, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
3
3
|
export { hostEnvironment } from "./host-environment.js";
|
|
4
4
|
export { App } from "./app.js";
|
|
5
5
|
export type { AppMouseEvent, AppOptions, AppPhase, Wheel } from "./app.js";
|