@promptctl/rich-js 0.22.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 +108 -31
  10. package/dist/core/strip.js +240 -131
  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 +91 -18
  25. package/dist/renderables/effect.js +158 -46
  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
@@ -59,16 +59,18 @@ class ColorOwners {
59
59
  }
60
60
  }
61
61
  /**
62
- * Which of a cell's two colours on screen are `owner`'s: both, for a cell
63
- * `owner` drew, itself or through an owner nested in its output; for a cell a
64
- * `CellJoiner` drew, the sides shown in `owner`'s colour.
62
+ * Which of a cell's two colours on screen are one of `owners`': both, for a
63
+ * cell one of them drew, itself or through an owner nested in its output; for
64
+ * a cell a `CellJoiner` drew, the sides shown in one of their colours.
65
65
  */
66
- export function colorsOwnedBy(anchor, owner) {
66
+ export function colorsOwnedBy(anchor, owners) {
67
67
  for (let level = anchor; level; level = level.inner) {
68
- if (level.owner === owner)
68
+ if (owners.has(level.owner))
69
69
  return { fg: true, bg: true };
70
- if (level.owner instanceof ColorOwners)
71
- return { fg: level.owner.fg === owner, bg: level.owner.bg === owner };
70
+ if (level.owner instanceof ColorOwners) {
71
+ const { fg, bg } = level.owner;
72
+ return { fg: fg !== null && owners.has(fg), bg: bg !== null && owners.has(bg) };
73
+ }
72
74
  }
73
75
  return { fg: false, bg: false };
74
76
  }
@@ -192,59 +194,106 @@ function parsePaint(raw, absent, refuse) {
192
194
  }
193
195
  const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
194
196
  const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
195
- function parseList(raw, where) {
196
- if (!Array.isArray(raw))
197
- throw new Error(`CellJoiner: ${where} is not a list of cells`);
198
- return raw.map((cell, i) => {
199
- if (typeof cell !== "object" || cell === null)
200
- throw new Error(`CellJoiner: ${where}[${i}] is not a cell`);
201
- return cell;
197
+ // Which argument of a joiner a part is authored in, so a refusal names it.
198
+ const AUTHORED_IN = { seam: "seam", divider: "seam", lead: "caps", tail: "caps" };
199
+ function parseCell(cell, form, part, refuse) {
200
+ const glyph = cell.glyph;
201
+ const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
202
+ if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
203
+ refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
204
+ }
205
+ if (cell.fg === "ground" && cell.bg === "ground")
206
+ refuse("has ground on both sides");
207
+ const glyphs = glyphsOf(cell);
208
+ if (glyphs.length === 0)
209
+ refuse("has an empty oneOf");
210
+ if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
211
+ refuse("has oneOf members of different widths");
212
+ if (form === "ascii" && !glyphs.every(isAscii))
213
+ refuse("is not ASCII");
214
+ return Object.freeze({
215
+ glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
216
+ fg: parseColor(cell.fg, ABSENT[part], refuse),
217
+ bg: parseColor(cell.bg, ABSENT[part], refuse),
202
218
  });
203
219
  }
204
- /**
205
- * [LAW:parse-dont-validate] The one place a shape is checked, so every rule
206
- * the cell model states is refused at construction and the render reads a
207
- * shape that already holds them. A shape is often authored as data — a
208
- * consumer's config — so the rules the types state are checked here too, and
209
- * what comes back is a frozen copy the caller can no longer change.
210
- */
211
- function parseShape(raw, form) {
212
- const parseCells = (part) => Object.freeze(parseList(raw[part], `${form} ${part}`).map((cell, i) => {
220
+ // `raw[form][part]` as frozen cells, each checked by `parseOne`, or refused
221
+ // naming where it sits.
222
+ function parseCells(raw, form, part, parseOne) {
223
+ const forms = raw;
224
+ const where = AUTHORED_IN[part];
225
+ if (typeof forms?.[form] !== "object" || forms[form] === null)
226
+ throw new Error(`${where}: has no ${form} form`);
227
+ const cells = forms[form][part];
228
+ if (!Array.isArray(cells))
229
+ throw new Error(`${where}: ${form} ${part} is not a list of cells`);
230
+ return Object.freeze(cells.map((cell, i) => {
213
231
  const refuse = (why) => {
214
- throw new Error(`CellJoiner: ${form} ${part}[${i}] ${why}`);
232
+ throw new Error(`${where}: ${form} ${part}[${i}] ${why}`);
215
233
  };
216
- const glyph = cell.glyph;
217
- const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
218
- if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
219
- refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
220
- }
221
- if (cell.fg === "ground" && cell.bg === "ground")
222
- refuse("has ground on both sides");
223
- const glyphs = glyphsOf(cell);
224
- if (glyphs.length === 0)
225
- refuse("has an empty oneOf");
226
- if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
227
- refuse("has oneOf members of different widths");
228
- if (form === "ascii" && !glyphs.every(isAscii))
229
- refuse("is not ASCII");
230
- return Object.freeze({
231
- glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
232
- fg: parseColor(cell.fg, ABSENT[part], refuse),
233
- bg: parseColor(cell.bg, ABSENT[part], refuse),
234
- });
234
+ return typeof cell === "object" && cell !== null ? parseOne(cell, refuse) : refuse("is not a cell");
235
235
  }));
236
- const seam = parseCells("seam");
237
- // A divider authored as the seam itself stays the seam, so a render can see
238
- // it has no choice to make.
239
- const divider = raw.divider === raw.seam ? seam : parseCells("divider");
240
- if (partWidth(divider) !== partWidth(seam))
241
- throw new Error(`CellJoiner: ${form} divider is not the width of the seam`);
242
- return Object.freeze({ seam, divider, lead: parseCells("lead"), tail: parseCells("tail") });
243
236
  }
244
- // Whether any cell of `shape` paints with `side`'s ground.
245
- 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")));
237
+ const asCell = (form, part) => (cell, refuse) => parseCell(cell, form, part, refuse);
238
+ const asSeamCell = (form, part) => (cell, refuse) => {
239
+ if (!("cap" in cell))
240
+ return parseCell(cell, form, part, refuse);
241
+ const { cap } = cell;
242
+ if (Object.keys(cell).length !== 1)
243
+ refuse("names a cap and draws cells of its own; a cell is one or the other");
244
+ return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
245
+ };
246
+ // A seam's width as a sum over whatever caps stand in it: its own cells', and
247
+ // how many times it names each cap. Two parts equal in all three are the same
248
+ // width under every caps.
249
+ const seamWidth = (cells) => [
250
+ partWidth(cells.filter((cell) => !("cap" in cell))),
251
+ cells.filter((cell) => "cap" in cell && cell.cap === "lead").length,
252
+ cells.filter((cell) => "cap" in cell && cell.cap === "tail").length,
253
+ ];
254
+ /**
255
+ * [LAW:parse-dont-validate] The one place a seam is checked, so every rule
256
+ * the cell model states is refused at construction and the render reads cells
257
+ * that already hold them. A seam is often authored as data — a consumer's
258
+ * config — so the rules the types state are checked here too, and what comes
259
+ * back is a frozen copy the caller can no longer change.
260
+ */
261
+ function parseSeam(raw) {
262
+ const parseForm = (form) => {
263
+ const seam = parseCells(raw, form, "seam", asSeamCell(form, "seam"));
264
+ // A divider authored as the seam itself stays the seam, so a render can see
265
+ // it has no choice to make.
266
+ const divider = raw[form].divider === raw[form].seam ? seam : parseCells(raw, form, "divider", asSeamCell(form, "divider"));
267
+ const [seamAt, dividerAt] = [seamWidth(seam), seamWidth(divider)];
268
+ if (seamAt.some((n, i) => n !== dividerAt[i]))
269
+ throw new Error(`seam: ${form} divider is not the width of the seam under every caps`);
270
+ return Object.freeze({ seam, divider });
271
+ };
272
+ return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
273
+ }
274
+ /** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
275
+ function parseCaps(raw) {
276
+ const parseForm = (form) => Object.freeze({ lead: parseCells(raw, form, "lead", asCell(form, "lead")), tail: parseCells(raw, form, "tail", asCell(form, "tail")) });
277
+ return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
278
+ }
279
+ // One form of the seam joined with that form of the caps.
280
+ function joinForms(seam, caps) {
281
+ const splice = (cells) => cells.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell]));
282
+ const seamCells = splice(seam.seam);
283
+ return { seam: seamCells, divider: seam.divider === seam.seam ? seamCells : splice(seam.divider), lead: caps.lead, tail: caps.tail };
284
+ }
285
+ // Whether any of `cells` paints with `side`'s ground.
286
+ 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")));
246
287
  function formOf(shape) {
247
- return { shape, reads: { left: readsGround(shape, "left"), right: readsGround(shape, "right") } };
288
+ const between = [...shape.seam, ...shape.divider];
289
+ return {
290
+ shape,
291
+ reads: {
292
+ seam: { left: readsGround(between, "left"), right: readsGround(between, "right") },
293
+ lead: readsGround(shape.lead, "right"),
294
+ tail: readsGround(shape.tail, "left"),
295
+ },
296
+ };
248
297
  }
249
298
  // A bg with a real colour to paint. `undefined` (no bg) and the terminal
250
299
  // DEFAULT colour (transparent — identical to the terminal background) are the
@@ -310,26 +359,29 @@ function cellStyle(cell, edges, options) {
310
359
  }
311
360
  const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
312
361
  /**
313
- * The one joiner: what it draws at each join is the `JoinShape` it is given,
314
- * so a new joiner is a value, never a class.
362
+ * The one joiner: what it draws is a `JoinSeam` between neighbours and a
363
+ * `JoinCaps` at the ends of each run, picked independently, so a new joiner is
364
+ * a pair of values, never a class.
315
365
  *
316
366
  * Which part a join draws is read off its two neighbours. A neighbour is
317
367
  * missing at the strip's ends, and counts as missing — an endpoint — when the
318
- * shape paints with its ground anywhere and it has none to paint. With both present the
319
- * join is the seam, or the divider where every seam cell's ink is drawn
320
- * indistinct from its ground at `options.colorSystem`; with only the right, the lead; with only
321
- * the left, the tail; with neither, nothing.
368
+ * seam paints with its ground anywhere and it has none to paint. With both
369
+ * present the join is the seam, or the divider where every seam cell's ink is
370
+ * drawn indistinct from its ground at `options.colorSystem`; with only the
371
+ * right, the lead; with only the left, the tail; with neither, nothing. A cap
372
+ * that paints with its neighbour's ground is drawn only beside a neighbour
373
+ * with one, so caps that need grounds never stop a seam that needs none.
322
374
  *
323
- * `ascii` is the shape an ASCII-only output draws instead, whole.
375
+ * An ASCII-only output draws the `ascii` form of both, whole.
324
376
  */
325
377
  export class CellJoiner {
326
- shape;
327
- ascii;
378
+ seam;
379
+ caps;
328
380
  _forms;
329
- constructor(shape, ascii) {
330
- this.shape = parseShape(shape, "glyph");
331
- this.ascii = parseShape(ascii, "ascii");
332
- this._forms = { glyph: formOf(this.shape), ascii: formOf(this.ascii) };
381
+ constructor(seam, caps) {
382
+ this.seam = parseSeam(seam);
383
+ this.caps = parseCaps(caps);
384
+ this._forms = { glyph: formOf(joinForms(this.seam.glyph, this.caps.glyph)), ascii: formOf(joinForms(this.seam.ascii, this.caps.ascii)) };
333
385
  }
334
386
  join(left, right, { index }) {
335
387
  const forms = this._forms;
@@ -339,8 +391,10 @@ export class CellJoiner {
339
391
  const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
340
392
  const leftBg = paintableBg(edges.left?.bgcolor);
341
393
  const rightBg = paintableBg(edges.right?.bgcolor);
342
- const hasLeft = left !== null && (!reads.left || leftBg !== undefined);
343
- const hasRight = right !== null && (!reads.right || rightBg !== undefined);
394
+ const hasLeft = left !== null && (!reads.seam.left || leftBg !== undefined);
395
+ const hasRight = right !== null && (!reads.seam.right || rightBg !== undefined);
396
+ const leads = hasRight && (!reads.lead || rightBg !== undefined);
397
+ const tails = hasLeft && (!reads.tail || leftBg !== undefined);
344
398
  const neighbours = { left, right };
345
399
  // On screen a cell's ink is its `fg` paint and its ground its `bg`,
346
400
  // reverse video or not (`cellStyle`).
@@ -350,8 +404,8 @@ export class CellJoiner {
350
404
  return new Segment(pickGlyph(cell, part, index, at), cellStyle(cell, edges, options).withAnchor({ owner: owners, row: 0, col: 0, inner: undefined }));
351
405
  });
352
406
  if (!hasLeft || !hasRight) {
353
- if (hasLeft || hasRight)
354
- yield* styled(hasRight ? "lead" : "tail");
407
+ if (leads || tails)
408
+ yield* styled(leads ? "lead" : "tail");
355
409
  return;
356
410
  }
357
411
  // A seam is lost only when every cell of it is: one cell rounded into
@@ -365,30 +419,34 @@ export class CellJoiner {
365
419
  };
366
420
  }
367
421
  }
368
- // --- The bundled joiners ---
422
+ // --- The bundled seams and caps ---
369
423
  const LEFT_BG = { edge: "left", part: "bg" };
370
424
  const RIGHT_BG = { edge: "right", part: "bg" };
371
425
  const LEFT_FG = { edge: "left", part: "fg" };
372
426
  const HALF = { mix: [LEFT_BG, RIGHT_BG, 1 / 2] };
373
427
  const GROUND = "ground";
374
- // A one-cell edge in the powerline family: `glyph` in the left ground over the
375
- // right; divided by `thin` in the left item's text; led by the same glyph with
376
- // the left ground replaced by the terminal's — in reverse video, a notch whose
377
- // ink is the terminal's background — and tailed by it over the terminal's.
378
- function edge(glyph, thin) {
379
- return {
380
- seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
381
- divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
382
- lead: [{ glyph, fg: GROUND, bg: RIGHT_BG }],
383
- tail: [{ glyph, fg: LEFT_BG, bg: GROUND }],
384
- };
385
- }
386
- // Three powerline arrows stepping through the two thirds between the grounds.
428
+ const LEAD = { cap: "lead" };
429
+ const TAIL = { cap: "tail" };
430
+ const both = (glyph, ascii) => ({ glyph, ascii });
431
+ const POWERLINE = { glyph: "\ue0b0", thin: "\ue0b1", ascii: ">", asciiThin: ">" };
432
+ const SLANT = { glyph: "\ue0bc", thin: "\ue0bd", ascii: "/", asciiThin: "/" };
433
+ const BACKSLANT = { glyph: "\ue0b8", thin: "\ue0b9", ascii: "\\", asciiThin: "\\" };
434
+ const FLAME = { glyph: "\ue0c0", thin: "\ue0c1", ascii: "}", asciiThin: "}" };
435
+ const PIXEL = { glyph: "\ue0c4", thin: "\ue0b1", ascii: "#", asciiThin: ">" };
436
+ const ICE = { glyph: "\ue0c8", thin: "\ue0b1", ascii: "]", asciiThin: ">" };
437
+ const BUBBLE = { glyph: "\ue0b4", thin: "\ue0b5", ascii: ")", asciiThin: ")" };
438
+ // `glyph` in the left ground over the right; divided by `thin` in the left
439
+ // item's text.
440
+ const arrow = (glyph, thin) => ({
441
+ seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
442
+ divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
443
+ });
444
+ const arrowSeam = (f) => both(arrow(f.glyph, f.thin), arrow(f.ascii, f.asciiThin));
445
+ // Three arrows stepping through the two thirds between the grounds.
387
446
  function prism(glyph, thin) {
388
447
  const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
389
448
  const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
390
449
  return {
391
- ...edge(glyph, thin),
392
450
  seam: [
393
451
  { glyph, fg: LEFT_BG, bg: third(1 / 3) },
394
452
  { glyph, fg: third(1 / 3), bg: third(2 / 3) },
@@ -397,63 +455,114 @@ function prism(glyph, thin) {
397
455
  divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
398
456
  };
399
457
  }
400
- // A powerline-family joiner: `shape` over `glyph`, its ASCII form the same
401
- // shape over `ascii`, beginning each run flat — an ASCII arrow reversed out of
402
- // the terminal's ground is a block of nothing.
403
- const arrows = (shape, glyph, thin, ascii, asciiThin) => new CellJoiner(shape(glyph, thin), { ...shape(ascii, asciiThin), lead: [] });
404
- // Each run closed by `close` and opened by `open`, both in its ground over the
405
- // terminal's, with `between` — cells on the terminal's ground — between runs.
406
- function pill(open, close, between) {
407
- const seam = [{ glyph: close, fg: LEFT_BG, bg: GROUND }, ...between, { glyph: open, fg: RIGHT_BG, bg: GROUND }];
408
- return { seam, divider: seam, lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }], tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }] };
458
+ // The runs parted: the caps close the one and open the other, `between` —
459
+ // cells on the terminal's ground — set between them.
460
+ function parted(between) {
461
+ const seam = [TAIL, ...between, LEAD];
462
+ return { seam, divider: seam };
409
463
  }
410
- const capsule = (open, close) => pill(open, close, [{ glyph: " ", fg: LEFT_FG, bg: GROUND }]);
411
- const metro = (open, close, rail) => pill(open, close, [{ glyph: rail, fg: { role: "strip.rail" }, bg: GROUND }]);
412
- // One cell drawn over the two grounds' midpoint, so it reads as neither item's;
413
- // nothing at the ends, where there is no second ground to meet.
464
+ // One cell drawn over the two grounds' midpoint, so it reads as neither item's.
414
465
  function midpoint(glyph, fg) {
415
466
  const seam = [{ glyph, fg, bg: HALF }];
416
- return { seam, divider: seam, lead: [], tail: [] };
467
+ return { seam, divider: seam };
417
468
  }
418
469
  // A ragged tear in the left ground over the midpoint; where that ink would
419
470
  // vanish into the midpoint — grounds a few steps apart, or one ground — the
420
471
  // same tear in the left item's text.
421
472
  function torn(glyphs) {
422
- return { ...midpoint({ oneOf: glyphs }, LEFT_BG), divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
473
+ return { seam: midpoint({ oneOf: glyphs }, LEFT_BG).seam, divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
423
474
  }
424
475
  const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
425
- const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM, lead: [], tail: [] };
476
+ const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM };
477
+ const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
478
+ const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
426
479
  /**
427
- * The bundled joiners, by name. What each draws is `docs/strip.md`'s
428
- * "Built-in joiners"; a name from config is read with `joinerNamed`.
480
+ * The bundled seams, by name: what a joiner draws between two neighbours.
481
+ * What each draws is `docs/strip.md`'s "Seams"; any of them ends in any of
482
+ * `CAPS`.
483
+ */
484
+ export const SEAMS = Object.freeze({
485
+ powerline: parseSeam(arrowSeam(POWERLINE)),
486
+ prism: parseSeam(both(prism(POWERLINE.glyph, POWERLINE.thin), prism(POWERLINE.ascii, POWERLINE.asciiThin))),
487
+ slant: parseSeam(arrowSeam(SLANT)),
488
+ backslant: parseSeam(arrowSeam(BACKSLANT)),
489
+ flame: parseSeam(arrowSeam(FLAME)),
490
+ pixel: parseSeam(arrowSeam(PIXEL)),
491
+ ice: parseSeam(arrowSeam(ICE)),
492
+ bubble: parseSeam(arrowSeam(BUBBLE)),
493
+ capsule: parseSeam(both(parted([SPACE]), parted([SPACE]))),
494
+ wrapper: parseSeam(both(parted([]), parted([]))),
495
+ metro: parseSeam(both(parted([rail("──")]), parted([rail("--")]))),
496
+ stitch: parseSeam(both(midpoint("┊", LEFT_FG), midpoint(":", LEFT_FG))),
497
+ torn: parseSeam(both(torn(["▚▞", "▞▚", "▘▗", "▝▖", "▖▝", "▗▘"]), torn(["/\\", "\\/"]))),
498
+ plain: parseSeam(both(PLAIN, PLAIN)),
499
+ });
500
+ // The family's glyph reversed out of the terminal's ground at the lead — a
501
+ // notch whose ink is the terminal's background — and over that ground at the
502
+ // tail, so a run reads as one chevron. ASCII opens each run flat: an ASCII
503
+ // glyph reversed out of the terminal's ground is a block of nothing.
504
+ 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 }] });
505
+ // `open` and `close`, each in its run's ground over the terminal's.
506
+ const pill = (open, close) => ({
507
+ lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }],
508
+ tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }],
509
+ });
510
+ const FLAT = { lead: [], tail: [] };
511
+ /**
512
+ * The bundled caps, by name: what a joiner draws where a run begins and ends.
513
+ * What each draws is `docs/strip.md`'s "Caps"; any of them ends any of
514
+ * `SEAMS`.
515
+ */
516
+ export const CAPS = Object.freeze({
517
+ powerline: parseCaps(flowCaps(POWERLINE)),
518
+ slant: parseCaps(flowCaps(SLANT)),
519
+ backslant: parseCaps(flowCaps(BACKSLANT)),
520
+ flame: parseCaps(flowCaps(FLAME)),
521
+ pixel: parseCaps(flowCaps(PIXEL)),
522
+ ice: parseCaps(flowCaps(ICE)),
523
+ bubble: parseCaps(flowCaps(BUBBLE)),
524
+ capsule: parseCaps(both(pill("\ue0b6", "\ue0b4"), pill("(", ")"))),
525
+ wrapper: parseCaps(both(pill("\ue0b2", "\ue0b0"), pill("<", ">"))),
526
+ flat: parseCaps(both(FLAT, FLAT)),
527
+ });
528
+ /**
529
+ * The bundled joiners, by name: each a seam from `SEAMS` ended in caps from
530
+ * `CAPS`. What each draws is `docs/strip.md`'s "Built-in joiners"; a name from
531
+ * config is read with `joinerNamed`.
429
532
  */
