@promptctl/rich-js 0.27.0 → 0.29.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.
Files changed (185) hide show
  1. package/README.md +7 -6
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +65 -46
  4. package/dist/core/animation.js +91 -67
  5. package/dist/core/ansi.d.ts +19 -12
  6. package/dist/core/ansi.js +114 -36
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +94 -6
  17. package/dist/core/clock.d.ts +1 -1
  18. package/dist/core/color.d.ts +56 -57
  19. package/dist/core/color.js +79 -81
  20. package/dist/core/console.d.ts +24 -7
  21. package/dist/core/console.js +27 -8
  22. package/dist/core/control-codes.d.ts +61 -0
  23. package/dist/core/control-codes.js +157 -0
  24. package/dist/core/easing.d.ts +40 -40
  25. package/dist/core/easing.js +40 -40
  26. package/dist/core/env.d.ts +6 -0
  27. package/dist/core/env.js +9 -1
  28. package/dist/core/export-html.js +5 -4
  29. package/dist/core/export-lines.d.ts +13 -11
  30. package/dist/core/export-lines.js +19 -15
  31. package/dist/core/export-svg.d.ts +3 -3
  32. package/dist/core/export-svg.js +10 -9
  33. package/dist/core/json.js +9 -2
  34. package/dist/core/markup.js +5 -4
  35. package/dist/core/memo.d.ts +1 -1
  36. package/dist/core/memo.js +1 -1
  37. package/dist/core/oklch.d.ts +14 -14
  38. package/dist/core/oklch.js +10 -10
  39. package/dist/core/orient.d.ts +68 -0
  40. package/dist/core/orient.js +224 -0
  41. package/dist/core/osc8.d.ts +12 -5
  42. package/dist/core/osc8.js +14 -7
  43. package/dist/core/paint.d.ts +9 -9
  44. package/dist/core/paint.js +3 -3
  45. package/dist/core/pretty.d.ts +1 -1
  46. package/dist/core/pretty.js +23 -19
  47. package/dist/core/protocol.d.ts +47 -26
  48. package/dist/core/protocol.js +31 -23
  49. package/dist/core/render.d.ts +19 -14
  50. package/dist/core/render.js +30 -17
  51. package/dist/core/seal.d.ts +26 -0
  52. package/dist/core/seal.js +75 -0
  53. package/dist/core/segment.d.ts +12 -4
  54. package/dist/core/segment.js +16 -6
  55. package/dist/core/strip.d.ts +83 -83
  56. package/dist/core/strip.js +70 -70
  57. package/dist/core/style.d.ts +46 -18
  58. package/dist/core/style.js +92 -56
  59. package/dist/core/text-order.d.ts +51 -0
  60. package/dist/core/text-order.js +50 -0
  61. package/dist/core/text.d.ts +12 -2
  62. package/dist/core/text.js +136 -90
  63. package/dist/core/timeline.d.ts +5 -5
  64. package/dist/core/timeline.js +11 -11
  65. package/dist/core/tween.d.ts +6 -6
  66. package/dist/core/tween.js +10 -10
  67. package/dist/core/view.d.ts +5 -5
  68. package/dist/core/view.js +15 -14
  69. package/dist/core/wrap.d.ts +24 -6
  70. package/dist/core/wrap.js +82 -16
  71. package/dist/host/app.d.ts +50 -9
  72. package/dist/host/app.js +120 -15
  73. package/dist/host/host-environment.d.ts +2 -2
  74. package/dist/host/host-environment.js +2 -2
  75. package/dist/host/index.d.ts +3 -2
  76. package/dist/host/index.js +1 -0
  77. package/dist/host/input.d.ts +42 -9
  78. package/dist/host/input.js +91 -7
  79. package/dist/host/terminal-colors.d.ts +18 -0
  80. package/dist/host/terminal-colors.js +46 -0
  81. package/dist/host/terminal-host.d.ts +4 -3
  82. package/dist/host/terminal-host.js +1 -1
  83. package/dist/index.d.ts +17 -16
  84. package/dist/index.js +11 -11
  85. package/dist/node/prompt.d.ts +1 -1
  86. package/dist/node/prompt.js +1 -1
  87. package/dist/node/terminal-host.d.ts +1 -1
  88. package/dist/node/traceback.js +2 -2
  89. package/dist/renderables/change.d.ts +1 -3
  90. package/dist/renderables/change.js +6 -8
  91. package/dist/renderables/effect.d.ts +88 -87
  92. package/dist/renderables/effect.js +124 -114
  93. package/dist/renderables/effects.d.ts +105 -76
  94. package/dist/renderables/effects.js +309 -176
  95. package/dist/renderables/embed.d.ts +1 -1
  96. package/dist/renderables/embed.js +2 -2
  97. package/dist/renderables/flexStrip.d.ts +7 -7
  98. package/dist/renderables/flexStrip.js +15 -15
  99. package/dist/renderables/frames.d.ts +7 -16
  100. package/dist/renderables/frames.js +12 -27
  101. package/dist/renderables/glyph-effect.d.ts +22 -23
  102. package/dist/renderables/glyph-effect.js +43 -41
  103. package/dist/renderables/glyph-effects.d.ts +33 -14
  104. package/dist/renderables/glyph-effects.js +90 -22
  105. package/dist/renderables/html-entities.d.ts +1 -1
  106. package/dist/renderables/html-entities.js +1 -1
  107. package/dist/renderables/lifecycle.d.ts +8 -10
  108. package/dist/renderables/lifecycle.js +17 -18
  109. package/dist/renderables/live.d.ts +2 -2
  110. package/dist/renderables/live.js +7 -7
  111. package/dist/renderables/markdown-parse.js +3 -3
  112. package/dist/renderables/markdown.js +4 -2
  113. package/dist/renderables/panel.js +7 -5
  114. package/dist/renderables/progress.d.ts +1 -1
  115. package/dist/renderables/progress.js +1 -1
  116. package/dist/renderables/progressBar.d.ts +3 -3
  117. package/dist/renderables/progressBar.js +21 -18
  118. package/dist/renderables/prompt.d.ts +1 -1
  119. package/dist/renderables/scroll.d.ts +1 -1
  120. package/dist/renderables/scroll.js +3 -3
  121. package/dist/renderables/spinner.d.ts +4 -4
  122. package/dist/renderables/spinner.js +10 -10
  123. package/dist/renderables/syntax.d.ts +2 -2
  124. package/dist/renderables/syntax.js +9 -4
  125. package/dist/renderables/table.js +5 -4
  126. package/dist/renderables/traceback.d.ts +1 -1
  127. package/dist/renderables/traceback.js +1 -1
  128. package/dist/renderables/tree.js +3 -3
  129. package/dist/template-bindings/animation-funcs.d.ts +60 -29
  130. package/dist/template-bindings/animation-funcs.js +126 -43
  131. package/dist/template-bindings/color-funcs.d.ts +14 -14
  132. package/dist/template-bindings/color-funcs.js +25 -25
  133. package/dist/template-bindings/index.d.ts +16 -14
  134. package/dist/template-bindings/index.js +17 -14
  135. package/dist/template-bindings/palette-funcs.d.ts +2 -2
  136. package/dist/template-bindings/palette-funcs.js +4 -4
  137. package/dist/template-bindings/style-funcs.d.ts +5 -5
  138. package/dist/template-bindings/style-funcs.js +16 -16
  139. package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
  140. package/dist/themes/{transpose.js → adjust.js} +28 -28
  141. package/dist/themes/buildPalette.d.ts +6 -5
  142. package/dist/themes/buildPalette.js +22 -22
  143. package/dist/themes/colorMath.d.ts +16 -16
  144. package/dist/themes/colorMath.js +32 -32
  145. package/dist/themes/data/ansi-tables.d.ts +2 -2
  146. package/dist/themes/data/ansi-tables.js +2 -2
  147. package/dist/themes/data/index.js +1 -4
  148. package/dist/themes/data/textual-ansi.js +0 -143
  149. package/dist/themes/data/types.d.ts +4 -4
  150. package/dist/themes/data/types.js +2 -2
  151. package/dist/themes/palette.d.ts +12 -12
  152. package/dist/themes/palette.js +16 -16
  153. package/dist/themes/ramp.d.ts +17 -17
  154. package/dist/themes/ramp.js +12 -12
  155. package/dist/themes/registry.d.ts +6 -8
  156. package/dist/themes/registry.js +6 -8
  157. package/dist/themes/terminalThemes.d.ts +4 -11
  158. package/dist/themes/terminalThemes.js +31 -47
  159. package/dist/widgets/button.d.ts +3 -3
  160. package/dist/widgets/button.js +8 -8
  161. package/dist/widgets/checkbox.d.ts +2 -2
  162. package/dist/widgets/checkbox.js +4 -4
  163. package/dist/widgets/dropdown.d.ts +3 -3
  164. package/dist/widgets/dropdown.js +7 -5
  165. package/dist/widgets/event-router.d.ts +6 -6
  166. package/dist/widgets/event-router.js +2 -2
  167. package/dist/widgets/focus-manager.d.ts +3 -3
  168. package/dist/widgets/focus-manager.js +3 -3
  169. package/dist/widgets/index.d.ts +2 -2
  170. package/dist/widgets/index.js +1 -1
  171. package/dist/widgets/ink.d.ts +3 -3
  172. package/dist/widgets/ink.js +4 -4
  173. package/dist/widgets/slider.d.ts +2 -2
  174. package/dist/widgets/static-item.d.ts +2 -2
  175. package/dist/widgets/text-input.d.ts +2 -2
  176. package/dist/widgets/text-input.js +5 -5
  177. package/dist/widgets/themed-widget.js +4 -4
  178. package/dist/widgets/toggle.d.ts +4 -4
  179. package/dist/widgets/toggle.js +6 -6
  180. package/dist/widgets/types.d.ts +6 -6
  181. package/dist/widgets/types.js +2 -2
  182. package/dist/widgets/widget-app.d.ts +10 -10
  183. package/dist/widgets/widget-app.js +8 -8
  184. package/dist/widgets/widget-base.d.ts +2 -2
  185. package/package.json +5 -4
