@promptctl/rich-js 0.23.0 → 0.24.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 (51) hide show
  1. package/dist/core/clock.d.ts +9 -1
  2. package/dist/core/clock.js +22 -2
  3. package/dist/core/color.d.ts +10 -0
  4. package/dist/core/color.js +36 -1
  5. package/dist/core/easing.d.ts +31 -0
  6. package/dist/core/easing.js +106 -1
  7. package/dist/core/segment.d.ts +10 -0
  8. package/dist/core/segment.js +15 -0
  9. package/dist/core/strip.d.ts +104 -27
  10. package/dist/core/strip.js +231 -124
  11. package/dist/core/style.d.ts +18 -1
  12. package/dist/core/style.js +32 -3
  13. package/dist/core/text.js +4 -1
  14. package/dist/host/app.js +2 -4
  15. package/dist/host/index.d.ts +1 -1
  16. package/dist/host/input.d.ts +37 -13
  17. package/dist/host/input.js +146 -56
  18. package/dist/host/terminal-host.d.ts +16 -0
  19. package/dist/host/terminal-host.js +1 -0
  20. package/dist/index.d.ts +11 -9
  21. package/dist/index.js +5 -4
  22. package/dist/node/terminal-host.d.ts +8 -1
  23. package/dist/node/terminal-host.js +2 -0
  24. package/dist/renderables/effect.d.ts +78 -10
  25. package/dist/renderables/effect.js +143 -39
  26. package/dist/renderables/effects.d.ts +73 -26
  27. package/dist/renderables/effects.js +412 -92
  28. package/dist/renderables/embed.d.ts +10 -0
  29. package/dist/renderables/embed.js +15 -7
  30. package/dist/renderables/frames.d.ts +95 -0
  31. package/dist/renderables/frames.js +106 -0
  32. package/dist/renderables/live.d.ts +23 -7
  33. package/dist/renderables/live.js +68 -34
  34. package/dist/renderables/markdown.d.ts +5 -2
  35. package/dist/renderables/markdown.js +210 -19
  36. package/dist/renderables/progress.d.ts +1 -1
  37. package/dist/renderables/progress.js +3 -1
  38. package/dist/renderables/progressBar.js +3 -3
  39. package/dist/renderables/spinner.d.ts +62 -28
  40. package/dist/renderables/spinner.js +99 -52
  41. package/dist/renderables/status.d.ts +8 -3
  42. package/dist/renderables/status.js +8 -5
  43. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  44. package/dist/template-bindings/animation-funcs.js +83 -0
  45. package/dist/template-bindings/color-funcs.d.ts +13 -0
  46. package/dist/template-bindings/color-funcs.js +1 -1
  47. package/dist/template-bindings/helpers.d.ts +6 -0
  48. package/dist/template-bindings/helpers.js +12 -3
  49. package/dist/template-bindings/index.d.ts +6 -2
  50. package/dist/template-bindings/index.js +6 -2
  51. package/package.json +1 -1
@@ -42,7 +42,15 @@ export interface Clock {
42
42
  now(): number;
43
43
  /** Call `tick` once a frame at `rate` until the returned function is called. */
44
44
  every(rate: FrameRate, tick: () => void): Unsubscribe;
45
+ /**
46
+ * Call `tick` once, when this clock reads `time` — as soon as it can for a
47
+ * time already past — unless the returned function is called first. What
48
+ * a host that knows when its frame next changes (`Segment.nextChange`)
49
+ * waits on. A `time` further off than a timer can wait, about 24.8 days,
50
+ * throws, as `frameRate` refuses the rate.
51
+ */
52
+ at(time: number, tick: () => void): Unsubscribe;
45
53
  }
46
- /** The platform's monotonic clock and its interval timer. */
54
+ /** The platform's monotonic clock and its timers. */
47
55
  export declare function systemClock(): Clock;
48
56
  export {};
@@ -30,13 +30,33 @@ export function frameRate(perSecond) {
30
30
  }
31
31
  return { perSecond, interval: 1 / perSecond };
32
32
  }