430
533
  export const JOINERS = Object.freeze({
431
- powerline: arrows(edge, "\ue0b0", "\ue0b1", ">", ">"),
432
- prism: arrows(prism, "\ue0b0", "\ue0b1", ">", ">"),
433
- slant: arrows(edge, "\ue0bc", "\ue0bd", "/", "/"),
434
- backslant: arrows(edge, "\ue0b8", "\ue0b9", "\\", "\\"),
435
- flame: arrows(edge, "\ue0c0", "\ue0c1", "}", "}"),
436
- pixel: arrows(edge, "\ue0c4", "\ue0b1", "#", ">"),
437
- ice: arrows(edge, "\ue0c8", "\ue0b1", "]", ">"),
438
- bubble: arrows(edge, "\ue0b4", "\ue0b5", ")", ")"),
439
- capsule: new CellJoiner(capsule("\ue0b6", "\ue0b4"), capsule("(", ")")),
440
- wrapper: new CellJoiner(pill("\ue0b2", "\ue0b0", []), pill("<", ">", [])),
441
- metro: new CellJoiner(metro("\ue0b6", "\ue0b4", "\u2500\u2500"), metro("(", ")", "--")),
442
- stitch: new CellJoiner(midpoint("\u250a", LEFT_FG), midpoint(":", LEFT_FG)),
443
- torn: new CellJoiner(torn(["\u259a\u259e", "\u259e\u259a", "\u2598\u2597", "\u259d\u2596", "\u2596\u259d", "\u2597\u2598"]), torn(["/\\", "\\/"])),
444
- plain: new CellJoiner(PLAIN, PLAIN),
534
+ powerline: new CellJoiner(SEAMS.powerline, CAPS.powerline),
535
+ prism: new CellJoiner(SEAMS.prism, CAPS.powerline),
536
+ slant: new CellJoiner(SEAMS.slant, CAPS.slant),
537
+ backslant: new CellJoiner(SEAMS.backslant, CAPS.backslant),
538
+ flame: new CellJoiner(SEAMS.flame, CAPS.flame),
539
+ pixel: new CellJoiner(SEAMS.pixel, CAPS.pixel),
540
+ ice: new CellJoiner(SEAMS.ice, CAPS.ice),
541
+ bubble: new CellJoiner(SEAMS.bubble, CAPS.bubble),
542
+ capsule: new CellJoiner(SEAMS.capsule, CAPS.capsule),
543
+ wrapper: new CellJoiner(SEAMS.wrapper, CAPS.wrapper),
544
+ metro: new CellJoiner(SEAMS.metro, CAPS.capsule),
545
+ stitch: new CellJoiner(SEAMS.stitch, CAPS.flat),
546
+ torn: new CellJoiner(SEAMS.torn, CAPS.flat),
547
+ plain: new CellJoiner(SEAMS.plain, CAPS.flat),
445
548
  });