@@ -2,23 +2,24 @@
2
2
  * Animation — a value for a time, and when it next changes: the one type
3
3
  * anything in rich-js that changes with time is an instance of.
4
4
  *
5
- * `at(t)` is the value at `t` seconds. `nextChange(t)` is the first moment,
5
+ * `at(t)` is the value at `t` seconds. `validUntil(t)` is the first moment,
6
6
  * on `t`'s own timeline, at which the value may stop being `at(t)`: never
7
7
  * before `t`. A later time says the value is held until then, `Infinity`
8
8
  * that it never changes again, and `t` itself that it is moving at `t` — it
9
9
  * changes every frame, however close together they are drawn. An early
10
10
  * answer costs a host a frame that draws what the last one did; a late one
11
11
  * skips a change. So an animation that cannot say answers `t`, as
12
- * `continuous` does. A value drawn as cells carries this answer on every
12
+ * `continuous` does. An answer that depends on what the frame drew, an
13
+ * effect's, is told by that drawing (`Reads`). A value drawn as cells carries this answer on every
13
14
  * cell (`Style.hold`, both ends of the hold below), so a host holding
14
- * the composed frame reads it there (`Segment.nextChange`); a value that is
15
- * not drawn — a number, a colour — has `nextChange` as its only way to say
15
+ * the composed frame reads it there (`Segment.validUntil`); a value that is
16
+ * not drawn — a number, a color — has `validUntil` as its only way to say
16
17
  * when to look again.
17
18
  *
18
- * `lastChange(t)` is the other end of the same hold: the last moment, never
19
+ * `validFrom(t)` is the other end of the same hold: the last moment, never
19
20
  * after `t`, at which the value may have become `at(t)` — `-Infinity` if it
