@promptctl/rich-js 0.27.0 → 0.28.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 (110) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +36 -17
  4. package/dist/core/animation.js +61 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  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 +93 -5
  17. package/dist/core/color.d.ts +6 -7
  18. package/dist/core/color.js +11 -13
  19. package/dist/core/console.d.ts +18 -1
  20. package/dist/core/console.js +24 -5
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/env.d.ts +6 -0
  24. package/dist/core/env.js +9 -1
  25. package/dist/core/export-html.js +4 -3
  26. package/dist/core/export-lines.d.ts +2 -0
  27. package/dist/core/export-lines.js +7 -3
  28. package/dist/core/export-svg.js +6 -5
  29. package/dist/core/json.js +9 -2
  30. package/dist/core/markup.js +4 -3
  31. package/dist/core/orient.d.ts +68 -0
  32. package/dist/core/orient.js +224 -0
  33. package/dist/core/osc8.d.ts +12 -5
  34. package/dist/core/osc8.js +14 -7
  35. package/dist/core/pretty.js +22 -18
  36. package/dist/core/protocol.d.ts +27 -6
  37. package/dist/core/protocol.js +14 -6
  38. package/dist/core/render.d.ts +8 -3
  39. package/dist/core/render.js +18 -5
  40. package/dist/core/seal.d.ts +26 -0
  41. package/dist/core/seal.js +75 -0
  42. package/dist/core/segment.d.ts +9 -1
  43. package/dist/core/segment.js +11 -1
  44. package/dist/core/style.d.ts +29 -1
  45. package/dist/core/style.js +41 -5
  46. package/dist/core/text-order.d.ts +51 -0
  47. package/dist/core/text-order.js +50 -0
  48. package/dist/core/text.d.ts +10 -0
  49. package/dist/core/text.js +133 -87
  50. package/dist/core/timeline.js +6 -6
  51. package/dist/core/view.d.ts +1 -1
  52. package/dist/core/view.js +5 -4
  53. package/dist/core/wrap.d.ts +24 -6
  54. package/dist/core/wrap.js +82 -16
  55. package/dist/host/app.d.ts +44 -3
  56. package/dist/host/app.js +111 -6
  57. package/dist/host/index.d.ts +3 -2
  58. package/dist/host/index.js +1 -0
  59. package/dist/host/input.d.ts +35 -2
  60. package/dist/host/input.js +88 -4
  61. package/dist/host/terminal-colors.d.ts +18 -0
  62. package/dist/host/terminal-colors.js +46 -0
  63. package/dist/host/terminal-host.d.ts +2 -1
  64. package/dist/index.d.ts +5 -4
  65. package/dist/index.js +2 -2
  66. package/dist/renderables/change.d.ts +1 -3
  67. package/dist/renderables/change.js +4 -6
  68. package/dist/renderables/effect.d.ts +60 -59
  69. package/dist/renderables/effect.js +91 -81
  70. package/dist/renderables/effects.d.ts +37 -8
  71. package/dist/renderables/effects.js +185 -52
  72. package/dist/renderables/frames.d.ts +1 -10
  73. package/dist/renderables/frames.js +1 -16
  74. package/dist/renderables/glyph-effect.d.ts +12 -13
  75. package/dist/renderables/glyph-effect.js +34 -32
  76. package/dist/renderables/glyph-effects.d.ts +27 -8
  77. package/dist/renderables/glyph-effects.js +75 -7
  78. package/dist/renderables/lifecycle.d.ts +5 -7
  79. package/dist/renderables/lifecycle.js +6 -7
  80. package/dist/renderables/markdown.js +4 -2
  81. package/dist/renderables/panel.js +6 -4
  82. package/dist/renderables/progressBar.d.ts +3 -3
  83. package/dist/renderables/progressBar.js +16 -13
  84. package/dist/renderables/syntax.js +7 -2
  85. package/dist/renderables/table.js +2 -1
  86. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  87. package/dist/template-bindings/animation-funcs.js +115 -32
  88. package/dist/template-bindings/index.d.ts +7 -5
  89. package/dist/template-bindings/index.js +8 -5
  90. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  91. package/dist/template-bindings/palette-funcs.js +1 -1
  92. package/dist/themes/buildPalette.d.ts +3 -2
  93. package/dist/themes/buildPalette.js +2 -2
  94. package/dist/themes/data/index.js +1 -4
  95. package/dist/themes/data/textual-ansi.js +0 -143
  96. package/dist/themes/ramp.d.ts +1 -1
  97. package/dist/themes/ramp.js +1 -1
  98. package/dist/themes/registry.d.ts +6 -8
  99. package/dist/themes/registry.js +2 -4
  100. package/dist/themes/terminalThemes.d.ts +4 -11
  101. package/dist/themes/terminalThemes.js +31 -47
  102. package/dist/widgets/button.d.ts +1 -1
  103. package/dist/widgets/button.js +6 -6
  104. package/dist/widgets/checkbox.js +4 -4
  105. package/dist/widgets/dropdown.d.ts +1 -1
  106. package/dist/widgets/dropdown.js +7 -5
  107. package/dist/widgets/themed-widget.js +3 -3
  108. package/dist/widgets/toggle.d.ts +1 -1
  109. package/dist/widgets/toggle.js +5 -5
  110. package/package.json +5 -4