446
549
  /**
447
- * The bundled joiner `name` names — a name read from config, parsed. Throws on
448
- * any other string, naming the ones there are; an own-key test, so `toString`
449
- * and the rest of `Object.prototype` are not joiners.
550
+ * The joiner `spec` names — a joiner read from config, parsed. Throws on a
551
+ * name its table lacks, naming the ones there are; an own-key test, so
552
+ * `toString` and the rest of `Object.prototype` are no one's name.
450
553
  */
451
- export function joinerNamed(name) {
452
- if (!isJoinerName(name))
453
- throw new Error(`no joiner named ${JSON.stringify(name)}; the joiners are ${Object.keys(JOINERS).join(", ")}`);
454
- return JOINERS[name];
554
+ export function joinerNamed(spec) {
555
+ if (typeof spec === "string")
556
+ return named(JOINERS, "joiner", "joiners", spec);
557
+ if (typeof spec !== "object" || spec === null)
558
+ throw new Error(`a joiner is a name or { seam, caps }, not ${JSON.stringify(spec)}`);
559
+ return new CellJoiner(named(SEAMS, "seam", "seams", spec.seam), named(CAPS, "caps", "caps", spec.caps));
560
+ }
561
+ function named(table, kind, kinds, name) {
562
+ if (!Object.hasOwn(table, name))
563
+ throw new Error(`no ${kind} named ${JSON.stringify(name)}; the ${kinds} are ${Object.keys(table).join(", ")}`);
564
+ return table[name];
455
565
  }
456
- const isJoinerName = (name) => Object.hasOwn(JOINERS, name);
457
566
  function gradient(steps, glyph) {
458
567
  // Midpoint sampling across `2 * steps` half-cell positions: cell i takes
459
568
  // samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
@@ -464,15 +573,15 @@ function gradient(steps, glyph) {
464
573
  fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
465
574
  bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
466
575
  }));
