@promptctl/rich-js 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -5
@@ -31,7 +31,7 @@ import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
31
31
  import { Oklch } from "./oklch.js";
32
32
  import { cellLen } from "./cells.js";
33
33
  import { fnv1a } from "./fnv1a.js";
34
- import { drawable, getStyle, isAscii } from "./protocol.js";
34
+ import { drawable, getStyle, isAscii, render } from "./protocol.js";
35
35
  /**
36
36
  * The owner every cell a strip's joiner draws is stamped with
37
37
  * (`Segment.anchorLines`), whatever the joiner: a seam is told by what drew
@@ -80,7 +80,7 @@ export function colorsOwnedBy(anchor, owners) {
80
80
  */
81
81
  export function drawJoin(joiner, left, right, context) {
82
82
  const join = joiner.join(left, right, context);
83
- return { render: (options) => Segment.anchorLines([[...join.render(options)]], JOIN)[0] };
83
+ return { render: (options) => Segment.anchorLines([[...render(join, options)]], JOIN)[0] };
84
84
  }
85
85
  /**
86
86
  * [LAW:single-enforcer] What `item` draws, every cell stamped as the item's,
@@ -89,7 +89,7 @@ export function drawJoin(joiner, left, right, context) {
89
89
  * item.
90
90
  */
91
91
  export function drawItem(item) {
92
- return { render: (options) => Segment.anchorLines([[...item.render(options)]], item)[0] };
92
+ return { render: (options) => Segment.anchorLines([[...render(item, options)]], item)[0] };
93
93
  }
94
94
  // --- Strip ---