33
- /** The platform's monotonic clock and its interval timer. */
33
+ /** The platform's monotonic clock and its timers. */
34
34
  export function systemClock() {
35
+ const now = () => performance.now() / 1000;
35
36
  return {
36
- now: () => performance.now() / 1000,
37
+ now,
37
38
  every: (rate, tick) => {
38
39
  const timer = setInterval(tick, rate.interval * 1000);
39
40
  return () => clearInterval(timer);
40
41
  },
42
+ at: (time, tick) => {
43
+ if (!(time - now() <= LONGEST_INTERVAL)) {
44
+ throw new RangeError(`a clock waits at most ${LONGEST_INTERVAL} s, not until ${time} s from ${now()} s`);
45
+ }
46
+ // [LAW:single-enforcer] A platform timer is due by a whole millisecond
47
+ // of the event loop's own time, truncated, so it can run while this
48
+ // clock still reads under `time` — by most of a millisecond. The
49
+ // contract is the clock's reading, and it is held here, once, for every
50
+ // host: the wait is rounded up, and a timer that runs early is armed
51
+ // again for what remains, so `tick` runs when `now()` has reached
52
+ // `time` and not before. A host re-drawing at a frame's end would
53
+ // otherwise draw the old frame again and wait again, every boundary.
54
+ let timer;
55
+ const arm = () => {
56
+ timer = setTimeout(() => (now() < time ? arm() : tick()), Math.ceil(Math.max(0, time - now()) * 1000));
57
+ };
58
+ arm();
59
+ return () => clearTimeout(timer);
60
+ },
41
61
  };
42
62
  }