@@ -13,8 +13,9 @@
13
13
  * "are we mid-sequence" mode flag — the buffer head carries that state, so on
14
14
  * a host whose chunks can split a key, a UTF-8 character or an escape
15
15
  * sequence split across two chunks is completed by the second. What a chunk
16
- * end means is the host's `InputChunks`, the one input to the parser that is
17
- * not a byte, and it decides only what becomes of an unfinished ESC.
16
+ * end means is the host's `InputChunks`, and whether an ask awaits a reply is
17
+ * what its owner last told `awaitReplies`: the two inputs to the parser that
18
+ * are not bytes, and both decide only what becomes of an unfinished ESC.
18
19
  *
19
20
  * An unfinished escape sequence is ambiguous: `ESC` alone is the Escape key
20
21
  * or the start of a sequence, and `ESC [` is Alt+[ or the start of a CSI.
@@ -29,6 +30,15 @@
29
30
  * lone ESC is also settled early by a next chunk that opens with anything but
30
31
  * `[` or `O`, because a busy worker can queue the next key behind it.
31
32
  * `flush()` settles the hold synchronously.
33
+ *
34
+ * `ESC ]`, `ESC P` and `ESC _` open the control strings a terminal replies
35
+ * in, and are also Alt+], Alt+P and Alt+_. A reply can arrive split only on
36
+ * a split host while an ask awaits one (`replyMayArrive`), and only then is an
37
+ * unterminated one, or a lone ESC that may open one, held — past the escape
38
+ * delay, until `awaitReplies(false)` arms the delay afresh. Otherwise it is Alt
39
+ * on its key at once. A terminated one is a reply either way, and one cut off
40
+ * by an ESC that is not ST was never a reply, and is Alt on its key, as with
41
+ * none awaited.
32
42
  */
33
43
  /**
34
44
  * The one name a key goes by — what a keymap is keyed by and what
@@ -136,6 +146,12 @@ const LANGLE = 0x3c; // <
136
146
  const O_BYTE = 0x4f; // O
137
147
  const M_UPPER = 0x4d; // M
138
148
  const M_LOWER = 0x6d; // m
149
+ const BEL = 0x07;
150
+ const BACKSLASH = 0x5c;
151
+ // The control strings a terminal answers in, by the byte after ESC: OSC, DCS, APC.
152
+ const STRING_REPLIES = { 0x5d: "osc", 0x50: "dcs", 0x5f: "apc" };
153
+ // A CSI whose parameters open with one of these is a report, never a key.
154
+ const PRIVATE_PREFIXES = new Set(["?", ">"]);
139
155
  // [LAW:single-enforcer] Shared encode/decode singletons. The parser
140
156
  // receives `Uint8Array | string` from the host and walks bytes through
141
157
  // its state machine; conversion happens at the two boundaries (string
@@ -198,6 +214,7 @@ export class InputParser {
198
214
  // [LAW:types-are-the-program] `setTimeout`/`clearTimeout` are universal
199
215
  // across node and browser; `setImmediate` is node-only.
200
216
  escTimer;
217
+ awaiting = false;
201
218
  constructor(sink, chunks) {
202
219
  this.sink = sink;
203
220
  this.chunks = chunks;
@@ -213,7 +230,9 @@ export class InputParser {
213
230
  // Any other next chunk means Escape was its own key, even when the hold
214
231
  // timer has not run yet: a busy worker can queue the next key ahead of it,
215
232
  // and joining them would read Escape then G as Alt+G (rich-host-input-0c4f).
216
- const escaped = this.holdingEscape() && next.length > 0 && next[0] !== LBRACKET && next[0] !== O_BYTE;
233
+ // While a reply is awaited, a next chunk opening one of its control strings
234
+ // continues it too: a reply split right after its ESC.
235
+ const escaped = this.holdingEscape() && next.length > 0 && !this.continuesEscape(next[0]);
217
236
  this.cancelEscape();
218
237
  // The buffer is settled before the sink hears Escape, so a sink that
219
238
  // resets the parser on it (an app stopping) leaves nothing to drain.
@@ -242,14 +261,41 @@ export class InputParser {
242
261
  // timer before it reads input queued behind a long frame, so the delay's end
243
262
  // settles the hold only after one more turn of the event loop has read what
244
263
  // was waiting. The parser holds one timer, so arming it replaces any other.
264
+ // While a reply may arrive the delay settles no hold that may open one: a
265
+ // reply split over a slow link may take longer than it. The wait's end arms
266
+ // it afresh (`awaitReplies`), so what is held then gets the whole delay.
245
267
  hold() {
246
268
  this.cancelEscape();
247
269
  if (this.buffer[0] !== ESC)
248
270
  return;
249
271
  this.escTimer = setTimeout(() => {
250
- this.escTimer = setTimeout(() => this.flush(), 0);
272
+ this.escTimer = setTimeout(() => {
273
+ if (!this.replyMayArrive() || !opensString(this.buffer))
274
+ this.flush();
275
+ }, 0);
251
276
  }, ESCAPE_DELAY_MS);
252
277
  }
278
+ /**
279
+ * Whether an ask is awaiting the terminal's replies, told by whoever asks
280
+ * at the ask's start and end. Only while one is does an unfinished
281
+ * `ESC ]`, `ESC P` or `ESC _` open a reply still arriving; otherwise it is
282
+ * Alt on that key. The wait's end restarts the escape delay for what is
283
+ * held, on a split host, the only one that holds.
284
+ */
285
+ awaitReplies(awaiting) {
286
+ this.awaiting = awaiting;
287
+ if (!awaiting && this.chunks === "split")
288
+ this.hold();
289
+ }
290
+ // [LAW:single-enforcer] The one reading of whether an unfinished control
291
+ // string may be a reply still arriving: only a split host splits one, and
292
+ // only an ask awaits one.
293
+ replyMayArrive() {
294
+ return this.chunks === "split" && this.awaiting;
295
+ }
296
+ continuesEscape(byte) {
297
+ return byte === LBRACKET || byte === O_BYTE || (STRING_REPLIES[byte] !== undefined && this.replyMayArrive());
298
+ }
253
299
  // A whole-key chunk has ended, so an ESC still at the buffer head opens no