95
95
  export class Strip {
@@ -112,12 +112,12 @@ export class Strip {
112
112
  // `left` fills the canvas, and the first item would pad over the rest of
113
113
  // the row. Where the row sits in the width is its container's to say.
114
114
  const row = { ...options, justify: undefined };
115
- yield* drawJoin(this.joiner, null, items[0], { index: 0 }).render(row);
115
+ yield* render(drawJoin(this.joiner, null, items[0], { index: 0 }), row);
116
116
  for (let i = 0; i < items.length; i++) {
117
117
  const item = items[i];
118
- yield* drawItem(item).render(row);
118
+ yield* render(drawItem(item), row);
119
119
  const next = i + 1 < items.length ? items[i + 1] : null;
120
- yield* drawJoin(this.joiner, item, next, { index: i + 1 }).render(row);
120
+ yield* render(drawJoin(this.joiner, item, next, { index: i + 1 }), row);
121
121
  }
122
122
  // [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
123
123
  // it emits, including the last, and the other block-level renderables
@@ -131,6 +131,10 @@ export class Strip {
131
131
  // of which renderables end their own line and which are fragments.
132
132
  yield Segment.line();
133
133
  }
134
+ // The items; a join is drawn from what the joiner emits for it, not held.
135
+ contents() {
136
+ return this.items;
137
+ }
134
138
  /**
135
139
  * [LAW:one-source-of-truth] The width of the one row it draws, and that is
136
140
  * both bounds: the row never wraps and no item is offered less than the whole
@@ -140,7 +144,7 @@ export class Strip {
140
144
  */
141
145
  measure(options) {
142
146
  let width = 0;
143
- for (const segment of this.render(options))
147
+ for (const segment of render(this, options))
144
148
  width += segment.cellLength;
145
149
  return { minimum: width, maximum: width };
146
150
  }
@@ -194,59 +198,106 @@ function parsePaint(raw, absent, refuse) {
194
198
  }
195
199
  const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
196
200
  const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
197
- function parseList(raw, where) {
198
- if (!Array.isArray(raw))
199
- throw new Error(`CellJoiner: ${where} is not a list of cells`);
200
- return raw.map((cell, i) => {
201
- if (typeof cell !== "object" || cell === null)
202
- throw new Error(`CellJoiner: ${where}[${i}] is not a cell`);
203
- return cell;
201
+ // Which argument of a joiner a part is authored in, so a refusal names it.
202
+ const AUTHORED_IN = { seam: "seam", divider: "seam", lead: "caps", tail: "caps" };
203
+ function parseCell(cell, form, part, refuse) {
204
+ const glyph = cell.glyph;
205
+ const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
206
+ if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
207
+ refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
208
+ }
209
+ if (cell.fg === "ground" && cell.bg === "ground")
210
+ refuse("has ground on both sides");
211
+ const glyphs = glyphsOf(cell);
212
+ if (glyphs.length === 0)
213
+ refuse("has an empty oneOf");
214
+ if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
215
+ refuse("has oneOf members of different widths");
216
+ if (form === "ascii" && !glyphs.every(isAscii))
217
+ refuse("is not ASCII");
218
+ return Object.freeze({
219
+ glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
220
+ fg: parseColor(cell.fg, ABSENT[part], refuse),
221
+ bg: parseColor(cell.bg, ABSENT[part], refuse),
204
222
  });
205
223
  }
206
- /**
207
- * [LAW:parse-dont-validate] The one place a shape is checked, so every rule
208
- * the cell model states is refused at construction and the render reads a
209
- * shape that already holds them. A shape is often authored as data — a
210
- * consumer's config — so the rules the types state are checked here too, and
211
- * what comes back is a frozen copy the caller can no longer change.
212
- */
213
- function parseShape(raw, form) {
214
- const parseCells = (part) => Object.freeze(parseList(raw[part], `${form} ${part}`).map((cell, i) => {
224
+ // `raw[form][part]` as frozen cells, each checked by `parseOne`, or refused
225
+ // naming where it sits.
226
+ function parseCells(raw, form, part, parseOne) {
227
+ const forms = raw;
228
+ const where = AUTHORED_IN[part];
229
+ if (typeof forms?.[form] !== "object" || forms[form] === null)
230
+ throw new Error(`${where}: has no ${form} form`);
231
+ const cells = forms[form][part];
232
+ if (!Array.isArray(cells))
233
+ throw new Error(`${where}: ${form} ${part} is not a list of cells`);
234
+ return Object.freeze(cells.map((cell, i) => {
215
235
  const refuse = (why) => {
216
- throw new Error(`CellJoiner: ${form} ${part}[${i}] ${why}`);
236
+ throw new Error(`${where}: ${form} ${part}[${i}] ${why}`);
217
237
  };
218
- const glyph = cell.glyph;
219
- const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
220
- if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
221
- refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
222
- }
223
- if (cell.fg === "ground" && cell.bg === "ground")
224
- refuse("has ground on both sides");
225
- const glyphs = glyphsOf(cell);
226
- if (glyphs.length === 0)
227
- refuse("has an empty oneOf");
228
- if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
229
- refuse("has oneOf members of different widths");
230
- if (form === "ascii" && !glyphs.every(isAscii))
231
- refuse("is not ASCII");
232
- return Object.freeze({
233
- glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
234
- fg: parseColor(cell.fg, ABSENT[part], refuse),
235
- bg: parseColor(cell.bg, ABSENT[part], refuse),
236
- });
238
+ return typeof cell === "object" && cell !== null ? parseOne(cell, refuse) : refuse("is not a cell");
237
239
  }));
238
- const seam = parseCells("seam");
239
- // A divider authored as the seam itself stays the seam, so a render can see
240
- // it has no choice to make.
241
- const divider = raw.divider === raw.seam ? seam : parseCells("divider");
242
- if (partWidth(divider) !== partWidth(seam))
243
- throw new Error(`CellJoiner: ${form} divider is not the width of the seam`);
244
- return Object.freeze({ seam, divider, lead: parseCells("lead"), tail: parseCells("tail") });
245
240
  }
246
- // Whether any cell of `shape` paints with `side`'s ground.
247
- const readsGround = (shape, side) => PARTS.flatMap((part) => shape[part]).some((cell) => [cell.fg, cell.bg].some((color) => color !== "ground" && [...paintsIn(color)].some((p) => "edge" in p && p.edge === side && p.part === "bg")));
241
+ const asCell = (form, part) => (cell, refuse) => parseCell(cell, form, part, refuse);
242
+ const asSeamCell = (form, part) => (cell, refuse) => {
243
+ if (!("cap" in cell))
244
+ return parseCell(cell, form, part, refuse);
245
+ const { cap } = cell;
246
+ if (Object.keys(cell).length !== 1)
247
+ refuse("names a cap and draws cells of its own; a cell is one or the other");
248
+ return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
249
+ };
250
+ // A seam's width as a sum over whatever caps stand in it: its own cells', and
251
+ // how many times it names each cap. Two parts equal in all three are the same
252
+ // width under every caps.
253
+ const seamWidth = (cells) => [
254
+ partWidth(cells.filter((cell) => !("cap" in cell))),
255
+ cells.filter((cell) => "cap" in cell && cell.cap === "lead").length,
256
+ cells.filter((cell) => "cap" in cell && cell.cap === "tail").length,
257
+ ];
258
+ /**
259
+ * [LAW:parse-dont-validate] The one place a seam is checked, so every rule
260
+ * the cell model states is refused at construction and the render reads cells
261
+ * that already hold them. A seam is often authored as data — a consumer's
262
+ * config — so the rules the types state are checked here too, and what comes
263
+ * back is a frozen copy the caller can no longer change.
264
+ */
265
+ function parseSeam(raw) {
266
+ const parseForm = (form) => {
267
+ const seam = parseCells(raw, form, "seam", asSeamCell(form, "seam"));
268
+ // A divider authored as the seam itself stays the seam, so a render can see
269
+ // it has no choice to make.
270
+ const divider = raw[form].divider === raw[form].seam ? seam : parseCells(raw, form, "divider", asSeamCell(form, "divider"));
271
+ const [seamAt, dividerAt] = [seamWidth(seam), seamWidth(divider)];
272
+ if (seamAt.some((n, i) => n !== dividerAt[i]))
273
+ throw new Error(`seam: ${form} divider is not the width of the seam under every caps`);
274
+ return Object.freeze({ seam, divider });
275
+ };
276
+ return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
277
+ }
278
+ /** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
279
+ function parseCaps(raw) {
280
+ const parseForm = (form) => Object.freeze({ lead: parseCells(raw, form, "lead", asCell(form, "lead")), tail: parseCells(raw, form, "tail", asCell(form, "tail")) });
281
+ return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
282
+ }
283
+ // One form of the seam joined with that form of the caps.
284
+ function joinForms(seam, caps) {
285
+ const splice = (cells) => cells.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell]));
286
+ const seamCells = splice(seam.seam);
287
+ return { seam: seamCells, divider: seam.divider === seam.seam ? seamCells : splice(seam.divider), lead: caps.lead, tail: caps.tail };
288
+ }
289
+ // Whether any of `cells` paints with `side`'s ground.
290
+ const readsGround = (cells, side) => cells.some((cell) => [cell.fg, cell.bg].some((color) => color !== "ground" && [...paintsIn(color)].some((p) => "edge" in p && p.edge === side && p.part === "bg")));
248
291
  function formOf(shape) {
249
- return { shape, reads: { left: readsGround(shape, "left"), right: readsGround(shape, "right") } };
292
+ const between = [...shape.seam, ...shape.divider];
293
+ return {
294
+ shape,
295
+ reads: {
296
+ seam: { left: readsGround(between, "left"), right: readsGround(between, "right") },
297
+ lead: readsGround(shape.lead, "right"),
298
+ tail: readsGround(shape.tail, "left"),
299
+ },
300
+ };
250
301
  }
251
302
  // A bg with a real colour to paint. `undefined` (no bg) and the terminal
252
303
  // DEFAULT colour (transparent — identical to the terminal background) are the
@@ -312,26 +363,29 @@ function cellStyle(cell, edges, options) {
312
363
  }
313
364
  const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
314
365
  /**
315
- * The one joiner: what it draws at each join is the `JoinShape` it is given,
316
- * so a new joiner is a value, never a class.
366
+ * The one joiner: what it draws is a `JoinSeam` between neighbours and a
367
+ * `JoinCaps` at the ends of each run, picked independently, so a new joiner is
368
+ * a pair of values, never a class.
317
369
  *
318
370
  * Which part a join draws is read off its two neighbours. A neighbour is
319
371
  * missing at the strip's ends, and counts as missing — an endpoint — when the
320
- * shape paints with its ground anywhere and it has none to paint. With both present the
321
- * join is the seam, or the divider where every seam cell's ink is drawn
322
- * indistinct from its ground at `options.colorSystem`; with only the right, the lead; with only
323
- * the left, the tail; with neither, nothing.
372
+ * seam paints with its ground anywhere and it has none to paint. With both
373
+ * present the join is the seam, or the divider where every seam cell's ink is
374
+ * drawn indistinct from its ground at `options.colorSystem`; with only the
375
+ * right, the lead; with only the left, the tail; with neither, nothing. A cap
376
+ * that paints with its neighbour's ground is drawn only beside a neighbour
377
+ * with one, so caps that need grounds never stop a seam that needs none.
324
378
  *
325
- * `ascii` is the shape an ASCII-only output draws instead, whole.
379
+ * An ASCII-only output draws the `ascii` form of both, whole.
326
380
  */
327
381
  export class CellJoiner {
328
- shape;
329
- ascii;
382
+ seam;
383
+ caps;
330
384
  _forms;
331
- constructor(shape, ascii) {
332
- this.shape = parseShape(shape, "glyph");
333
- this.ascii = parseShape(ascii, "ascii");
334
- this._forms = { glyph: formOf(this.shape), ascii: formOf(this.ascii) };
385
+ constructor(seam, caps) {
386
+ this.seam = parseSeam(seam);
387
+ this.caps = parseCaps(caps);
388
+ this._forms = { glyph: formOf(joinForms(this.seam.glyph, this.caps.glyph)), ascii: formOf(joinForms(this.seam.ascii, this.caps.ascii)) };
335
389
  }
336
390
  join(left, right, { index }) {
337
391
  const forms = this._forms;
@@ -341,8 +395,10 @@ export class CellJoiner {
341
395
  const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
342
396
  const leftBg = paintableBg(edges.left?.bgcolor);
343
397
  const rightBg = paintableBg(edges.right?.bgcolor);
344
- const hasLeft = left !== null && (!reads.left || leftBg !== undefined);
345
- const hasRight = right !== null && (!reads.right || rightBg !== undefined);
398
+ const hasLeft = left !== null && (!reads.seam.left || leftBg !== undefined);
399
+ const hasRight = right !== null && (!reads.seam.right || rightBg !== undefined);
400
+ const leads = hasRight && (!reads.lead || rightBg !== undefined);
401
+ const tails = hasLeft && (!reads.tail || leftBg !== undefined);
346
402
  const neighbours = { left, right };
347
403
  // On screen a cell's ink is its `fg` paint and its ground its `bg`,
348
404
  // reverse video or not (`cellStyle`).
@@ -352,8 +408,8 @@ export class CellJoiner {
352
408
  return new Segment(pickGlyph(cell, part, index, at), cellStyle(cell, edges, options).withAnchor({ owner: owners, row: 0, col: 0, inner: undefined }));
353
409
  });
354
410
  if (!hasLeft || !hasRight) {
355
- if (hasLeft || hasRight)
356
- yield* styled(hasRight ? "lead" : "tail");
411
+ if (leads || tails)
412
+ yield* styled(leads ? "lead" : "tail");
357
413
  return;
358
414
  }
359
415
  // A seam is lost only when every cell of it is: one cell rounded into
@@ -367,30 +423,34 @@ export class CellJoiner {
367
423
  };
368
424
  }
369
425
  }
370
- // --- The bundled joiners ---
426
+ // --- The bundled seams and caps ---
371
427
  const LEFT_BG = { edge: "left", part: "bg" };
372
428
  const RIGHT_BG = { edge: "right", part: "bg" };
373
429
  const LEFT_FG = { edge: "left", part: "fg" };
374
430
  const HALF = { mix: [LEFT_BG, RIGHT_BG, 1 / 2] };
375
431
  const GROUND = "ground";
376
- // A one-cell edge in the powerline family: `glyph` in the left ground over the
377
- // right; divided by `thin` in the left item's text; led by the same glyph with
378
- // the left ground replaced by the terminal's — in reverse video, a notch whose
379
- // ink is the terminal's background — and tailed by it over the terminal's.
380
- function edge(glyph, thin) {
381
- return {
382
- seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
383
- divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
384
- lead: [{ glyph, fg: GROUND, bg: RIGHT_BG }],
385
- tail: [{ glyph, fg: LEFT_BG, bg: GROUND }],
386
- };
387
- }
388
- // Three powerline arrows stepping through the two thirds between the grounds.
432
+ const LEAD = { cap: "lead" };
433
+ const TAIL = { cap: "tail" };
434
+ const both = (glyph, ascii) => ({ glyph, ascii });
435
+ const POWERLINE = { glyph: "\ue0b0", thin: "\ue0b1", ascii: ">", asciiThin: ">" };
436
+ const SLANT = { glyph: "\ue0bc", thin: "\ue0bd", ascii: "/", asciiThin: "/" };
437
+ const BACKSLANT = { glyph: "\ue0b8", thin: "\ue0b9", ascii: "\\", asciiThin: "\\" };
438
+ const FLAME = { glyph: "\ue0c0", thin: "\ue0c1", ascii: "}", asciiThin: "}" };
439
+ const PIXEL = { glyph: "\ue0c4", thin: "\ue0b1", ascii: "#", asciiThin: ">" };
440
+ const ICE = { glyph: "\ue0c8", thin: "\ue0b1", ascii: "]", asciiThin: ">" };
441
+ const BUBBLE = { glyph: "\ue0b4", thin: "\ue0b5", ascii: ")", asciiThin: ")" };
442
+ // `glyph` in the left ground over the right; divided by `thin` in the left
443
+ // item's text.
444
+ const arrow = (glyph, thin) => ({
445
+ seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
446
+ divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
447
+ });
448
+ const arrowSeam = (f) => both(arrow(f.glyph, f.thin), arrow(f.ascii, f.asciiThin));
449
+ // Three arrows stepping through the two thirds between the grounds.
389
450
  function prism(glyph, thin) {
390
451
  const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
391
452
  const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
392
453
  return {
393
- ...edge(glyph, thin),
394
454
  seam: [
395
455
  { glyph, fg: LEFT_BG, bg: third(1 / 3) },
396
456
  { glyph, fg: third(1 / 3), bg: third(2 / 3) },
@@ -399,63 +459,114 @@ function prism(glyph, thin) {
399
459
  divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
400
460
  };
401
461
  }
402
- // A powerline-family joiner: `shape` over `glyph`, its ASCII form the same
403
- // shape over `ascii`, beginning each run flat — an ASCII arrow reversed out of
404
- // the terminal's ground is a block of nothing.
405
- const arrows = (shape, glyph, thin, ascii, asciiThin) => new CellJoiner(shape(glyph, thin), { ...shape(ascii, asciiThin), lead: [] });
406
- // Each run closed by `close` and opened by `open`, both in its ground over the
407
- // terminal's, with `between` — cells on the terminal's ground — between runs.
408
- function pill(open, close, between) {
409
- const seam = [{ glyph: close, fg: LEFT_BG, bg: GROUND }, ...between, { glyph: open, fg: RIGHT_BG, bg: GROUND }];
410
- return { seam, divider: seam, lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }], tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }] };
462
+ // The runs parted: the caps close the one and open the other, `between` —
463
+ // cells on the terminal's ground — set between them.
464
+ function parted(between) {
465
+ const seam = [TAIL, ...between, LEAD];
466
+ return { seam, divider: seam };
411
467
  }
412
- const capsule = (open, close) => pill(open, close, [{ glyph: " ", fg: LEFT_FG, bg: GROUND }]);
413
- const metro = (open, close, rail) => pill(open, close, [{ glyph: rail, fg: { role: "strip.rail" }, bg: GROUND }]);
414
- // One cell drawn over the two grounds' midpoint, so it reads as neither item's;
415
- // nothing at the ends, where there is no second ground to meet.
468
+ // One cell drawn over the two grounds' midpoint, so it reads as neither item's.
416
469
  function midpoint(glyph, fg) {
417
470
  const seam = [{ glyph, fg, bg: HALF }];
418
- return { seam, divider: seam, lead: [], tail: [] };
471
+ return { seam, divider: seam };
419
472
  }
420
473
  // A ragged tear in the left ground over the midpoint; where that ink would
421
474
  // vanish into the midpoint — grounds a few steps apart, or one ground — the
422
475
  // same tear in the left item's text.
423
476
  function torn(glyphs) {
424
- return { ...midpoint({ oneOf: glyphs }, LEFT_BG), divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
477
+ return { seam: midpoint({ oneOf: glyphs }, LEFT_BG).seam, divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
425
478
  }
426
479
  const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
427
- const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM, lead: [], tail: [] };
480
+ const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM };
481
+ const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
482
+ const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
428
483
  /**
429
- * The bundled joiners, by name. What each draws is `docs/strip.md`'s
430
- * "Built-in joiners"; a name from config is read with `joinerNamed`.
484
+ * The bundled seams, by name: what a joiner draws between two neighbours.
485
+ * What each draws is `docs/strip.md`'s "Seams"; any of them ends in any of
486
+ * `CAPS`.
487
+ */
488
+ export const SEAMS = Object.freeze({
489
+ powerline: parseSeam(arrowSeam(POWERLINE)),
490
+ prism: parseSeam(both(prism(POWERLINE.glyph, POWERLINE.thin), prism(POWERLINE.ascii, POWERLINE.asciiThin))),
491
+ slant: parseSeam(arrowSeam(SLANT)),
492
+ backslant: parseSeam(arrowSeam(BACKSLANT)),
493
+ flame: parseSeam(arrowSeam(FLAME)),
494
+ pixel: parseSeam(arrowSeam(PIXEL)),
495
+ ice: parseSeam(arrowSeam(ICE)),
496
+ bubble: parseSeam(arrowSeam(BUBBLE)),
497
+ capsule: parseSeam(both(parted([SPACE]), parted([SPACE]))),
498
+ wrapper: parseSeam(both(parted([]), parted([]))),
499
+ metro: parseSeam(both(parted([rail("──")]), parted([rail("--")]))),
500
+ stitch: parseSeam(both(midpoint("┊", LEFT_FG), midpoint(":", LEFT_FG))),
501
+ torn: parseSeam(both(torn(["▚▞", "▞▚", "▘▗", "▝▖", "▖▝", "▗▘"]), torn(["/\\", "\\/"]))),
502
+ plain: parseSeam(both(PLAIN, PLAIN)),
503
+ });
504
+ // The family's glyph reversed out of the terminal's ground at the lead — a
505
+ // notch whose ink is the terminal's background — and over that ground at the
506
+ // tail, so a run reads as one chevron. ASCII opens each run flat: an ASCII
507
+ // glyph reversed out of the terminal's ground is a block of nothing.
508
+ const flowCaps = (f) => both({ lead: [{ glyph: f.glyph, fg: GROUND, bg: RIGHT_BG }], tail: [{ glyph: f.glyph, fg: LEFT_BG, bg: GROUND }] }, { lead: [], tail: [{ glyph: f.ascii, fg: LEFT_BG, bg: GROUND }] });
509
+ // `open` and `close`, each in its run's ground over the terminal's.
510
+ const pill = (open, close) => ({
511
+ lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }],
512
+ tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }],
513
+ });
514
+ const FLAT = { lead: [], tail: [] };
515
+ /**
516
+ * The bundled caps, by name: what a joiner draws where a run begins and ends.
517
+ * What each draws is `docs/strip.md`'s "Caps"; any of them ends any of
518
+ * `SEAMS`.
519
+ */
520
+ export const CAPS = Object.freeze({
521
+ powerline: parseCaps(flowCaps(POWERLINE)),
522
+ slant: parseCaps(flowCaps(SLANT)),
523
+ backslant: parseCaps(flowCaps(BACKSLANT)),
524
+ flame: parseCaps(flowCaps(FLAME)),
525
+ pixel: parseCaps(flowCaps(PIXEL)),
526
+ ice: parseCaps(flowCaps(ICE)),
527
+ bubble: parseCaps(flowCaps(BUBBLE)),
528
+ capsule: parseCaps(both(pill("\ue0b6", "\ue0b4"), pill("(", ")"))),
529
+ wrapper: parseCaps(both(pill("\ue0b2", "\ue0b0"), pill("<", ">"))),
530
+ flat: parseCaps(both(FLAT, FLAT)),
531
+ });
532
+ /**
533
+ * The bundled joiners, by name: each a seam from `SEAMS` ended in caps from
534
+ * `CAPS`. What each draws is `docs/strip.md`'s "Built-in joiners"; a name from
535
+ * config is read with `joinerNamed`.
431
536
  */
432
537
  export const JOINERS = Object.freeze({
433
- powerline: arrows(edge, "\ue0b0", "\ue0b1", ">", ">"),
434
- prism: arrows(prism, "\ue0b0", "\ue0b1", ">", ">"),
435
- slant: arrows(edge, "\ue0bc", "\ue0bd", "/", "/"),
436
- backslant: arrows(edge, "\ue0b8", "\ue0b9", "\\", "\\"),
437
- flame: arrows(edge, "\ue0c0", "\ue0c1", "}", "}"),
438
- pixel: arrows(edge, "\ue0c4", "\ue0b1", "#", ">"),
439
- ice: arrows(edge, "\ue0c8", "\ue0b1", "]", ">"),
440
- bubble: arrows(edge, "\ue0b4", "\ue0b5", ")", ")"),
441
- capsule: new CellJoiner(capsule("\ue0b6", "\ue0b4"), capsule("(", ")")),
442
- wrapper: new CellJoiner(pill("\ue0b2", "\ue0b0", []), pill("<", ">", [])),
443
- metro: new CellJoiner(metro("\ue0b6", "\ue0b4", "\u2500\u2500"), metro("(", ")", "--")),
444
- stitch: new CellJoiner(midpoint("\u250a", LEFT_FG), midpoint(":", LEFT_FG)),
445
- torn: new CellJoiner(torn(["\u259a\u259e", "\u259e\u259a", "\u2598\u2597", "\u259d\u2596", "\u2596\u259d", "\u2597\u2598"]), torn(["/\\", "\\/"])),
446
- plain: new CellJoiner(PLAIN, PLAIN),
538
+ powerline: new CellJoiner(SEAMS.powerline, CAPS.powerline),
539
+ prism: new CellJoiner(SEAMS.prism, CAPS.powerline),
540
+ slant: new CellJoiner(SEAMS.slant, CAPS.slant),
541
+ backslant: new CellJoiner(SEAMS.backslant, CAPS.backslant),
542
+ flame: new CellJoiner(SEAMS.flame, CAPS.flame),
543
+ pixel: new CellJoiner(SEAMS.pixel, CAPS.pixel),
544
+ ice: new CellJoiner(SEAMS.ice, CAPS.ice),
545
+ bubble: new CellJoiner(SEAMS.bubble, CAPS.bubble),
546
+ capsule: new CellJoiner(SEAMS.capsule, CAPS.capsule),
547
+ wrapper: new CellJoiner(SEAMS.wrapper, CAPS.wrapper),
548
+ metro: new CellJoiner(SEAMS.metro, CAPS.capsule),
549
+ stitch: new CellJoiner(SEAMS.stitch, CAPS.flat),
550
+ torn: new CellJoiner(SEAMS.torn, CAPS.flat),
551
+ plain: new CellJoiner(SEAMS.plain, CAPS.flat),
447
552
  });
448
553
  /**
449
- * The bundled joiner `name` names — a name read from config, parsed. Throws on
450
- * any other string, naming the ones there are; an own-key test, so `toString`
451
- * and the rest of `Object.prototype` are not joiners.
554
+ * The joiner `spec` names — a joiner read from config, parsed. Throws on a
555
+ * name its table lacks, naming the ones there are; an own-key test, so
556
+ * `toString` and the rest of `Object.prototype` are no one's name.
452
557
  */
453
- export function joinerNamed(name) {
454
- if (!isJoinerName(name))
455
- throw new Error(`no joiner named ${JSON.stringify(name)}; the joiners are ${Object.keys(JOINERS).join(", ")}`);
456
- return JOINERS[name];
558
+ export function joinerNamed(spec) {
559
+ if (typeof spec === "string")
560
+ return named(JOINERS, "joiner", "joiners", spec);
561
+ if (typeof spec !== "object" || spec === null)
562
+ throw new Error(`a joiner is a name or { seam, caps }, not ${JSON.stringify(spec)}`);
563
+ return new CellJoiner(named(SEAMS, "seam", "seams", spec.seam), named(CAPS, "caps", "caps", spec.caps));
564
+ }
565
+ function named(table, kind, kinds, name) {
566
+ if (!Object.hasOwn(table, name))
567
+ throw new Error(`no ${kind} named ${JSON.stringify(name)}; the ${kinds} are ${Object.keys(table).join(", ")}`);
568
+ return table[name];
457
569
  }
458
- const isJoinerName = (name) => Object.hasOwn(JOINERS, name);
459
570
  function gradient(steps, glyph) {
460
571
  // Midpoint sampling across `2 * steps` half-cell positions: cell i takes
461
572
  // samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
@@ -466,15 +577,15 @@ function gradient(steps, glyph) {
466
577
  fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
467
578
  bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
468
579
  }));
469
- return { seam, divider: seam, lead: [], tail: [] };
580
+ return { seam, divider: seam };
470
581
  }
471
582
  /**
472
583
  * A ramp of `steps` cells from the left ground to the right. Each cell is the
473
584
  * half block U+258C, ink on its left half and ground on its right, so it
474
- * carries two samples; ASCII draws a space, keeping the grounds alone. Nothing
475
- * at the ends, or beside a neighbour with no ground — there is no colour to
476
- * ramp toward.
585
+ * carries two samples; ASCII draws a space, keeping the grounds alone. Beside
586
+ * a neighbour with no ground there is no colour to ramp toward, so that join
587
+ * draws the caps the ramp is ended in.
477
588
  */
478
- export function gradientJoiner(steps = 4) {
479
- return new CellJoiner(gradient(steps, "▌"), gradient(steps, " "));
589
+ export function gradientSeam(steps = 4) {
590
+ return parseSeam(both(gradient(steps, "▌"), gradient(steps, " ")));
480
591
  }
@@ -38,6 +38,7 @@ export interface StyleOptions {
38
38
  link?: string;
39
39
  meta?: Record<string, unknown>;
40
40
  anchor?: Anchor;
41
+ nextChange?: number;
41
42
  }
42
43
  export declare class StyleSyntaxError extends Error {
43
44
  constructor(message: string, options?: ErrorOptions);
@@ -66,6 +67,18 @@ export declare class Style {
66
67
  * unequal, because they came from different places.
67
68
  */
68
69
  readonly anchor: Anchor | undefined;
70
+ /**
71
+ * When what this style's cells show next changes, in seconds on the
72
+ * timeline of whoever drew the frame — `t` itself for a cell drawn by
73
+ * something that moves every frame, and `Infinity`, the default, for one
74
+ * that never changes. It draws nothing. Whatever draws for a handed time
75
+ * stamps its cells with it (`Frames`, `Effected`), the stamp rides up
76
+ * through every container as the anchor does (`./anchor.ts`), and
77
+ * whoever holds the composed frame reads the soonest off it
78
+ * (`Segment.nextChange`), so a host knows when to draw again without
79
+ * asking each container. Two styles laid together change at the sooner.
80
+ */
81
+ readonly nextChange: number;
69
82
  constructor(options?: StyleOptions);
70
83
  get isNull(): boolean;
71
84
  get transparentBackground(): boolean;
@@ -115,7 +128,11 @@ export declare class Style {
115
128
  */
116
129
  toSgrCodes(colorSystem: ColorDepth): string;
117
130
  /**
118
- * Serializes to a canonical string that round-trips through `Style.parse()`.
131
+ * The look as a canonical string that round-trips through `Style.parse()`:
132
+ * colours, attributes and the link, which is all a style string can spell.
133
+ * What a style carries beyond its look — `meta`, `anchor`, `nextChange` —
134
+ * has no spelling and is not here, so two styles that differ only there
135
+ * read alike; `equals` tells them apart.
119
136
  */
120
137
  toString(): string;
121
138
  static null(): Style;