20
- * always was, `t` while it moves. Together they bound the hold, `[lastChange(t),
21
- * nextChange(t))`, and the answer may be narrower than the truth and never
21
+ * always was, `t` while it moves. Together they bound the hold, `[validFrom(t),
22
+ * validUntil(t))`, and the answer may be narrower than the truth and never
22
23
  * wider. It is what a host running the animation backwards reads
23
24
  * (`./timeline.ts`): run down the timeline, the value next changes where it
24
25
  * began. An animation that cannot say answers `t` here too.
@@ -37,10 +38,10 @@
37
38
  * which content shows is a fact about time that motion is not: a value with
38
39
  * no time could only show a scheduled exit as already gone.
39
40
  *
40
- * The instances already in the tree: a `Beat` is an animation of which hold
41
+ * The instances already in the tree: a `StepSequence` is an animation of which hold
41
42
  * is in progress (`./easing.ts`), a `FrameSequence` of the frame shown and a
42
43
  * `Spinner` of the spinner drawn (`../renderables/`), and an `Effect` is, for
43
- * what an element draws in a frame, an animation of how its cells' colours
44
+ * what an element draws in a frame, an animation of how its cells' colors
44
45
  * move (`../renderables/effect.ts`). Every kind of motion is one more.
45
46
  *
46
47
  * An animation of renderables is a renderable — its face, `placedRender`:
@@ -66,17 +67,17 @@
66
67
  * of motion is built from — a constant, a value moving every moment, a map
67
68
  * over the value, several at once, one animation after another, and one
68
69
  * played again every period — and each says how
69
- * `nextChange` composes, so an animation built from them answers it without
70
+ * `validUntil` composes, so an animation built from them answers it without
70
71
  * its author doing anything.
71
72
  *
72
73
  * Tier 5 of `src/core/`: `./easing.ts`, `./measure.ts`, `./protocol.ts`,
73
74
  * `./segment.ts` and `./style.ts`, nothing else. [LAW:one-way-deps]
74
75
  */
75
- import { beat, firstReaching, passAt, positiveSeconds } from "./easing.js";
76
+ import { stepSequence, firstReaching, passAt, positiveSeconds } from "./easing.js";
76
77
  import { Measurement } from "./measure.js";
77
78
  import { isRenderable, render } from "./protocol.js";
78
79
  import { Segment } from "./segment.js";
