@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.
Files changed (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -5
@@ -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
- this.link = options.link;
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
- * Serializes to a canonical string that round-trips through `Style.parse()`.
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("bold"),
672
- "markdown.h3": Style.parse("bold dim"),
673
- "markdown.h4": Style.parse("bold dim italic"),
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.hr": Style.parse("yellow"),
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 C0 byte of content can start an escape sequence or
13
- // move the cursor on the wire. C1 controls (U+0080–U+009F) pass, as in Rich.
14
- // [LAW:single-enforcer] Single place where control chars are sanitized
15
- const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
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 c = char ?? " ";
474
- const padding = c.repeat(count);
475
- // Shift all spans to the right
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 c = char ?? " ";
493
- this._spans = this._spans.map((s) => s.move(count));
494
- this._text = c.repeat(count) + 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
- const c = char ?? " ";
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 = new InputParser({
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()).render({ ...options, height }));
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.
@@ -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";