467
- return { seam, divider: seam, lead: [], tail: [] };
576
+ return { seam, divider: seam };
468
577
  }
469
578
  /**
470
579
  * A ramp of `steps` cells from the left ground to the right. Each cell is the
471
580
  * half block U+258C, ink on its left half and ground on its right, so it
472
- * carries two samples; ASCII draws a space, keeping the grounds alone. Nothing
473
- * at the ends, or beside a neighbour with no ground — there is no colour to
474
- * ramp toward.
581
+ * carries two samples; ASCII draws a space, keeping the grounds alone. Beside
582
+ * a neighbour with no ground there is no colour to ramp toward, so that join
583
+ * draws the caps the ramp is ended in.
475
584
  */
476
- export function gradientJoiner(steps = 4) {
477
- return new CellJoiner(gradient(steps, "▌"), gradient(steps, " "));
585
+ export function gradientSeam(steps = 4) {
586
+ return parseSeam(both(gradient(steps, "▌"), gradient(steps, " ")));
478
587
  }
@@ -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;
@@ -94,7 +94,26 @@ export class Style {
94
94
  * unequal, because they came from different places.
95
95
  */
96
96
  anchor;
97
+ /**
98
+ * When what this style's cells show next changes, in seconds on the
99
+ * timeline of whoever drew the frame — `t` itself for a cell drawn by
100
+ * something that moves every frame, and `Infinity`, the default, for one
101
+ * that never changes. It draws nothing. Whatever draws for a handed time
102
+ * stamps its cells with it (`Frames`, `Effected`), the stamp rides up
103
+ * through every container as the anchor does (`./anchor.ts`), and
104
+ * whoever holds the composed frame reads the soonest off it
105
+ * (`Segment.nextChange`), so a host knows when to draw again without
106
+ * asking each container. Two styles laid together change at the sooner.
107
+ */
108
+ nextChange;
97
109
  constructor(options = {}) {
110
+ const nextChange = options.nextChange ?? Number.POSITIVE_INFINITY;
111
+ // [LAW:parse-dont-validate] A time or never, settled where the style is
112
+ // made: NaN and -Infinity fail here, so no reader of a frame meets one.
113
+ if (!(nextChange > Number.NEGATIVE_INFINITY)) {
114
+ throw new RangeError(`Style nextChange must be a time in seconds or Infinity, got ${nextChange}`);
115
+ }
116
+ this.nextChange = nextChange;
98
117
  this.color = resolveColor(options.color);
99
118
  this.bgcolor = resolveBackground(options.bgcolor);
100
119
  this.bold = options.bold;
@@ -132,7 +151,8 @@ export class Style {
132
151
  this.overline === undefined &&
133
152
  this.link === undefined &&
134
153
  this.meta === undefined &&
135
- this.anchor === undefined);
154
+ this.anchor === undefined &&
155
+ this.nextChange === Number.POSITIVE_INFINITY);
136
156
  }
137
157
  get transparentBackground() {
138
158
  return this.bgcolor === undefined || this.bgcolor.isDefault;
@@ -204,6 +224,8 @@ export class Style {
204
224
  ? { ...this.meta, ...other.meta }
205
225
  : other.meta ?? this.meta,
206
226
  anchor: other.anchor ?? this.anchor,
227
+ // Cells under both change when either does: the sooner.
228
+ nextChange: Math.min(this.nextChange, other.nextChange),
207
229
  });
208
230
  }