79
- import { Style } from "./style.js";
80
+ import { intersection, NEVER_CHANGES, Style } from "./style.js";
80
81
  /**
81
82
  * What a frame drawn at `t` shows of `animation` under `motion`, and when
82
83
  * that changes: the animation's, or under reduced motion its reduced form's
@@ -88,25 +89,25 @@ import { Style } from "./style.js";
88
89
  export function shownAt(animation, t, motion) {
89
90
  const reduced = motion.reducedMotion === true;
90
91
  const drawn = reduced ? animation.reduced() : animation;
91
- return { value: drawn.at(t), nextChange: drawn.nextChange(t), drawn, reduced };
92
+ return { value: drawn.at(t), drawn, reduced };
92
93
  }
93
- /** The hold `shown`, drawn at `t`, is in: its animation's two answers there. */
94
- export function holdOf(shown, t) {
95
- return { lastChange: shown.drawn.lastChange(t), nextChange: shown.nextChange };
94
+ /** The hold `shown`, drawn at `t`, is in: its animation's two answers there, given what its drawing said. */
95
+ export function validityOf(shown, t, moves) {
96
+ return { validFrom: shown.drawn.validFrom(t, moves), validUntil: shown.drawn.validUntil(t, moves) };
96
97
  }
97
98
  /** The style saying a cell is in `hold`. */
98
- export function stamp(hold) {
99
- return new Style({ hold });
99
+ export function stampValidity(validity) {
100
+ return new Style({ validity });
100
101
  }
101
102
  /**
102
103
  * `segments`, every one carrying `hold` under whatever it already said: the
103
- * later `lastChange` and the sooner `nextChange` win (`Style.add`), so a
104
+ * later `validFrom` and the sooner `validUntil` win (`Style.add`), so a
104
105
  * frame's hold is the intersection of its cells', narrower than the truth
105
106
  * and never wider. [LAW:single-enforcer] The one way a time is put on drawn
106
107
  * cells, by whatever draws for a time it knows.
107
108
  */
108
- export function stampFrame(segments, hold) {
109
- return Segment.applyStyle(segments, stamp(hold));
109
+ export function stampValidityFrame(segments, validity) {
110
+ return Segment.applyStyle(segments, stampValidity(validity));
110
111
  }
111
112
  /**
112
113
  * The moment a frame drawn under `options` is drawn for: the frame's time,
@@ -127,22 +128,33 @@ function drawable(value) {
127
128
  }
128
129
  /**
129
130
  * An animation placed in the frame `options` draws: its value at the frame's
130
- * time (`frameTimeOf`, `shownAt`), drawn by `draw`, every segment stamped
131
- * with its hold there under whatever its own cells say, and the hold told to
132
- * the frame (`RenderOptions.onHold`), because a frame of no cells — an
133
- * element not yet entered — may have cells later, and must say when without
134
- * taking a row to say it in. [LAW:single-enforcer] The one placement: an
135
- * animation of renderables (`placedRender`) and a widget drawing one cell
136
- * from an animation of its own (a caret's blink) place through it, so how a
137
- * time reaches an animation and how its hold reaches the frame is written
138
- * once.
131
+ * time (`frameTimeOf`, `shownAt`), drawn whole by `draw` under the options it
132
+ * is handed, every segment then stamped with its hold there under whatever
133
+ * its own cells say, and the hold told to the frame (`RenderOptions.onValidity`),
134
+ * because a frame of no cells — an element not yet entered — may have cells
135
+ * later, and must say when without taking a row to say it in. Drawn first,
136
+ * because the hold is asked with what the drawing said (`Reads`); each report
137
+ * goes on outward too, since a value drawn inside a placement in this value
138
+ * may be a part of an animation placed further out. [LAW:single-enforcer]
139
+ * The one placement: an animation of renderables (`placedRender`), an effect
140
+ * and a widget drawing one cell from an animation of its own (a caret's
141
+ * blink) place through it, so how a time reaches an animation and how its
142
+ * hold reaches the frame is written once.
139
143
  */
140
144
  export function* placed(animation, options, draw) {
141
145
  const t = frameTimeOf(options);
142
146
  const shown = shownAt(animation, t, options);
143
- const hold = holdOf(shown, t);
144
- options.onHold?.(hold);
145
- yield* stampFrame(draw(shown.value), hold);
147
+ const said = new Map();
148
+ const part = (at, key) => `${at} ${key}`;
149
+ const onReads = (at, key, validity) => {
150
+ const known = said.get(part(at, key));
151
+ said.set(part(at, key), known === undefined ? validity : intersection(known, validity));
152
+ options.onReads?.(at, key, validity);
153
+ };
154
+ const segments = [...draw(shown.value, { ...options, onReads })];
155
+ const validity = validityOf(shown, t, (at, key) => said.get(part(at, key)));
156
+ options.onValidity?.(validity);
157
+ yield* stampValidityFrame(segments, validity);
146
158
  }
147
159
  /**
148
160
  * An animation's face, `render`: `placed`, its value drawn as the renderable
@@ -151,19 +163,26 @@ export function* placed(animation, options, draw) {
151
163
  * the hold goes back the way the time came in. [LAW:composability]
152
164
  */
153
165
  export function placedRender(animation, options) {
154
- return placed(animation, options, (value) => render(drawable(value), options));
166
+ return placed(animation, options, (value, drawing) => render(drawable(value), drawing));
155
167
  }
156
168
  /**
157
169
  * An animation's face, `measure`: what it shows at the frame's time, measured,
158
- * and its hold there told (`RenderOptions.onHold`), because a container lays
170
+ * and its hold there told (`RenderOptions.onValidity`), because a container lays
159
171
  * out from the measurement and must lay out again when it changes.
160
172
  */
161
173
  export function placedMeasure(animation, options) {
162
174
  const t = frameTimeOf(options);
163
175
  const shown = shownAt(animation, t, options);
164
- options.onHold?.(holdOf(shown, t));
176
+ options.onValidity?.(validityOf(shown, t, UNLAID));
165
177
  return Measurement.get(options, drawable(shown.value));
166
178
  }
179
+ /**
180
+ * What a measurement's drawing would say: held for good. An effect changes
181
+ * how cells look and never which exist, so nothing it does can change a
182
+ * layout; a combinator's own switches and a child's holds still reach the
183
+ * measurement. [LAW:types-are-the-program]
184
+ */
185
+ const UNLAID = () => NEVER_CHANGES;
167
186
  /**
168
187
  * Every member of `source`, its own and its class's, as own properties of a
169
188
  * new object: a method bound to `source`, a getter read off it live, a field
@@ -210,11 +229,11 @@ export const Animation = {
210
229
  * is only ever a true never.
211
230
  */
212
231
  done(animation, t) {
213
- return animation.nextChange(t) === Number.POSITIVE_INFINITY;
232
+ return animation.validUntil(t) === Number.POSITIVE_INFINITY;
214
233
  },
215
234
  /** `value` at every time, never changing. */
216
235
  constant(value) {
217
- const still = Animation.of({ at: () => value, nextChange: () => Number.POSITIVE_INFINITY, lastChange: () => Number.NEGATIVE_INFINITY, reduced: () => still });
236
+ const still = Animation.of({ at: () => value, validUntil: () => Number.POSITIVE_INFINITY, validFrom: () => Number.NEGATIVE_INFINITY, reduced: () => still });
218
237
  return still;
219
238
  },
220
239
  /**
@@ -224,8 +243,8 @@ export const Animation = {
224
243
  cut(before, after) {
225
244
  const cut = Animation.of({
226
245
  at: (t) => (t < 0 ? before : after),
227
- nextChange: (t) => (t < 0 ? 0 : Number.POSITIVE_INFINITY),
228
- lastChange: (t) => (t < 0 ? Number.NEGATIVE_INFINITY : 0),
246
+ validUntil: (t) => (t < 0 ? 0 : Number.POSITIVE_INFINITY),
247
+ validFrom: (t) => (t < 0 ? Number.NEGATIVE_INFINITY : 0),
229
248
  reduced: () => cut,
230
249
  });
231
250
  return cut;
@@ -237,7 +256,7 @@ export const Animation = {
237
256
  * it says its own end (`transition`, `../renderables/effects.ts`).
238
257
  */
239
258
  continuous(at) {
240
- return Animation.of({ at, nextChange: (t) => t, lastChange: (t) => t, reduced: () => Animation.constant(at(0)) });
259
+ return Animation.of({ at, validUntil: (t) => t, validFrom: (t) => t, reduced: () => Animation.constant(at(0)) });
241
260
  },
242
261
  /**
243
262
  * The time itself, in seconds, moving at every moment: what a clock, a
@@ -248,7 +267,7 @@ export const Animation = {
248
267
  * [LAW:types-are-the-program]
249
268
  */
250
269
  time() {
251
- const time = Animation.of({ at: (t) => t, nextChange: (t) => t, lastChange: (t) => t, reduced: () => time });
270
+ const time = Animation.of({ at: (t) => t, validUntil: (t) => t, validFrom: (t) => t, reduced: () => time });
252
271
  return time;
253
272
  },
254
273
  /**
@@ -258,8 +277,8 @@ export const Animation = {
258
277
  map(animation, f) {
259
278
  return Animation.of({
260
279
  at: (t) => f(animation.at(t)),
261
- nextChange: (t) => animation.nextChange(t),
262
- lastChange: (t) => animation.lastChange(t),
280
+ validUntil: (t, moves) => animation.validUntil(t, moves),
281
+ validFrom: (t, moves) => animation.validFrom(t, moves),
263
282
  reduced: () => Animation.map(animation.reduced(), f),
264
283
  });
265
284
  },
@@ -276,8 +295,8 @@ export const Animation = {
276
295
  at: (t) => animations.map((animation) => animation.at(t)),
277
296
  // A fold, not a spread: a stagger over a table's rows is a list as long
278
297
  // as the table, past what an engine takes as arguments.
279
- nextChange: (t) => animations.reduce((soonest, animation) => Math.min(soonest, animation.nextChange(t)), Number.POSITIVE_INFINITY),
280
- lastChange: (t) => animations.reduce((latest, animation) => Math.max(latest, animation.lastChange(t)), Number.NEGATIVE_INFINITY),
298
+ validUntil: (t, moves) => animations.reduce((soonest, animation) => Math.min(soonest, animation.validUntil(t, moves)), Number.POSITIVE_INFINITY),
299
+ validFrom: (t, moves) => animations.reduce((latest, animation) => Math.max(latest, animation.validFrom(t, moves)), Number.NEGATIVE_INFINITY),
281
300
  reduced: () => Animation.all(animations.map((animation) => animation.reduced())),
282
301
  });
283
302
  },
@@ -293,21 +312,26 @@ export const Animation = {
293
312
  * [LAW:one-source-of-truth] The one stepping rule, for an effect's frames
294
313
  * and a transition's schedule alike.
295
314
  */
296
- onBeat(animation, beat) {
315
+ sampleAndHold(animation, stepSequence) {
297
316
  return Animation.of({
298
- at: (t) => animation.at(beat.lastChange(t)),
299
- nextChange(t) {
300
- const moved = animation.nextChange(beat.lastChange(t));
317
+ at: (t) => animation.at(stepSequence.validFrom(t)),
318
+ validUntil(t, moves) {
319
+ const moved = animation.validUntil(stepSequence.validFrom(t), moves);
301
320
  if (moved === Number.POSITIVE_INFINITY)
302
321
  return moved;
303
- const end = beat.nextChange(t);
322
+ const end = stepSequence.validUntil(t);
304
323
  // The hold boundaries after `t` are `end`, then each hold's end in turn;
305
324
  // the value turns at the first one the animation has moved by.
306
- return moved <= end ? end : beat.lastChange(moved) === moved ? moved : beat.nextChange(moved);
325
+ return moved <= end ? end : stepSequence.validFrom(moved) === moved ? moved : stepSequence.validUntil(moved);
307
326
  },
308
327
  // The value held now began at this hold's start at the latest: an early
309
- // answer where the animation had not moved by it, which the contract allows.
310
- lastChange: (t) => beat.lastChange(t),
328
+ // answer where the animation had not moved by it, which the contract
329
+ // allows. One that always was never turned on any beat.
330
+ validFrom(t, moves) {
331
+ const began = stepSequence.validFrom(t);
332
+ const moved = animation.validFrom(began, moves);
333
+ return moved === Number.NEGATIVE_INFINITY ? moved : began;
334
+ },
311
335
  // Reduced, nothing moves between beats, so there is nothing to sample on them.
312
336
  reduced: () => animation.reduced(),
313
337
  });
@@ -331,19 +355,19 @@ export const Animation = {
331
355
  const carried = (local) => (Number.isFinite(local) ? firstReaching(local + seconds, (x) => x - seconds >= local) : local);
332
356
  return Animation.of({
333
357
  at: (t) => (t - seconds >= 0 ? then.at(t - seconds) : first.at(t)),
334
- nextChange(t) {
358
+ validUntil(t, moves) {
335
359
  const into = t - seconds;
336
360
  if (into < 0)
337
- return Math.min(first.nextChange(t), seconds);
338
- return Math.max(t, carried(then.nextChange(into)));
361
+ return Math.min(first.validUntil(t, moves), seconds);
362
+ return Math.max(t, carried(then.validUntil(into, moves)));
339
363
  },
340
364
  // After the switch the value became what it is at the switch or when
341
365
  // `then`'s did since, whichever is later; before it, when `first`'s did.
342
- lastChange(t) {
366
+ validFrom(t, moves) {
343
367
  const into = t - seconds;
344
368
  if (into < 0)
345
- return first.lastChange(t);
346
- return Math.min(t, Math.max(seconds, carried(then.lastChange(into))));
369
+ return first.validFrom(t, moves);
370
+ return Math.min(t, Math.max(seconds, carried(then.validFrom(into, moves))));
347
371
  },
348
372
  reduced: () => Animation.sequence(first.reduced(), seconds, then.reduced()),
349
373
  });
@@ -363,10 +387,10 @@ export const Animation = {
363
387
  repeat(period, nth) {
364
388
  positiveSeconds("Animation.repeat period", period);
365
389
  // [LAW:single-enforcer] Which turn a time is in, and how far into it, is
366
- // `passAt`, the wrap a `beat` and `Phase.loop` read time through, so a
390
+ // `passAt`, the wrap a `beat` and `WrapMode.loop` read time through, so a
367
391
  // repeat beside a loop of the same period turns on the same float; where
368
392
  // a turn begins and the next begins are a one-hold beat's two ends.
369
- const turns = beat([period]);
393
+ const turns = stepSequence([period]);
370
394
  // The turn's moment `local`, on its own clock, carried onto this one: the
371
395
  // first float the wrap places at or past `local` in turn `n`, or in a
372
396
  // later turn, found as `sequence` carries its second part's.
@@ -381,14 +405,14 @@ export const Animation = {
381
405
  const { pass, into } = passAt("Animation.repeat", t, period);
382
406
  return nth(pass).at(into);
383
407
  },
384
- nextChange(t) {
408
+ validUntil(t, moves) {
385
409
  const { pass, into } = passAt("Animation.repeat", t, period);
386
- return Math.max(t, Math.min(turns.nextChange(t), carried(pass, turns.lastChange(t), nth(pass).nextChange(into))));
410
+ return Math.max(t, Math.min(turns.validUntil(t), carried(pass, turns.validFrom(t), nth(pass).validUntil(into, moves))));
387
411
  },
388
- lastChange(t) {
412
+ validFrom(t, moves) {
389
413
  const { pass, into } = passAt("Animation.repeat", t, period);
390
- const began = turns.lastChange(t);
391
- return Math.min(t, Math.max(began, carried(pass, began, nth(pass).lastChange(into))));
414
+ const began = turns.validFrom(t);
415
+ return Math.min(t, Math.max(began, carried(pass, began, nth(pass).validFrom(into, moves))));
392
416
  },
393
417
  reduced: () => Animation.repeat(period, (n) => nth(n).reduced()),
394
418
  });
@@ -7,12 +7,12 @@
7
7
  * own `new Console()`, a CLI's output shown inside a Panel, a log replayed into
8
8
  * a Table cell.
9
9
  *
10
- * [LAW:types-are-the-program] Every colour decodes to the `ColorSpec` its
11
- * escape names — `31` the standard colour 1, `38;5;n` palette index n,
10
+ * [LAW:types-are-the-program] Every color decodes to the `ColorSpec` its
11
+ * escape names — `31` the standard color 1, `38;5;n` palette index n,
12
12
  * `38;2;r;g;b` that truecolor — and none is resolved to RGB here. A standard
13
- * colour is a slot in whatever theme draws it, so one captured run exports
13
+ * color is a slot in whatever theme draws it, so one captured run exports
14
14
  * correctly under a light theme and a dark one; resolved here, dark mode would
15
- * show light mode's colours. Resolution belongs to `export-lines`.
15
+ * show light mode's colors. Resolution belongs to `export-lines`.
16
16
  *
17
17
  * [LAW:types-are-the-program] The link is decoder state of its own, beside
18
18
  * the SGR style, not a field of it: a terminal ends a hyperlink only at an OSC
@@ -21,18 +21,25 @@
21
21
  *
22
22
  * Carriage returns are the other departure, and the same argument. Rich splits
23
23
  * lines with Python's `splitlines`, which also breaks at `\r`, so its `a\rb`
24
- * is two lines. Here a line ends only at `\n`, and `\r` returns to the first
25
- * column, so later text overwrites earlier text character by character:
26
- * `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
- * is still `done`. Columns are counted in characters, not cells, so a wide
28
- * character overwritten by a narrow one is where this and a terminal differ.
29
- * Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
24
+ * is two lines. Here a line ends at `\n` and the census's other line breaks, and
25
+ * `\r` returns to the first column, so later text overwrites earlier text cell by
26
+ * cell: `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
+ * is still `done`. Columns are a terminal's cells: a wide character takes two, a
28
+ * combining mark none, a tab moves to the next stop over what is there, and a wide
29
+ * character or a tab half overwritten leaves blanks. Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
30
30
  * is usually `\r\x1b[K` and the old text must not show through a shorter new
31
31
  * one.
32
32
  *
33
+ * Backspace is the third. Rich drops it; here it is nroff's overstrike, the
34
+ * adjacent `x BS x` bold and `_ BS x` underline that `man` writes, and a bare one
35
+ * moves back a column, so `12%\b\b\b13%` is the redraw it looks like. The bytes
36
+ * alone cannot tell `1\b1` the redraw from `1\b1` the bold, and `less` reads them
37
+ * as overstrike with no other evidence, so this does.
38
+ *
33
39
  * Everything else follows Rich: malformed SGR parameters are skipped rather
34
- * than refused, and every other escape is dropped. It is not a terminal
35
- * emulator — any other cursor movement is dropped, not performed.
40
+ * than refused, and every other escape is dropped, with every control a terminal
41
+ * draws nothing for. It is not a terminal emulator — any other cursor movement is
42
+ * dropped, not performed.
36
43
  *
37
44
  * Tier 5 of `src/core/`: it builds `RichText`, so it sits above `text`, and
38
45
  * `RichText` cannot offer a `fromAnsi` static without importing upward.
package/dist/core/ansi.js CHANGED
@@ -7,12 +7,12 @@
7
7
  * own `new Console()`, a CLI's output shown inside a Panel, a log replayed into
8
8
  * a Table cell.
9
9
  *
10
- * [LAW:types-are-the-program] Every colour decodes to the `ColorSpec` its
11
- * escape names — `31` the standard colour 1, `38;5;n` palette index n,
10
+ * [LAW:types-are-the-program] Every color decodes to the `ColorSpec` its
11
+ * escape names — `31` the standard color 1, `38;5;n` palette index n,
12
12
  * `38;2;r;g;b` that truecolor — and none is resolved to RGB here. A standard
13
- * colour is a slot in whatever theme draws it, so one captured run exports
13
+ * color is a slot in whatever theme draws it, so one captured run exports
14
14
  * correctly under a light theme and a dark one; resolved here, dark mode would
15
- * show light mode's colours. Resolution belongs to `export-lines`.
15
+ * show light mode's colors. Resolution belongs to `export-lines`.
16
16
  *
17
17
  * [LAW:types-are-the-program] The link is decoder state of its own, beside
18
18
  * the SGR style, not a field of it: a terminal ends a hyperlink only at an OSC
@@ -21,48 +21,78 @@
21
21
  *
22
22
  * Carriage returns are the other departure, and the same argument. Rich splits
23
23
  * lines with Python's `splitlines`, which also breaks at `\r`, so its `a\rb`
24
- * is two lines. Here a line ends only at `\n`, and `\r` returns to the first
25
- * column, so later text overwrites earlier text character by character:
26
- * `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
- * is still `done`. Columns are counted in characters, not cells, so a wide
28
- * character overwritten by a narrow one is where this and a terminal differ.
29
- * Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
24
+ * is two lines. Here a line ends at `\n` and the census's other line breaks, and
25
+ * `\r` returns to the first column, so later text overwrites earlier text cell by
26
+ * cell: `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
+ * is still `done`. Columns are a terminal's cells: a wide character takes two, a
28
+ * combining mark none, a tab moves to the next stop over what is there, and a wide
29
+ * character or a tab half overwritten leaves blanks. Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
30
30
  * is usually `\r\x1b[K` and the old text must not show through a shorter new
31
31
  * one.
32
32
  *
33
+ * Backspace is the third. Rich drops it; here it is nroff's overstrike, the
34
+ * adjacent `x BS x` bold and `_ BS x` underline that `man` writes, and a bare one
35
+ * moves back a column, so `12%\b\b\b13%` is the redraw it looks like. The bytes
36
+ * alone cannot tell `1\b1` the redraw from `1\b1` the bold, and `less` reads them
37
+ * as overstrike with no other evidence, so this does.
38
+ *
33
39
  * Everything else follows Rich: malformed SGR parameters are skipped rather
34
- * than refused, and every other escape is dropped. It is not a terminal
35
- * emulator — any other cursor movement is dropped, not performed.
40
+ * than refused, and every other escape is dropped, with every control a terminal
41
+ * draws nothing for. It is not a terminal emulator — any other cursor movement is
42
+ * dropped, not performed.
36
43
  *
37
44
  * Tier 5 of `src/core/`: it builds `RichText`, so it sits above `text`, and
38
45
  * `RichText` cannot offer a `fromAnsi` static without importing upward.
39
46
  */
47
+ import { cellLen, graphemes } from "./cells.js";
40
48
  import { ColorSpec } from "./color.js";
49
+ import { controlClass } from "./control-codes.js";
41
50
  import { osc8Sequences } from "./osc8.js";
42
51
  import { ATTRIBUTE_NAMES, ATTRIBUTE_SGR, NULL_STYLE, Style } from "./style.js";
43
52
  import { RichText } from "./text.js";
53
+ const BOLD = new Style({ bold: true });
54
+ const UNDERLINED = new Style({ underline: true });
55
+ const BOLD_UNDERLINED = BOLD.add(UNDERLINED);
56
+ const STRING_ESCAPE = String.raw `\x1b[\]P_^X][^\n]*?(?:\x07|\x1b\\|\x9c|\n|$)`;
44
57
  /**
45
58
  * Every escape but OSC 8, which `osc8Sequences` reads before this runs. In
46
- * order: a carriage return; an SGR sequence (group 1, its parameters — a
47
- * private marker such as the `>` of `\x1b[>4;2m` makes it some other CSI);
48
- * erase in line (group 2, its mode); any other CSI sequence; any other string
49
- * escape — OSC, DCS, APC, PM, SOS — run to the terminators OSC 8 accepts or
50
- * cut off by the end of the line; any other escape, ECMA-48's intermediates
51
- * and one final byte (`\x1b(B`, `\x1b)0`, `\x1b#8`, `\x1bc`). Only the first
52
- * three become tokens.
59
+ * order: a carriage return; an SGR sequence (`sgr`, its parameters — a private
60
+ * marker such as the `>` of `\x1b[>4;2m` makes it some other CSI); erase in line
61
+ * (`mode`); any other CSI sequence; any other string escape — OSC, DCS, APC, PM,
62
+ * SOS — run to the terminators OSC 8 accepts or cut off by the end of the line;
63
+ * any other escape, ECMA-48's intermediates and one final byte (`\x1b(B`,
64
+ * `\x1b)0`, `\x1b#8`, `\x1bc`); a character struck over itself (`struck`), over an
65
+ * underscore first or not (`underscore`); a character struck over an underscore
66
+ * alone (`underlined`); a backspace; and every control a line may not hold or a
67
+ * terminal draws nothing for. The silent ones are the two "any other" escapes and
68
+ * that last class.
53
69
  */
54
- const escapes = () => /\r|\x1b\[([0-9;:]*)m|\x1b\[([012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[\]P_^X][\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[ -/]*[0-~]/g;
70
+ const ESCAPES = [
71
+ String.raw `\r|\x1b\[(?<sgr>[0-9;:]*)m|\x1b\[(?<mode>[012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|${STRING_ESCAPE}|\x1b[ -/]*[0-~]`,
72
+ String.raw `(?<underscore>_\x08)?(?<struck>\P{Cc})(?:\x08\k<struck>)+|_\x08(?<underlined>\P{Cc})|\x08`,
73
+ controlClass((disposition) => disposition !== "keep" && disposition !== "zeroWidth"),
74
+ ].join("|");
75
+ const escapes = () => new RegExp(ESCAPES, "gu");
76
+ // A string escape goes first so a line break in its payload stays in the payload.
77
+ const lineBreaks = () => new RegExp(`${STRING_ESCAPE}|${controlClass((disposition) => disposition === "line-break")}`, "gu");
55
78
  function* escapeTokens(bytes) {
56
79
  let at = 0;
57
80
  for (const match of bytes.matchAll(escapes())) {
81
+ const { sgr, mode, underscore, struck, underlined } = match.groups;
58
82
  if (match.index > at)
59
83
  yield { kind: "text", text: bytes.slice(at, match.index) };
60
84
  if (match[0] === "\r")
61
85
  yield { kind: "return" };
62
- if (match[1] !== undefined)
63
- yield { kind: "sgr", params: match[1] };
64
- if (match[2] !== undefined)
65
- yield { kind: "erase", mode: (match[2] || "0") };
86
+ if (sgr !== undefined)
87
+ yield { kind: "sgr", params: sgr };
88
+ if (mode !== undefined)
89
+ yield { kind: "erase", mode: (mode || "0") };
90
+ if (struck !== undefined)
91
+ yield { kind: "overstrike", character: struck, emphasis: underscore === undefined ? BOLD : BOLD_UNDERLINED };
92
+ if (underlined !== undefined)
93
+ yield { kind: "overstrike", character: underlined, emphasis: UNDERLINED };
94
+ if (match[0] === "\b")
95
+ yield { kind: "backspace" };
66
96
  at = match.index + match[0].length;
67
97
  }
68
98
  if (at < bytes.length)
@@ -90,7 +120,7 @@ function next(rest) {
90
120
  }
91
121
  /**
92
122
  * `5;n` or `2;r;g;b`, the arguments of `38` and `48`. A sequence cut short
93
- * names no colour, and the codes it did consume are spent, as in Rich.
123
+ * names no color, and the codes it did consume are spent, as in Rich.
94
124
  */
95
125
  function extendedColor(rest) {
96
126
  const kind = next(rest);
@@ -115,8 +145,8 @@ const SGR_OPS = new Map([
115
145
  [23, adding({ italic: false })],
116
146
  // ECMA-48: 24 is "neither singly nor doubly underlined", 25 "steady" at
117
147
  // either speed. Rich's table clears only the first of each pair.
118
- [24, adding({ underline: false, underline2: false })],
119
- [25, adding({ blink: false, blink2: false })],
148
+ [24, adding({ underline: false, doubleUnderline: false })],
149
+ [25, adding({ blink: false, rapidBlink: false })],
120
150
  [27, adding({ reverse: false })],
121
151
  [28, adding({ conceal: false })],
122
152
  [29, adding({ strike: false })],
@@ -124,7 +154,7 @@ const SGR_OPS = new Map([
124
154
  [39, adding({ color: ColorSpec.default() })],
125
155
  [48, (style, rest) => style.add(new Style({ bgcolor: extendedColor(rest) }))],
126
156
  [49, adding({ bgcolor: ColorSpec.default() })],
127
- // Underline colour has no field on Style; its arguments are still spent, or
157
+ // Underline color has no field on Style; its arguments are still spent, or
128
158
  // `58;2;1;2;3` would read on as bold, dim and italic.
129
159
  [58, (style, rest) => (extendedColor(rest), style)],
130
160
  [54, adding({ frame: false, encircle: false })],
@@ -153,11 +183,52 @@ function applySgr(style, params) {
153
183
  result = (SGR_OPS.get(code) ?? identity)(result, codes);
154
184
  return result;
155
185
  }
186
+ const ORIGIN = ["", NULL_STYLE];
156
187
  const BLANK = [" ", NULL_STYLE];
188
+ const TAB_STOP = 8;
189
+ /** The column holding the cluster drawn across `column`. */
190
+ function leadOf(cells, column) {
191
+ let at = column;
192
+ while (at > 0 && cells[at][0] === "")
193
+ at -= 1;
194
+ return at;
195
+ }
196
+ /** A cluster across the edge before `column` goes blank, as on a terminal. */
197
+ function splitWide(cells, column) {
198
+ if (cells[column]?.[0] !== "")
199
+ return;
200
+ const lead = leadOf(cells, column);
201
+ for (let at = lead; at === lead || cells[at]?.[0] === ""; at += 1)
202
+ cells[at] = BLANK;
203
+ }
204
+ /** `cluster` drawn at `column`, and the columns the cursor moves. */
205
+ function draw(cells, column, cluster, style) {
206
+ const width = cluster === "\t" ? TAB_STOP - ((column - 1) % TAB_STOP) : cellLen(cluster);
207
+ if (width === 0) {
208
+ const lead = leadOf(cells, column - 1);
209
+ cells[lead] = [cells[lead][0] + cluster, cells[lead][1]];
210
+ return 0;
211
+ }
212
+ if (cluster === "\t" && column < cells.length) {
213
+ while (cells.length < column + width)
214
+ cells.push(BLANK);
215
+ return width;
216
+ }
217
+ splitWide(cells, column);
218
+ splitWide(cells, column + width);
219
+ cells[column] = [cells[0][0] + cluster, style];
220
+ cells[0] = ORIGIN;
221
+ for (let at = 1; at < width; at += 1)
222
+ cells[column + at] = ["", style];
223
+ return width;
224
+ }
157
225
  /** Erase in line: the cells before the cursor go blank, the ones from it on go. */
158
226
  function erase(cells, column, mode) {
227
+ splitWide(cells, column);
228
+ splitWide(cells, column + 1);
229
+ cells[0] = ORIGIN;
159
230
  if (mode !== "0")
160
- cells.fill(BLANK, 0, Math.min(column + 1, cells.length));
231
+ cells.fill(BLANK, 1, Math.min(column + 1, cells.length));
161
232
  if (mode !== "1")
162
233
  cells.length = Math.min(column, cells.length);
163
234
  }
@@ -173,25 +244,32 @@ export class AnsiDecoder {
173
244
  link = undefined;
174
245
  /** One `RichText` per line of `ansi`; a final newline ends a line rather than starting one. */
175
246
  decode(ansi) {
176
- return (ansi.match(/[^\n]*\n|[^\n]+/g) ?? []).map((line) => this.decodeLine(line.replace(/\r?\n$/, "")));
247
+ const lined = ansi.replace(lineBreaks(), (found) => (found.startsWith("\x1b") ? found : "\n"));
248
+ return (lined.match(/[^\n]*\n|[^\n]+/g) ?? []).map((line) => this.decodeLine(line.replace(/\r?\n$/, "")));
177
249
  }
178
250
  /**
179
251
  * One line of output, holding no `\n`, written the way a terminal writes
180
252
  * it: `\r` returns to the first column, and text overwrites from there.
181
253
  */
182
254
  decodeLine(line) {
183
- const cells = [];
184
- let column = 0;
255
+ const cells = [ORIGIN];
256
+ let column = 1;
185
257
  for (const token of tokens(line)) {
186
258
  switch (token.kind) {
187
259
  case "text": {
188
260
  const style = this.sgr.withLink(this.link);
189
- for (const char of token.text)
190
- cells[column++] = [char, style];
261
+ for (const cluster of graphemes(token.text))
262
+ column += draw(cells, column, cluster, style);
191
263
  break;
192
264
  }
265
+ case "overstrike":
266
+ column += draw(cells, column, token.character, this.sgr.add(token.emphasis).withLink(this.link));
267
+ break;
193
268
  case "return":
194
- column = 0;
269
+ column = 1;
270
+ break;
271
+ case "backspace":
272
+ column = Math.max(1, column - 1);
195
273
  break;
196
274
  case "erase":
197
275
  erase(cells, column, token.mode);
@@ -205,7 +283,7 @@ export class AnsiDecoder {
205
283
  }
206
284
  }
207
285
  const runs = [];
208
- for (const [char, style] of cells) {
286
+ for (const [char, style] of cells.filter(([char]) => char !== "")) {
209
287
  const last = runs.at(-1);
210
288
  if (last?.[1].equals(style))
211
289
  last[0] += char;