@@ -259,6 +259,16 @@ export declare class ColorSpec {
259
259
  * on a ground (`flattenAlpha`).
260
260
  */
261
261
  static matchOn(value: ColorRgba, depth: ColorDepth, theme: TerminalTheme, foreground: boolean): ColorSpec;
262
+ /**
263
+ * Every colour `depth` writes by index, each with the colour a terminal
264
+ * known to show `theme` draws it in, lowest index first: the theme's
265
+ * sixteen, and at 256 colours the cube and grey ramp after them. Each is
266
+ * drawn alike as glyph and as ground, which the default colour is not, so
267
+ * it is not among them. Truecolor writes a colour itself and the default
268
+ * depth only the default, so neither writes any by index. The colours
269
+ * `matchOn` rounds to, but for the default, read off the same two tables.
270
+ */
271
+ static indexed(depth: ColorDepth, theme: TerminalTheme): readonly (readonly [ColorSpec, ColorRgba])[];
262
272
  /**
263
273
  * The spec a slot is written in once its colour moves from `was`, what its
264
274
  * written spec draws on `theme`, to the opaque `now`: `matchOn`'s — or
@@ -614,9 +614,11 @@ export class ColorSpec {
614
614
  return ColorSpec.fromRgba(value);
615
615
  if (depth === ColorDepth.DEFAULT)
616
616
  return ColorSpec.default();
617
- const slot = theme.ansiColors.match(value);
618
617
  // [LAW:dataflow-not-control-flow] Every candidate is scored; the depth
619
618
  // only decides whether the cube is among them. Ties go to the earlier.
619
+ // Each table's nearest stands in for all of it: the tables' `match` is
620
+ // memoised and ranks by this distance, so the winner is `indexed`'s.
621
+ const slot = theme.ansiColors.match(value);
620
622
  const candidates = [
621
623
  [ColorSpec.default(), foreground ? theme.foregroundColor : theme.backgroundColor],
622
624
  [new ColorSpec(`color(${slot})`, depth === ColorDepth.WINDOWS ? depth : ColorDepth.STANDARD, slot), theme.ansiColors.get(slot)],
@@ -624,6 +626,22 @@ export class ColorSpec {
624
626
  ];
625
627
  return candidates.reduce((best, next) => (rgbDistance(next[1], value) < rgbDistance(best[1], value) ? next : best))[0];
626
628
  }
629
+ /**
630
+ * Every colour `depth` writes by index, each with the colour a terminal
631
+ * known to show `theme` draws it in, lowest index first: the theme's
632
+ * sixteen, and at 256 colours the cube and grey ramp after them. Each is
633
+ * drawn alike as glyph and as ground, which the default colour is not, so
634
+ * it is not among them. Truecolor writes a colour itself and the default
635
+ * depth only the default, so neither writes any by index. The colours
636
+ * `matchOn` rounds to, but for the default, read off the same two tables.
637
+ */
638
+ static indexed(depth, theme) {
639
+ const byDepth = indexedMemo.get(theme) ?? new Map();
640
+ indexedMemo.set(theme, byDepth);
641
+ const listed = byDepth.get(depth) ?? listIndexed(depth, theme);
642
+ byDepth.set(depth, listed);
643
+ return listed;
644
+ }
627
645
  /**
628
646
  * The spec a slot is written in once its colour moves from `was`, what its
629
647
  * written spec draws on `theme`, to the opaque `now`: `matchOn`'s — or
@@ -670,6 +688,23 @@ function cubeCandidate(value) {
670
688
  const index = EIGHT_BIT_DOWNGRADE_TABLE.match(value);
671
689
  return [ColorSpec.fromAnsi(index), EIGHT_BIT_DOWNGRADE_TABLE.get(index)];
672
690
  }
691
+ /** `ColorSpec.indexed`'s list, made once for each theme and depth. */
692
+ const indexedMemo = new WeakMap();
693
+ function listIndexed(depth, theme) {
694
+ const slots = () => Array.from({ length: 16 }, (_, slot) => [new ColorSpec(`color(${slot})`, depth === ColorDepth.WINDOWS ? depth : ColorDepth.STANDARD, slot), theme.ansiColors.get(slot)]);
695
+ switch (depth) {
696
+ case ColorDepth.TRUECOLOR:
697
+ case ColorDepth.DEFAULT:
698
+ return [];
699
+ case ColorDepth.STANDARD:
700
+ case ColorDepth.WINDOWS:
701
+ return slots();
702
+ case ColorDepth.EIGHT_BIT: {
703
+ const { firstIndex, size } = EIGHT_BIT_DOWNGRADE_TABLE;
704
+ return [...slots(), ...Array.from({ length: size }, (_, i) => [ColorSpec.fromAnsi(firstIndex + i), EIGHT_BIT_DOWNGRADE_TABLE.get(firstIndex + i)])];
705
+ }
706
+ }
707
+ }
673
708
  function mix(from, to, t, theme) {
674
709
  return blendRgb(from.getTruecolor(theme), to.getTruecolor(theme), t);
675
710
  }
@@ -75,6 +75,37 @@ export type EaseName = keyof typeof EASES;
75
75
  * Unknown names throw naming every legal one. [LAW:no-silent-failure]
76
76
  */
77
77
  export declare function parseEase(name: string): Ease;
78
+ /**
79
+ * A beat: holds of given lengths in seconds, played in order and looped, the
80
+ * way the frames of an animation are each held and then replaced. `at(t)` is
81
+ * the index of the hold in progress at `t`, counted from the first hold at
82
+ * `t = 0`, and the sequence wraps every `period` seconds, before zero too,
83
+ * as `Phase.loop` wraps. With equal holds `at` is `steps(n)` read as an
84
+ * index; the holds here may differ, which `steps` cannot say, so a frame
85
+ * held for a second and the next for a quarter are one beat.
86
+ */
87
+ export interface Beat {
88
+ readonly holds: readonly number[];
89
+ /** The sum of the holds: how long one pass takes, in seconds. */
90
+ readonly period: number;
91
+ /** Which hold is in progress at `t` seconds: an index into `holds`. */
92
+ at(t: number): number;
93
+ /**
94
+ * When the hold in progress at `t` began, in seconds on `t`'s own
95
+ * timeline: what a sample-and-hold on this beat reads at `t`. Every `t`
96
+ * inside one hold of one pass answers the same number, to the bit, so an
97
+ * effect handed it draws the same frame throughout.
98
+ */
99
+ start(t: number): number;
100
+ /**
101
+ * When the hold in progress at `t` ends, in seconds on `t`'s own timeline:
102
+ * the start of the next hold, the first moment `at` places in it, so it is
103
+ * always after `t`. What is drawn on this beat at `t` is drawn until then.
104
+ */
105
+ end(t: number): number;
106
+ }
107
+ /** `holds` as a `Beat`; a beat with no hold, or a hold that is not a positive number of seconds, throws. */
108
+ export declare function beat(holds: readonly number[]): Beat;
78
109
  /** The three phase shapes. Each takes seconds and returns a `Phase`. */
79
110
  export declare const Phase: {
80
111
  /** 0 before `start`, rising to 1 over `duration` seconds, then held at 1. */
@@ -148,6 +148,111 @@ function positiveSeconds(what, seconds) {
148
148
  throw new RangeError(`${what} must be a positive number of seconds, got ${seconds}`);
149
149
  }
150
150
  }
151
+ /**
152
+ * The seconds into the current pass of a `period` at `t`: `t` taken modulo
153
+ * the period, into [0, period), before zero too. [LAW:single-enforcer] The
154
+ * one wrap `Phase.loop` and `beat` both read time through, so a `t` that is
155
+ * not a finite number of seconds fails here, loudly, and nowhere else. A
156
+ * `t` at or past zero is `t % period` alone, which is exact; only a negative
157
+ * `t` is carried up by a period, so a boundary a host samples exactly lands
158
+ * on the pass that begins there, and the answer is always under the period.
159
+ */
160
+ function wrapSeconds(what, t, period) {
161
+ if (!Number.isFinite(t))
162
+ throw new RangeError(`${what} needs a finite t, got ${t}`);
163
+ const into = t % period;
164
+ if (into >= 0)
165
+ return into;
166
+ // A negative epsilon carried up rounds to the period itself; that is the
167
+ // start of the next pass, never a point past the end of this one.
168
+ const carried = into + period;
169
+ return carried >= period ? 0 : carried;
170
+ }
171
+ /**
172
+ * The float after `x`: one ulp up, away from zero for a positive `x` and
173
+ * toward it for a negative one, read through the bits of the double.
174
+ */
175
+ function nextUp(x) {
176
+ if (x === 0)
177
+ return Number.MIN_VALUE;
178
+ DOUBLE[0] = x;
179
+ BITS[0] = BITS[0] + (x > 0 ? 1n : -1n);
180
+ return DOUBLE[0];
181
+ }
182
+ const DOUBLE = new Float64Array(1);
183
+ const BITS = new BigUint64Array(DOUBLE.buffer);
184
+ /** How many ulps the two roundings in `Beat.start`'s sum can put it under the hold's boundary; a walk past this is a hold too fine for the time. */
185
+ const ULPS_OF_ROUNDING = 8;
186
+ /** `holds` as a `Beat`; a beat with no hold, or a hold that is not a positive number of seconds, throws. */
187
+ export function beat(holds) {
188
+ if (holds.length === 0)
189
+ throw new RangeError("a beat needs at least one hold");
190
+ holds.forEach((hold, i) => positiveSeconds(`beat hold ${i}`, hold));
191
+ const period = holds.reduce((sum, hold) => sum + hold, 0);
192
+ // Where each hold begins, in seconds into the period: the unit
193
+ // `wrapSeconds` answers in, so the sum a boundary sits at is the number
194
+ // the wrap of a `t` on that boundary produces, and the hold that begins
195
+ // there is the one in progress, at any rate the host samples.
196
+ const starts = [];
197
+ let elapsed = 0;
198
+ for (const hold of holds) {
199
+ starts.push(elapsed);
200
+ elapsed += hold;
201
+ }
202
+ // A copy, and the one `at` walks: what `at` and `period` were built from
203
+ // stays what `holds` says whatever the caller does to its array.
204
+ const own = [...holds];
205
+ // The last hold whose start the time has reached. The first starts at
206
+ // 0, so the walk always stops, and its length is the count of holds,
207
+ // never a function of `t`.
208
+ const index = (into) => {
209
+ let i = own.length - 1;
210
+ while (starts[i] > into)
211
+ i--;
212
+ return i;
213
+ };
214
+ // Where hold `i` of pass `pass` begins: the pass's start plus the hold's,
215
+ // two integers and two constants, so every `t` in the hold builds the one
216
+ // float. That sum can land an ulp or two under the boundary when the holds
217
+ // are not exact in binary — a tenth of a second is not — and `at` would
218
+ // then read it as the hold before, so it is walked up to the first float
219
+ // `at` places in the hold. The walk is a function of the pass and the hold
220
+ // alone, so every `t` in the hold still answers one float.
221
+ const boundary = (what, pass, i, t) => {
222
+ let began = pass * period + starts[i];
223
+ for (let walked = 0; index(wrapSeconds(what, began, period)) !== i; walked++) {
224
+ // The sum is off by the rounding of two operations, under the few
225
+ // ulps allowed here; a walk that long means the hold is narrower
226
+ // than the floats around `t` are apart, so no float starts it.
227
+ if (walked === ULPS_OF_ROUNDING)
228
+ throw new RangeError(`${what}: hold ${i} (${own[i]} s) is finer than time resolves near t = ${t}`);
229
+ began = nextUp(began);
230
+ }
231
+ return began;
232
+ };
233
+ // Which pass `t` is in, as a whole number: `t - into` is a whole number of
234
+ // periods but for rounding, which `round` takes off; carrying `starts[i]`
235
+ // out of `t` itself would leave each sample its own rounding.
236
+ const pass = (t, into) => Math.round((t - into) / period);
237
+ return {
238
+ holds: own,
239
+ period,
240
+ at(t) {
241
+ return index(wrapSeconds("Beat.at", t, period));
242
+ },
243
+ start(t) {
244
+ const into = wrapSeconds("Beat.start", t, period);
245
+ return boundary("Beat.start", pass(t, into), index(into), t);
246
+ },
247
+ // The next hold's start, in the next pass when this hold is the last.
248
+ end(t) {
249
+ const into = wrapSeconds("Beat.end", t, period);
250
+ const i = index(into);
251
+ const last = i === own.length - 1;
252
+ return boundary("Beat.end", pass(t, into) + (last ? 1 : 0), last ? 0 : i + 1, t);
253
+ },
254
+ };
255
+ }
151
256
  /** The three phase shapes. Each takes seconds and returns a `Phase`. */
152
257
  export const Phase = {
153
258
  /** 0 before `start`, rising to 1 over `duration` seconds, then held at 1. */
@@ -160,7 +265,7 @@ export const Phase = {
160
265
  /** 0 → 1 every `period` seconds, wrapping back to 0; negative `t` wraps too. */
161
266
  loop(period) {
162
267
  positiveSeconds("Phase.loop period", period);
163
- return (t) => (((t % period) + period) % period) / period;
268
+ return (t) => wrapSeconds("Phase.loop", t, period) / period;
164
269
  },
165
270
  /** 0 → 1 → 0 every `period` seconds: there in the first half, back in the second. */
166
271
  pingPong(period) {
@@ -51,6 +51,16 @@ export declare class Segment {
51
51
  * the owner's row that `lines[0]` is, for output that arrives in pieces.
52
52
  */
53
53
  static anchorLines(lines: Segment[][], owner: object, firstRow?: number): Segment[][];
54
+ /**
55
+ * When what `segments` show next changes, in seconds on the timeline of
56
+ * whoever drew them: the soonest `Style.nextChange` among them, read off
57
+ * the composed frame the way `anchorAt` reads an owner — `t` itself for a
58
+ * frame something in moves every frame, `Infinity` for one that never
59
+ * changes. What a host that drew the frame at `t` asks before drawing
60
+ * again: a frame of frames answers the next frame's start, a transition
61
+ * `t` until it is done, still content never.
62
+ */
63
+ static nextChange(segments: Iterable<Segment>): number;
54
64
  /**
55
65
  * The anchor of the cell at column `x` of row `y` in a composed frame, or
56
66
  * `undefined` when that cell was drawn by no owner or lies outside the frame.
@@ -91,6 +91,21 @@ export class Segment {
91
91
  });
92
92
  });
93
93
  }
94
+ /**
95
+ * When what `segments` show next changes, in seconds on the timeline of
96
+ * whoever drew them: the soonest `Style.nextChange` among them, read off
97
+ * the composed frame the way `anchorAt` reads an owner — `t` itself for a
98
+ * frame something in moves every frame, `Infinity` for one that never
99
+ * changes. What a host that drew the frame at `t` asks before drawing
100
+ * again: a frame of frames answers the next frame's start, a transition
101
+ * `t` until it is done, still content never.
102
+ */
103
+ static nextChange(segments) {
104
+ let soonest = Number.POSITIVE_INFINITY;
105
+ for (const segment of segments)
106
+ soonest = Math.min(soonest, segment.style?.nextChange ?? Number.POSITIVE_INFINITY);
107
+ return soonest;
108
+ }
94
109
  /**
95
110
  * The anchor of the cell at column `x` of row `y` in a composed frame, or
96
111
  * `undefined` when that cell was drawn by no owner or lies outside the frame.
@@ -156,22 +156,49 @@ export interface JoinCell {
156
156
  readonly bg: CellColor;
157
157
  }
158
158
  /**
159
- * The cells a joiner draws at each kind of join:
160
- * • `seam` — between two neighbours;
159
+ * A cell of a seam, or one of the joiner's caps standing in a seam's place:
160
+ * `{ cap: "tail" }` closes the run the seam leaves, `{ cap: "lead" }` opens the
161
+ * run it enters, drawn as whatever caps the seam is joined with. That is how
162
+ * a seam that parts its runs — `capsule`, `metro` — ends each in the caps
163
+ * picked for it rather than in caps of its own.
164
+ */
165
+ export type SeamCell = JoinCell | {
166
+ readonly cap: "lead" | "tail";
167
+ };
168
+ /**
169
+ * What a joiner draws between two neighbours, in one form:
170
+ * • `seam` — the cells between them;
161
171
  * • `divider` — in the seam's place where every seam cell's ink is drawn
162
172
  * indistinct from its own ground (see `SEAM_MIN_DELTA_E`), so the seam
163
- * would vanish;
164
- * the same width as `seam`;
173
+ * would vanish; the same width as `seam`.
174
+ */
175
+ export interface SeamForm {
176
+ readonly seam: readonly SeamCell[];
177
+ readonly divider: readonly SeamCell[];
178
+ }
179
+ /**
180
+ * What a joiner draws at the ends of a run, in one form:
165
181
  * • `lead` — where a run is entered from nothing: the strip's start, or a
166
182
  * neighbour with no ground for the seam to paint. It cannot name `left`;
167
183
  * • `tail` — where a run leaves into nothing. It cannot name `right`.
168
184
  */
169
- export interface JoinShape {
170
- readonly seam: readonly JoinCell[];
171
- readonly divider: readonly JoinCell[];
185
+ export interface CapsForm {
172
186
  readonly lead: readonly JoinCell[];
173
187
  readonly tail: readonly JoinCell[];
174
188
  }
189
+ /**
190
+ * The middle of a joiner: its seam, and the `ascii` seam an ASCII-only output
191
+ * draws instead. Picked apart from the caps, so any seam ends in any caps.
192
+ */
193
+ export interface JoinSeam {
194
+ readonly glyph: SeamForm;
195
+ readonly ascii: SeamForm;
196
+ }
197
+ /** The ends of a joiner: its caps, and the `ascii` caps an ASCII-only output draws instead. */
198
+ export interface JoinCaps {
199
+ readonly glyph: CapsForm;
200
+ readonly ascii: CapsForm;
201
+ }
175
202
  /**
176
203
  * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
177
204
  * seam to be seen. Below it the eye cannot tell the ink from the ground,
@@ -180,28 +207,70 @@ export interface JoinShape {
180
207
  */
181
208
  export declare const SEAM_MIN_DELTA_E = 0.04;
182
209
  /**
183
- * The one joiner: what it draws at each join is the `JoinShape` it is given,
184
- * so a new joiner is a value, never a class.
210
+ * The one joiner: what it draws is a `JoinSeam` between neighbours and a
211
+ * `JoinCaps` at the ends of each run, picked independently, so a new joiner is
212
+ * a pair of values, never a class.
185
213
  *
186
214
  * Which part a join draws is read off its two neighbours. A neighbour is
187
215
  * missing at the strip's ends, and counts as missing — an endpoint — when the
188
- * shape paints with its ground anywhere and it has none to paint. With both present the
189
- * join is the seam, or the divider where every seam cell's ink is drawn
190
- * indistinct from its ground at `options.colorSystem`; with only the right, the lead; with only
191
- * the left, the tail; with neither, nothing.
216
+ * seam paints with its ground anywhere and it has none to paint. With both
217
+ * present the join is the seam, or the divider where every seam cell's ink is
218
+ * drawn indistinct from its ground at `options.colorSystem`; with only the
219
+ * right, the lead; with only the left, the tail; with neither, nothing. A cap
220
+ * that paints with its neighbour's ground is drawn only beside a neighbour
221
+ * with one, so caps that need grounds never stop a seam that needs none.
192
222
  *
193
- * `ascii` is the shape an ASCII-only output draws instead, whole.
223
+ * An ASCII-only output draws the `ascii` form of both, whole.
194
224
  */
195
225
  export declare class CellJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
196
- readonly shape: JoinShape;
197
- readonly ascii: JoinShape;
226
+ readonly seam: JoinSeam;
227
+ readonly caps: JoinCaps;
198
228
  private readonly _forms;
199
- constructor(shape: JoinShape, ascii: JoinShape);
229
+ constructor(seam: JoinSeam, caps: JoinCaps);
200
230
  join(left: T | null, right: T | null, { index }: JoinContext): Renderable;
201
231
  }
202
232
  /**
203
- * The bundled joiners, by name. What each draws is `docs/strip.md`'s
204
- * "Built-in joiners"; a name from config is read with `joinerNamed`.
233
+ * The bundled seams, by name: what a joiner draws between two neighbours.
234
+ * What each draws is `docs/strip.md`'s "Seams"; any of them ends in any of
235
+ * `CAPS`.
236
+ */
237
+ export declare const SEAMS: Readonly<{
238
+ powerline: JoinSeam;
239
+ prism: JoinSeam;
240
+ slant: JoinSeam;
241
+ backslant: JoinSeam;
242
+ flame: JoinSeam;
243
+ pixel: JoinSeam;
244
+ ice: JoinSeam;
245
+ bubble: JoinSeam;
246
+ capsule: JoinSeam;
247
+ wrapper: JoinSeam;
248
+ metro: JoinSeam;
249
+ stitch: JoinSeam;
250
+ torn: JoinSeam;
251
+ plain: JoinSeam;
252
+ }>;
253
+ /**
254
+ * The bundled caps, by name: what a joiner draws where a run begins and ends.
255
+ * What each draws is `docs/strip.md`'s "Caps"; any of them ends any of
256
+ * `SEAMS`.
257
+ */
258
+ export declare const CAPS: Readonly<{
259
+ powerline: JoinCaps;
260
+ slant: JoinCaps;
261
+ backslant: JoinCaps;
262
+ flame: JoinCaps;
263
+ pixel: JoinCaps;
264
+ ice: JoinCaps;
265
+ bubble: JoinCaps;
266
+ capsule: JoinCaps;
267
+ wrapper: JoinCaps;
268
+ flat: JoinCaps;
269
+ }>;
270
+ /**
271
+ * The bundled joiners, by name: each a seam from `SEAMS` ended in caps from
272
+ * `CAPS`. What each draws is `docs/strip.md`'s "Built-in joiners"; a name from
273
+ * config is read with `joinerNamed`.
205
274
  */
206
275
  export declare const JOINERS: Readonly<{
207
276
  powerline: CellJoiner<StyledRenderable>;
@@ -220,16 +289,24 @@ export declare const JOINERS: Readonly<{
220
289
  plain: CellJoiner<StyledRenderable>;
221
290
  }>;
222
291
  /**
223
- * The bundled joiner `name` names — a name read from config, parsed. Throws on
224
- * any other string, naming the ones there are; an own-key test, so `toString`
225
- * and the rest of `Object.prototype` are not joiners.
292
+ * A joiner as config names it: one of `JOINERS`, or a seam from `SEAMS` ended
293
+ * in caps from `CAPS`, each named apart.
294
+ */
295
+ export type JoinerSpec = string | {
296
+ readonly seam: string;
297
+ readonly caps: string;
298
+ };
299
+ /**
300
+ * The joiner `spec` names — a joiner read from config, parsed. Throws on a
301
+ * name its table lacks, naming the ones there are; an own-key test, so
302
+ * `toString` and the rest of `Object.prototype` are no one's name.
226
303
  */
227
- export declare function joinerNamed(name: string): CellJoiner;
304
+ export declare function joinerNamed(spec: JoinerSpec): CellJoiner;
228
305
  /**
229
306
  * A ramp of `steps` cells from the left ground to the right. Each cell is the
230
307
  * half block U+258C, ink on its left half and ground on its right, so it
231
- * carries two samples; ASCII draws a space, keeping the grounds alone. Nothing
232
- * at the ends, or beside a neighbour with no ground — there is no colour to
233
- * ramp toward.
308
+ * carries two samples; ASCII draws a space, keeping the grounds alone. Beside
309
+ * a neighbour with no ground there is no colour to ramp toward, so that join
310
+ * draws the caps the ramp is ended in.
234
311
  */
235
- export declare function gradientJoiner(steps?: number): CellJoiner;
312
+ export declare function gradientSeam(steps?: number): JoinSeam;