209
231
  equals(other) {
@@ -225,7 +247,8 @@ export class Style {
225
247
  this.encircle === other.encircle &&
226
248
  this.overline === other.overline &&
227
249
  this.link === other.link &&
228
- anchorsEqual(this.anchor, other.anchor));
250
+ anchorsEqual(this.anchor, other.anchor) &&
251
+ this.nextChange === other.nextChange);
229
252
  }
230
253
  /**
231
254
  * The colours this style puts on screen at `colorSystem`: each flattened
@@ -273,7 +296,11 @@ export class Style {
273
296
  ].join(";");
274
297
  }
275
298
  /**
276
- * Serializes to a canonical string that round-trips through `Style.parse()`.
299
+ * The look as a canonical string that round-trips through `Style.parse()`:
300
+ * colours, attributes and the link, which is all a style string can spell.
301
+ * What a style carries beyond its look — `meta`, `anchor`, `nextChange` —
302
+ * has no spelling and is not here, so two styles that differ only there
303
+ * read alike; `equals` tells them apart.
277
304
  */
278
305
  toString() {
279
306
  if (this.isNull)
@@ -678,6 +705,8 @@ export const DEFAULT_STYLES = {
678
705
  "markdown.hr": Style.parse("yellow"),
679
706
  "markdown.link": Style.parse("bright_blue"),
680
707
  "markdown.link_url": Style.parse("blue"),
708
+ "markdown.table.border": Style.parse("cyan"),
709
+ "markdown.table.header": Style.parse("not bold cyan"),
681
710
  // Progress
682
711
  "progress.description": NULL_STYLE,
683
712
  "progress.percentage": Style.parse("magenta"),
package/dist/core/text.js CHANGED
@@ -73,7 +73,10 @@ function sanitizeStyleLink(style) {
73
73
  * [LAW:single-enforcer] Nor does a RichText keep an anchor (`./anchor.ts`). It
74
74
  * lays its text out anew, so a stamp naming where a cell sat in the output it
75
75
  * came from would name the wrong cell once it wraps, slices or overlaps; the
76
- * owner that renders the RichText is the one that stamps its cells.
76
+ * owner that renders the RichText is the one that stamps its cells. A
77
+ * `nextChange` stays: when a span's cells next change is true of them
78
+ * wherever the text lays them, so a frame appended into a line still says
79
+ * when it turns.
77
80
  */
78
81
  function admitStyle(style) {
79
82
  if (typeof style === "string")
package/dist/host/app.js CHANGED
@@ -91,13 +91,11 @@ export class App {
91
91
  mouseHandlers = new Set();
92
92
  wheelHandlers = new Set();
93
93
  quitKeys;
94
- input = new InputParser({
95
- key: (press) => this.key(press),
96
- mouse: (event) => this.mouse(event),
97
- });
94
+ input;
98
95
  settle = () => { };
99
96
  constructor(options) {
100
97
  this.host = options.host;
98
+ this.input = new InputParser({ key: (press) => this.key(press), mouse: (event) => this.mouse(event) }, options.host.inputChunks);
101
99
  this.surface = options.surface;
102
100
  this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
103
101
  this.view = options.view;
@@ -1,5 +1,5 @@
1
1
  export { BrowserTerminalHost } from "./terminal-host.js";
2
- export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
2
+ export type { TerminalHost, TerminalSize, DataHandler, InputChunks, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
3
3
  export { hostEnvironment } from "./host-environment.js";
4
4
  export { App } from "./app.js";
5
5
  export type { AppMouseEvent, AppOptions, AppPhase, Wheel } from "./app.js";