254
300
  // sequence, and is read as `reissue` reads it. Each key is taken off the
255
301
  // buffer before the sink hears it, so no byte of one chunk outlives it.
@@ -311,6 +357,8 @@ export class InputParser {
311
357
  this.sink.key(result.event);
312
358
  else if (result.kind === "mouse")
313
359
  this.sink.mouse(result.event);
360
+ else if (result.kind === "reply")
361
+ this.sink.reply(result.event);
314
362
  // skip → drop bytes silently
315
363
  }
316
364
  }
@@ -377,6 +425,15 @@ export class InputParser {
377
425
  // ESC [ ... → CSI
378
426
  if (b1 === LBRACKET)
379
427
  return this.consumeCSI(buf);
428
+ const string = STRING_REPLIES[b1];
429
+ if (string !== undefined) {
430
+ const result = consumeString(buf, string);
431
+ // Unterminated where no reply may arrive, it was never a reply: Alt on
432
+ // its key, and what followed is read on its own.
433
+ if (result.kind === "incomplete" && !this.replyMayArrive())
434
+ return { kind: "key", bytes: 2, event: altKey(b1) };
435
+ return result;
436
+ }
380
437
  // ESC O X → SS3 (F1–F4 / arrows on some terms)
381
438
  if (b1 === O_BYTE)
382
439
  return this.consumeSS3(buf);
@@ -555,7 +612,34 @@ function decodeMouseFromCb(cb, x, y, isPress) {
555
612
  ctrl,
556
613
  };
557
614
  }
615
+ // A lone ESC, or one opening a control string: the holds a reply may be.
616
+ function opensString(buf) {
617
+ return buf.length === 1 || STRING_REPLIES[buf[1]] !== undefined;
618
+ }
619
+ // A control string runs to its terminator, ST (`ESC \\`) or BEL, and holds
620
+ // any byte but ESC before it. Terminated, it is a reply whether or not one was
621
+ // awaited. Unterminated, it is unfinished, and `consumeEscape` decides by
622
+ // whether a reply may arrive if that means held or Alt on its key. An ESC that
623
+ // is not ST ends it before it closed: it was never a reply, so it is Alt on
624
+ // its key and what followed is read on its own, as with none awaited.
625
+ function consumeString(buf, kind) {
626
+ for (let i = 2; i < buf.length; i++) {
627
+ const b = buf[i];
628
+ if (b === BEL)
629
+ return { kind: "reply", bytes: i + 1, event: { kind, body: UTF8_DECODER.decode(buf.subarray(2, i)) } };
630
+ if (b !== ESC)
631
+ continue;
632
+ if (i + 1 === buf.length)
633
+ return { kind: "incomplete" };
634
+ if (buf[i + 1] !== BACKSLASH)
635
+ return { kind: "key", bytes: 2, event: altKey(buf[1]) };
636
+ return { kind: "reply", bytes: i + 2, event: { kind, body: UTF8_DECODER.decode(buf.subarray(2, i)) } };
637
+ }
638
+ return { kind: "incomplete" };
639
+ }
558
640
  function decodeCSI(params, final, bytes) {
641
+ if (PRIVATE_PREFIXES.has(params[0] ?? ""))
642
+ return { kind: "reply", bytes, event: { kind: "csi", body: params + final } };
559
643
  // Handle modifier suffixes like `1;2A` (shift+up). We split params on `;`.
560
644
  const parts = params.split(";");
561
645
  const first = parts[0] ?? "";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The terminal's own foreground and background, asked of it with OSC 10 and
3
+ * OSC 11 — the colours every contrast decision is measured against, which the
4
+ * library otherwise has to assume.
5
+ *
6
+ * [LAW:effects-at-boundaries] The ask is `App`'s, the one reader of its
7
+ * terminal's input; what is here is the query bytes and the parse of their
8
+ * replies, which stays pure.
9
+ */
10
+ import { TerminalColors } from "../core/color.js";
11
+ import type { App } from "./app.js";
12
+ /**
13
+ * Ask the terminal `app` runs on for its foreground and background, and return
14
+ * the terminal `app` draws on with them in place — its sixteen ANSI colours
15
+ * kept, since nothing asks for those. `undefined` when the terminal did not
16
+ * answer both, so the caller keeps what it had. Set it as `app.terminal`.
17
+ */
18
+ export declare function terminalColors(app: App): Promise<TerminalColors | undefined>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The terminal's own foreground and background, asked of it with OSC 10 and
3
+ * OSC 11 — the colours every contrast decision is measured against, which the
4
+ * library otherwise has to assume.
5
+ *
6
+ * [LAW:effects-at-boundaries] The ask is `App`'s, the one reader of its
7
+ * terminal's input; what is here is the query bytes and the parse of their
8
+ * replies, which stays pure.
9
+ */
10
+ import { ColorRgba, TerminalColors } from "../core/color.js";
11
+ import { pattern } from "../core/pattern.js";
12
+ // OSC 10 and OSC 11 with `?` ask for the dynamic foreground and background.
13
+ const QUERIES = ["\x1b]10;?\x1b\\", "\x1b]11;?\x1b\\"];
14
+ // XParseColor's `rgb:` form, 1–4 hex digits a channel, as xterm and every
15
+ // terminal that copies it answers, or `rgba:`, as urxvt answers with
16
+ // transparency on; the alpha says nothing about the colour a cell is drawn in.
17
+ const RGB = pattern(/^rgba?:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})(?:\/[0-9a-f]{1,4})?$/i);
18
+ // A channel of n hex digits is a fraction of 16ⁿ − 1, scaled to 8 bits.
19
+ const channel = (hex) => Math.round((parseInt(hex, 16) / (16 ** hex.length - 1)) * 255);
20
+ /**
21
+ * The colour an OSC 10 or OSC 11 reply reports, or `undefined` when `reply`
22
+ * is not that answer to `slot`.
23
+ */
24
+ function parseColorReply(reply, slot) {
25
+ if (reply.kind !== "osc")
26
+ return undefined;
27
+ const prefix = `${slot};`;
28
+ if (!reply.body.startsWith(prefix))
29
+ return undefined;
30
+ const match = RGB.exec(reply.body.slice(prefix.length));
31
+ return match === null ? undefined : new ColorRgba(channel(match[1]), channel(match[2]), channel(match[3]));
32
+ }
33
+ /**
34
+ * Ask the terminal `app` runs on for its foreground and background, and return
35
+ * the terminal `app` draws on with them in place — its sixteen ANSI colours
36
+ * kept, since nothing asks for those. `undefined` when the terminal did not
37
+ * answer both, so the caller keeps what it had. Set it as `app.terminal`.
38
+ */
39
+ export async function terminalColors(app) {
40
+ const { replies } = await app.ask(QUERIES);
41
+ const foreground = replies.map((reply) => parseColorReply(reply, 10)).find((color) => color !== undefined);
42
+ const background = replies.map((reply) => parseColorReply(reply, 11)).find((color) => color !== undefined);
43
+ if (foreground === undefined || background === undefined)
44
+ return undefined;
45
+ return new TerminalColors(background, foreground, app.terminal.ansiColors);
46
+ }
@@ -69,7 +69,8 @@ export interface TerminalHost {
69
69
  onData(handler: DataHandler): Unsubscribe;
70
70
  /**
71
71
  * Where `onData`'s chunks may end. The parser holds an unfinished escape
72
- * sequence open for its rest, for the escape delay, only on a host whose
72
+ * sequence open for its rest — for the escape delay, or a control string
73
+ * for as long as an ask awaits its reply — only on a host whose
73
74
  * chunks can split one; a whole-keys host settles each chunk as it arrives,
74
75
  * because a busy worker there reads a queued next key before any timer
75
76
  * runs (rich-host-input-ab9n).
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { cellLen, setCellSize, splitText, chopCells, cellFit, graphemes, asCellC
2
2
  export type { CellCol, CodeUnit, CodePoint } from "./core/cells.js";
3
3
  export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, type TerminalColors, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
4
  export type { ColorSystemName, DetectColorOptions, Destination, DestinationOptions } from "./core/color.js";
5
+ export type { TextOrder } from "./core/text-order.js";
5
6
  export { Oklch, IDENTITY, INVERT_LIGHTNESS, isIdentityKey, } from "./core/oklch.js";
6
7
  export type { ThemeKey, OklchWeights } from "./core/oklch.js";
7
8
  export { Palette } from "./themes/palette.js";
@@ -19,7 +20,7 @@ export type { ColorStop, RampMotion } from "./themes/ramp.js";
19
20
  export { EASES, Phase, beat, cubicBezier, parseEase, spring, steps } from "./core/easing.js";
20
21
  export type { Beat, Ease, SpringOptions, StepPosition, Timing } from "./core/easing.js";
21
22
  export { Animation } from "./core/animation.js";
22
- export type { Timed } from "./core/animation.js";
23
+ export type { Moves, Timed } from "./core/animation.js";
23
24
  export { tween } from "./core/tween.js";
24
25
  export { Timeline } from "./core/timeline.js";
25
26
  export type { View } from "./core/view.js";
@@ -63,7 +64,7 @@ export type { Env } from "./core/env.js";
63
64
  export { Constrain } from "./renderables/constrain.js";
64
65
  export { Effected, confined, drawnOf } from "./renderables/effect.js";
65
66
  export type { CellAttributes, CellColors, CellLook, Drawn, Effect, EffectCell, EffectedOptions, EffectFrame, ListedCell, Pair, Placed } from "./renderables/effect.js";
66
- export { ATTRIBUTE_DEFAULTS, EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, blink, dissolveOut, fadeIn, gradient, onBeat, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
67
+ export { ATTRIBUTE_DEFAULTS, EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, blink, dissolveOut, fadeIn, gradient, onBeat, layered, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
67
68
  export type { AttributeLoopName, Blink, ColorStep, Curve, EffectTarget, Field, Loop, LoopName, Touch, Transition, TransitionName } from "./renderables/effects.js";
68
69
  export { crossfade, wipe } from "./renderables/change.js";
69
70
  export type { Between, Change } from "./renderables/change.js";
@@ -73,8 +74,8 @@ export { scroll } from "./renderables/scroll.js";
73
74
  export type { ScrollOptions } from "./renderables/scroll.js";
74
75
  export { GlyphEffected } from "./renderables/glyph-effect.js";
75
76
  export type { DrawnGlyphs, GlyphCell, GlyphEffect, GlyphEffectedOptions, GlyphFrame } from "./renderables/glyph-effect.js";
76
- export { GLYPH_DEFAULTS, GLYPH_EFFECT_CATALOGUE, backspace, caseWave, particles, scramble, scrambleOut, typewriter } from "./renderables/glyph-effects.js";
77
- export type { CaseWave, GlyphLoopName, GlyphTransition, GlyphTransitionName, Particles } from "./renderables/glyph-effects.js";
77
+ export { GLYPH_DEFAULTS, GLYPH_EFFECT_CATALOGUE, backspace, caseWave, marchingAnts, particles, scramble, scrambleOut, typewriter } from "./renderables/glyph-effects.js";
78
+ export type { CaseWave, GlyphLoopName, GlyphTransition, GlyphTransitionName, MarchingAnts, Particles } from "./renderables/glyph-effects.js";
78
79
  export { Align } from "./renderables/align.js";
79
80
  export type { Alignment } from "./core/place.js";
80
81
  export { Padding } from "./renderables/padding.js";
package/dist/index.js CHANGED
@@ -79,12 +79,12 @@ export { Console } from "./core/console.js";
79
79
  // Renderables
80
80
  export { Constrain } from "./renderables/constrain.js";
81
81
  export { Effected, confined, drawnOf } from "./renderables/effect.js";
82
- export { ATTRIBUTE_DEFAULTS, EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, blink, dissolveOut, fadeIn, gradient, onBeat, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
82
+ export { ATTRIBUTE_DEFAULTS, EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, blink, dissolveOut, fadeIn, gradient, onBeat, layered, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
83
83
  export { crossfade, wipe } from "./renderables/change.js";
84
84
  export { lifecycle, throughColors, throughGlyphs } from "./renderables/lifecycle.js";
85
85
  export { scroll } from "./renderables/scroll.js";
86
86
  export { GlyphEffected } from "./renderables/glyph-effect.js";
87
- export { GLYPH_DEFAULTS, GLYPH_EFFECT_CATALOGUE, backspace, caseWave, particles, scramble, scrambleOut, typewriter } from "./renderables/glyph-effects.js";
87
+ export { GLYPH_DEFAULTS, GLYPH_EFFECT_CATALOGUE, backspace, caseWave, marchingAnts, particles, scramble, scrambleOut, typewriter } from "./renderables/glyph-effects.js";
88
88
  export { Align } from "./renderables/align.js";
89
89
  export { Padding } from "./renderables/padding.js";
90
90
  export { Viewport, ViewportPerKey, SCROLLBAR } from "./renderables/viewport.js";
@@ -21,14 +21,12 @@
21
21
  * frame to the next, so the same `t` draws the same bytes at any frame rate.
22
22
  */
23
23
  import { Animation } from "../core/animation.js";
24
- import type { TerminalTheme } from "../core/color.js";
25
24
  import type { Renderable } from "../core/protocol.js";
26
25
  import { type Curve, type Transition } from "./effects.js";
27
- /** What a change is between: the renderable drawn until now, the one drawn from now on, and the terminal whose ground the change passes through. */
26
+ /** What a change is between: the renderable drawn until now and the one drawn from now on. */
28
27
  export interface Between {
29
28
  readonly from: Renderable;
30
29
  readonly to: Renderable;
31
- readonly theme: TerminalTheme;
32
30
  }
33
31
  /** A change from one renderable to another: what it draws at each moment, from `t = 0` on its own clock. */
34
32
  export type Change = (between: Between) => Animation<Renderable>;
@@ -32,7 +32,7 @@ const shows = (p) => Math.abs(2 * p - 1);
32
32
  /** The change over `curve`, each cell as far through it as `progress` says. */
33
33
  function change(curve, progress) {
34
34
  requireCurve(curve);
35
- return transition(curve.seconds, ({ from, to, theme }) => Animation.sequence(Animation.continuous((t) => new ChangeFrame(from, to, theme, progress, t, curve.swing)), curve.seconds, Animation.constant(to)));
35
+ return transition(curve.seconds, ({ from, to }) => Animation.sequence(Animation.continuous((t) => new ChangeFrame(from, to, progress, t, curve.swing)), curve.seconds, Animation.constant(to)));
36
36
  }
37
37
  /** A crossfade: the old renderable fades to the terminal's ground and the new one rises from it, every cell together, over `curve`. */
38
38
  export function crossfade(curve) {
@@ -61,14 +61,12 @@ export function wipe(curve, edge = WIPE_EDGE) {
61
61
  class ChangeFrame {
62
62
  from;
63
63
  to;
64
- theme;
65
64
  progress;
66
65
  t;
67
66
  swing;
68
- constructor(from, to, theme, progress, t, swing) {
67
+ constructor(from, to, progress, t, swing) {
69
68
  this.from = from;
70
69
  this.to = to;
71
- this.theme = theme;
72
70
  this.progress = progress;
73
71
  this.t = t;
74
72
  this.swing = swing;
@@ -86,9 +84,9 @@ class ChangeFrame {
86
84
  ...splice(Segment.adjustLineLength(before[row] ?? [], lengths[row]), line, (col) => at(row, col) >= 0.5),
87
85
  ...(row < after.length - 1 || ended ? [Segment.line()] : []),
88
86
  ]);
89
- // The frame says nothing of when it changes: the change's own animation says it moves every frame.
87
+ // The change's own animation says it moves every frame, and reads nothing this frame tells.
90
88
  const veil = veiled(() => (cell) => shows(at(cell.row, cell.col)), this.swing);
91
- return render(new Recolored({ render: () => rows }, veil, { t: this.t, key: "change", theme: this.theme }), options);
89
+ return render(new Recolored({ render: () => rows }, veil, { t: this.t, key: "change" }), options);
92
90
  }
93
91
  measure(options) {
94
92
  return Measurement.get(options, this.to);
@@ -17,9 +17,10 @@
17
17
  * (`JoinedCell`, `../core/strip.ts`), the way the joiner drew it. Shimmer,
18
18
  * pulse, fade, dissolve and a blinking attribute are all this one shape,
19
19
  * differing by what the function does with its inputs
20
- * [LAW:one-type-per-behavior]; two effects stack by nesting one
21
- * wrapper in another, which is the same as calling one after the other but
22
- * for what the outer one measures: the inner one's output.
20
+ * [LAW:one-type-per-behavior]; two effects stack as layers of one effect
21
+ * (`layered`, `./effects.ts`), each moving what the last left and recognising
22
+ * a cell by what the element drew (`EffectCell.pair`), or by nesting one
23
+ * wrapper in another, which measures the inner one's output instead.
23
24
  *
24
25
  * A cell's look is one value, its colours and its attributes together
25
26
  * (`CellLook`), rather than an attribute stage beside the colour one, so
@@ -69,12 +70,11 @@
69
70
  * what the child drew, so a sweep crosses the element, not the screen.
70
71
  */
71
72
  import { type Anchor } from "../core/anchor.js";
72
- import { Animation } from "../core/animation.js";
73
- import type { Hold } from "../core/style.js";
73
+ import { Animation, type Moves, type Shown } from "../core/animation.js";
74
74
  import { ColorDepth, type ColorRgba, type TerminalColors } from "../core/color.js";
75
75
  import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
76
76
  import { Segment } from "../core/segment.js";
77
- import { type Attributes } from "../core/style.js";
77
+ import { type Attributes, type Hold } from "../core/style.js";
78
78
  /**
79
79
  * A cell's foreground and background as they appear on screen — the glyph's
80
80
  * colour and the colour behind it — which are the colours an effect moves.
@@ -98,33 +98,32 @@ export interface CellLook extends CellColors {
98
98
  * Where a cell is in the output of the renderable an effect wraps, its seed:
99
99
  * a number in [0, 1) that is the same for that cell on every frame, for an
100
100
  * effect that varies cell by cell (a per-cell offset in time, a sparkle) —
101
- * and which of its on-screen colours are the terminal's.
101
+ * and what the element drew it in.
102
102
  */
103
103
  export interface EffectCell {
104
104
  readonly row: number;
105
105
  readonly col: number;
106
106
  readonly seed: number;
107
107
  /**
108
- * Whether the glyph's colour and the colour behind it, as the screen shows
109
- * them, are the terminal's own: a slot the element's style leaves unset or
110
- * sets to `default`. Such a colour is resolved through the theme, so its
111
- * hex can equal one the element wrote; this is what tells them apart.
108
+ * The ink and ground the element drew the cell in: what the cell is, where
109
+ * the `CellLook` beside it is what is there now. Under one effect they are
110
+ * the same colours; under `layered` (`./effects.ts`) the look is what the
111
+ * layers beneath left, so an effect recognises a cell by its pair and moves
112
+ * its look. [LAW:one-source-of-truth] A pair's `terminal` is what tells a
113
+ * colour the terminal resolved from one the element wrote at the same hex.
112
114
  */
113
- readonly terminal: {
114
- readonly fg: boolean;
115
- readonly bg: boolean;
116
- };
117
- /** Whether the cell's style swaps its two slots, as `Pair.reverse` says: with `colors` and `terminal`, the cell's `Pair`. */
118
- readonly reverse: boolean;
115
+ readonly pair: Pair;
119
116
  /**
120
117
  * Who drew the cell, and where it sits in their output, as
121
118
  * `Segment.anchorAt` reads it off a frame: what tells one owner's cells
122
119
  * from another's when their colours are alike (`confined`).
123
120
  */
124
121
  readonly anchor: Anchor | undefined;
122
+ /** The drawing of a box the cell is the frame of (`Style.boxFrame`), or none. */
123
+ readonly boxFrame: object | undefined;
125
124
  }
126
125
  /** Where a cell sits and who drew it: an `EffectCell` before it has a seed or colours. */
127
- export type Placed = Pick<EffectCell, "row" | "col" | "anchor">;
126
+ export type Placed = Pick<EffectCell, "row" | "col" | "anchor" | "boxFrame">;
128
127
  /**
129
128
  * A cell as `Drawn.all` lists it: where it sits and who drew it, the
130
129
  * columns its glyph takes, its ink and ground as the screen shows them, and
@@ -145,7 +144,16 @@ export interface ListedCell extends Placed {
145
144
  * is handed for that cell.
146
145
  */
147
146
  export interface Pair extends CellColors {
148
- readonly terminal: EffectCell["terminal"];
147
+ /**
148
+ * Whether the glyph's colour and the colour behind it, as the screen shows
149
+ * them, are the terminal's own: a slot the element's style leaves unset or
150
+ * sets to `default`. Such a colour is resolved through the terminal, so its
151
+ * hex can equal one the element wrote; this is what tells them apart.
152
+ */
153
+ readonly terminal: {
154
+ readonly fg: boolean;
155
+ readonly bg: boolean;
156
+ };
149
157
  /**
150
158
  * Whether the ink is written in the style's ground slot and the ground in
151
159
  * its ink slot. Below truecolor the slot decides what a moved colour can
@@ -194,7 +202,7 @@ export interface Drawn {
194
202
  /** The depth the frame is written at: below truecolor a moved colour lands on a palette colour. */
195
203
  readonly depth: ColorDepth;
196
204
  /** The terminal the frame is drawn on. */
197
- readonly theme: TerminalColors;
205
+ readonly terminal: TerminalColors;
198
206
  }
199
207
  /** What an effect does to the cells of one frame: each cell's look and where it is in, its look out. Return `look` to leave a cell be. */
200
208
  export type EffectFrame = (look: CellLook, cell: EffectCell) => CellLook;
@@ -206,11 +214,12 @@ export type EffectFrame = (look: CellLook, cell: EffectCell) => CellLook;
206
214
  *
207
215
  * Its `nextChange` is when the frame next changes: a stepped effect its next
208
216
  * step's start, a transition `t` until it is done and `Infinity` after, a
209
- * loop that holds every cell still at the frame's depth `Infinity` there, and
210
- * a loop that moves `t` (`Animation.continuous`). [LAW:types-are-the-program]
211
- * The answer is a fact about the frame, so the animation is made from
212
- * everything the frame is drawn from; one made from `t` alone could not say
213
- * that a frame is still because of its depth.
217
+ * loop that holds every cell still, at the frame's depth or with nothing in
218
+ * it to move, `Infinity` there, and a loop that moves `t`
219
+ * (`Animation.continuous`). [LAW:types-are-the-program] The answer is a fact
220
+ * about the frame, so the animation is made from everything the frame is drawn
221
+ * from; one made from `t` alone could not say that a frame is still because of
222
+ * its depth.
214
223
  */
215
224
  export type Effect = (drawn: Drawn) => Animation<EffectFrame>;
216
225
  export interface EffectedOptions {
@@ -219,37 +228,30 @@ export interface EffectedOptions {
219
228
  * not twinkle in lockstep. The same key gives the same seeds on every frame.
220
229
  */
221
230
  readonly key: string;
222
- /**
223
- * The terminal the frame is drawn on: the colours a cell that sets none is
224
- * drawn in, and the sixteen an ANSI colour names. The terminal's own palette
225
- * is never queried, so the caller says what it is.
226
- */
227
- readonly theme: TerminalColors;
228
- }
229
- /** A frame an effect drew at a moment, and the hold the effect says it is in there. */
230
- export interface Moved {
231
- readonly segments: Iterable<Segment>;
232
- readonly hold: Hold;
233
231
  }
232
+ /**
233
+ * The hold an effect drawn at `t` tells: its animation's there, or held for
234
+ * good over no cell, since it changes how cells look and never which exist.
235
+ * What drew none says for itself when it will (a lifecycle not yet entered).
236
+ */
237
+ export declare function holdOver(anyCell: boolean, shown: Shown<unknown>, t: number): Hold;
234
238
  /**
235
239
  * An `Effected` at `t`, or an element through an effect at a time an
236
240
  * animation's `at` was handed — a lifecycle's time since it entered: `renderable`
237
- * with its cells' looks passed through `effect` at `t`, saying nothing of
238
- * when that changes, since whatever placed the animation stamps its hold.
239
- * [LAW:one-type-per-behavior]
241
+ * with its cells' looks passed through `effect` at `t`. Its cells say nothing
242
+ * of when that changes, since `t` is not the frame's clock: it tells the hold
243
+ * it drew in (`RenderOptions.onMoved`), and whatever placed the animation
244
+ * carries that onto the frame's and stamps it. [LAW:one-type-per-behavior]
240
245
  */
241
246
  export declare class Recolored implements Renderable, Measurable {
242
247
  readonly renderable: Renderable;
243
248
  readonly effect: Effect;
244
- readonly t: number;
249
+ private readonly _t;
245
250
  readonly key: string;
246
- readonly theme: TerminalColors;
247
251
  constructor(renderable: Renderable, effect: Effect, options: EffectedOptions & {
248
252
  readonly t: number;
249
253
  });
250
254
  render(options: RenderOptions): Iterable<Segment>;
251
- /** The frame the effect draws at `t`, and the hold it says that is in. */
252
- moved(options: RenderOptions): Moved;
253
255
  private _recolored;
254
256
  /** `segment`'s cells drawn in the looks the effect painted them. */
255
257
  private _repainted;
@@ -275,24 +277,22 @@ export declare class Recolored implements Renderable, Measurable {
275
277
  /**
276
278
  * `renderable` under `effect` over time: an `Animation<Renderable>` whose
277
279
  * value at `t` is the element through the effect at `t`, placed wherever a
278
- * renderable goes. Its own answer to when it changes is `t`, at every time:
279
- * an `Effect` is made from what the element draws, so with nothing drawn
280
- * nothing can say more — early, never false. Placed, it says exactly: its
281
- * cells carry the effect's own hold at the frame's time and the output's
282
- * depth (`stampFrame`), under whatever the element already said, so at a
283
- * depth that shows no motion a loop says it never changes there and the
284
- * host sleeps.
280
+ * renderable goes. An `Effect` is made from what the element draws, so with
281
+ * nothing drawn its answer to when it changes is `t`, at every time: early,
282
+ * never false. Placed, on its own or as a part of any combinator, it says
283
+ * exactly: the frame's drawing told the effect's hold at that time and the
284
+ * output's depth (`Moves`), so at a depth that shows no motion a loop says it
285
+ * never changes there and the host sleeps.
285
286
  */
286
287
  export declare class Effected implements Animation<Renderable> {
287
288
  readonly renderable: Renderable;
288
289
  readonly effect: Effect;
289
290
  readonly key: string;
290
- readonly theme: TerminalColors;
291
291
  constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
292
292
  /** The element through the effect at `t`, unstamped. */
293
293
  at(t: number): Recolored;
294
- nextChange(t: number): number;
295
- lastChange(t: number): number;
294
+ nextChange(t: number, moves?: Moves): number;
295
+ lastChange(t: number, moves?: Moves): number;
296
296
  /** The element under the effect with its motion taken out. */
297
297
  reduced(): Effected;
298
298
  /** Drawn at the frame's time, its cells stamped with the effect's exact hold there. */
@@ -303,12 +303,12 @@ export declare class Effected implements Animation<Renderable> {
303
303
  };
304
304
  /**
305
305
  * The colours the screen shows for `pair` once it is drawn moved to `to`,
306
- * both as the screen shows them, at `depth` on `theme`: what an effect
306
+ * both as the screen shows them, at `depth` on `terminal`: what an effect
307
307
  * moving a pair's colours leaves its words reading in.
308
308
  * [LAW:one-source-of-truth] Read through `movedSpecs`, the one writer a
309
309
  * moved slot is written with.
310
310
  */
311
- static shows(pair: Pair, to: CellColors, depth: ColorDepth, theme: TerminalColors): CellColors;
311
+ static shows(pair: Pair, to: CellColors, depth: ColorDepth, terminal: TerminalColors): CellColors;
312
312
  }
313
313
  /**
314
314
  * A cell's seed: FNV-1a of the element's key and the cell's position, scaled
@@ -351,11 +351,12 @@ export declare function inked(text: string): boolean;
351
351
  */
352
352
  export declare function confined(effect: Effect, ...owners: readonly object[]): Effect;
353
353
  /**
354
- * What `segments`, drawn at `depth` on `theme`, draw their words in, measured
355
- * as `Effected` measures its child for every frame. Measure an element's
356
- * output under an effect to read the contrast its words are left at.
354
+ * What `segments`, drawn at `depth` on `terminal`, draw their words in, measured
355
+ * as `Effected` measures its child for every frame, so `options.terminal` means
356
+ * here what it means on a frame. Measure an element's output under an effect to
357
+ * read the contrast its words are left at.
357
358
  */
358
- export declare function drawnOf(segments: Iterable<Segment>, depth: ColorDepth, theme: TerminalColors): Drawn;
359
+ export declare function drawnOf(segments: Iterable<Segment>, depth: ColorDepth, terminal?: TerminalColors): Drawn;
359
360
  /**
360
361
  * The attributes a look carries, read off anything that sets them — a
361
362
  * `Style`, or a look a frame returned with more on it than `CellAttributes`