@promptctl/rich-js 0.24.1 → 0.25.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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +24 -33
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +98 -29
  47. package/dist/core/strip.js +211 -150
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +158 -63
  73. package/dist/renderables/effect.js +378 -155
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -39,41 +39,11 @@ import { drawable, getStyle, isAscii, render } from "./protocol.js";
39
39
  * not draw at all. An effect reads it to leave a seam out of the words it
40
40
  * keeps legible (`drawnOf`, `../renderables/effect.ts`).
41
41
  *
42
- * Inside it, every cell a `CellJoiner` draws names whose colour each of its
43
- * two sides shows (`colorsOwnedBy`). One seam cell can show two neighbours —
44
- * a powerline arrow is inked in the fill it points out of, over the fill it
45
- * points into — so a cell has no single owner; each side has one.
42
+ * Inside it, every cell a `CellJoiner` draws is stamped with what it was
43
+ * resolved from (`JoinedCell`), so a cell whose neighbours an effect moves is
44
+ * resolved again from where they moved to.
46
45
  */
47
46
  export const JOIN = Object.freeze({ name: "join" });
48
- /**
49
- * Whose colour each side of a join cell shows on screen, its ink and its
50
- * ground: the neighbour whose edge a `Paint` names, or `null` for a role, a
51
- * mix or the terminal's ground, which are no neighbour's.
52
- */
53
- class ColorOwners {
54
- fg;
55
- bg;
56
- constructor(fg, bg) {
57
- this.fg = fg;
58
- this.bg = bg;
59
- }
60
- }
61
- /**
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
- */
66
- export function colorsOwnedBy(anchor, owners) {
67
- for (let level = anchor; level; level = level.inner) {
68
- if (owners.has(level.owner))
69
- return { fg: true, bg: true };
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
- }
74
- }
75
- return { fg: false, bg: false };
76
- }
77
47
  /**
78
48
  * [LAW:single-enforcer] What `joiner` draws at one join, every cell stamped
79
49
  * as `JOIN`'s: the one way `Strip` and `FlexStrip` draw a join.
@@ -131,10 +101,6 @@ export class Strip {
131
101
  // of which renderables end their own line and which are fragments.
132
102
  yield Segment.line();
133
103
  }
134
- // The items; a join is drawn from what the joiner emits for it, not held.
135
- contents() {
136
- return this.items;
137
- }
138
104
  /**
139
105
  * [LAW:one-source-of-truth] The width of the one row it draws, and that is
140
106
  * both bounds: the row never wraps and no item is offered less than the whole
@@ -149,12 +115,12 @@ export class Strip {
149
115
  return { minimum: width, maximum: width };
150
116
  }
151
117
  }
152
- const PARTS = ["seam", "divider", "lead", "tail"];
118
+ const PARTS = ["seam", "lead", "tail"];
153
119
  /**
154
- * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
155
- * seam to be seen. Below it the eye cannot tell the ink from the ground,
156
- * so the joiner draws the divider instead. Twice the ~.02 threshold of a visible difference, because a seam is
157
- * one cell wide.
120
+ * The least ΔE_OK a join cell's ink must stand off its own ground by to be
121
+ * seen. Below it the eye cannot tell the ink from the ground, so the joiner
122
+ * lifts the ink to it (`standOff`). Twice the ~.02 threshold of a visible
123
+ * difference, because a seam is one cell wide.
158
124
  */
159
125
  export const SEAM_MIN_DELTA_E = 0.04;
160
126
  function* paintsIn(paint) {
@@ -174,7 +140,6 @@ const pickGlyph = (cell, part, index, at) => {
174
140
  return glyphs[Math.floor((parseInt(fnv1a(`${part}:${index}:${at}`), 16) / 2 ** 32) * glyphs.length)];
175
141
  };
176
142
  const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
177
- const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
178
143
  // `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
179
144
  // name, because the part is also drawn where that neighbour does not exist.
180
145
  function parsePaint(raw, absent, refuse) {
@@ -197,9 +162,9 @@ function parsePaint(raw, absent, refuse) {
197
162
  return refuse(`has a colour that is not a Paint: ${JSON.stringify(raw)}`);
198
163
  }
199
164
  const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
200
- const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
165
+ const ABSENT = { seam: undefined, lead: "left", tail: "right" };
201
166
  // 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" };
167
+ const AUTHORED_IN = { seam: "seam", lead: "caps", tail: "caps" };
203
168
  function parseCell(cell, form, part, refuse) {
204
169
  const glyph = cell.glyph;
205
170
  const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
@@ -247,14 +212,6 @@ const asSeamCell = (form, part) => (cell, refuse) => {
247
212
  refuse("names a cap and draws cells of its own; a cell is one or the other");
248
213
  return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
249
214
  };
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
215
  /**
259
216
  * [LAW:parse-dont-validate] The one place a seam is checked, so every rule
260
217
  * the cell model states is refused at construction and the render reads cells
@@ -263,16 +220,7 @@ const seamWidth = (cells) => [
263
220
  * back is a frozen copy the caller can no longer change.
264
221
  */
265
222
  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
- };
223
+ const parseForm = (form) => Object.freeze({ seam: parseCells(raw, form, "seam", asSeamCell(form, "seam")) });
276
224
  return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
277
225
  }
278
226
  /** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
@@ -282,21 +230,22 @@ function parseCaps(raw) {
282
230
  }
283
231
  // One form of the seam joined with that form of the caps.
284
232
  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 };
233
+ return { seam: seam.seam.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell])), lead: caps.lead, tail: caps.tail };
288
234
  }
235
+ // The parts of `side`'s edge `cell` paints with: its ground (`bg`), its text (`fg`).
236
+ const partsRead = (cell, side) => new Set([cell.fg, cell.bg].flatMap((color) => (color === "ground" ? [] : [...paintsIn(color)].flatMap((p) => ("edge" in p && p.edge === side ? [p.part] : [])))));
289
237
  // Whether any of `cells` paints with `side`'s ground.
290
238
  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")));
291
239
  function formOf(shape) {
292
- const between = [...shape.seam, ...shape.divider];
240
+ const cells = (part) => shape[part].map((cell) => ({ cell, parts: { left: partsRead(cell, "left"), right: partsRead(cell, "right") } }));
293
241
  return {
294
242
  shape,
295
243
  reads: {
296
- seam: { left: readsGround(between, "left"), right: readsGround(between, "right") },
244
+ seam: { left: readsGround(shape.seam, "left"), right: readsGround(shape.seam, "right") },
297
245
  lead: readsGround(shape.lead, "right"),
298
246
  tail: readsGround(shape.tail, "left"),
299
247
  },
248
+ cells: { seam: cells("seam"), lead: cells("lead"), tail: cells("tail") },
300
249
  };
301
250
  }
302
251
  // A bg with a real colour to paint. `undefined` (no bg) and the terminal
@@ -312,29 +261,82 @@ function paintableBg(bg) {
312
261
  function drawnGround(bg) {
313
262
  return bg.flattenAlpha(SURFACE_BLACK);
314
263
  }
315
- // A cell whose ink the eye cannot tell from its ground, read off the style it
316
- // is drawn in. What is measured is what is drawn — `Style.drawnColors`, the
317
- // colours the writer encodes: the ink flattened onto the cell's own ground and
318
- // the ground onto the terminal's black, both downgraded to the depth the
319
- // render encodes at, so two colours that depth rounds to one entry are the one
320
- // entry they render as. A colour whose RGB is the terminal theme's own (ANSI
321
- // 0–15, a blend reaching one) has no value here, so two of them are the same
322
- // only when they are one expression: the same palette slot — its `number`,
323
- // never the name or the depth that spells it ("red", "color(1)", an EIGHT_BIT
324
- // spec on 0–15 are one slot) — or the same blend, whose name spells each end
325
- // by its slot for the same reason. A cell on the terminal's ground, or in
326
- // reverse video over it, has a colour nobody knows on one side, and is seen.
327
- function vanishes(style, colorSystem) {
328
- if (style.reverse)
329
- return false;
330
- const { color, bgcolor } = style.drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
331
- if (color === undefined || bgcolor === undefined)
332
- return false;
333
- const av = color.fixedValue;
334
- const bv = bgcolor.fixedValue;
335
- return av !== undefined && bv !== undefined
336
- ? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
337
- : color.number !== undefined ? color.number === bgcolor.number : color.name === bgcolor.name;
264
+ // Whether ink stands off its ground, both as a depth draws them, by the floor a
265
+ // seam is seen at. A colour whose RGB is the terminal theme's own (ANSI 0–15, a
266
+ // blend reaching one) has no value here, so two of them stand apart unless they
267
+ // are one expression: the same palette slot — its `number`, never the name or
268
+ // the depth that spells it ("red", "color(1)", an EIGHT_BIT spec on 0–15 are
269
+ // one slot) — or the same blend, whose name spells each end by its slot for the
270
+ // same reason. At sixteen colours, then, the floor is a different slot. The
271
+ // terminal's default colour is nobody's to measure, and stands apart.
272
+ function standsOff(ink, ground) {
273
+ if (ink.isDefault || ground.isDefault)
274
+ return true;
275
+ const [a, b] = [ink.fixedValue, ground.fixedValue];
276
+ return a !== undefined && b !== undefined
277
+ ? Oklch.fromRgba(a).deltaE(Oklch.fromRgba(b)) >= SEAM_MIN_DELTA_E
278
+ : ink.number !== undefined ? ink.number !== ground.number : ink.name !== ground.name;
279
+ }
280
+ // A step along a lightness ray: finer than the steps between the colours a
281
+ // truecolor writer draws, so the walk passes over none of them.
282
+ const LIFT_STEP = 0.001;
283
+ // The steps along `origin`'s lightness ray, its chroma and hue held, in the
284
+ // order the lift tries them: the smaller move first, and at an equal move the
285
+ // lighter, out to black and to white.
286
+ function* lightnessRay(origin) {
287
+ for (let k = 1; k * LIFT_STEP <= 1; k++) {
288
+ for (const l of [origin.l + k * LIFT_STEP, origin.l - k * LIFT_STEP]) {
289
+ if (l >= 0 && l <= 1)
290
+ yield new Oklch(l, origin.c, origin.h);
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * `ink`, a glyph's colour on `ground`, held at least `SEAM_MIN_DELTA_E` off it
296
+ * as `depth` draws both (`Style.drawnColors`). Ink that already stands off is
297
+ * returned as it is. Ink that does not becomes the drawn form of the nearest
298
+ * step along its own OKLCH lightness ray, chroma and hue held, that stands off:
299
+ * the smaller step, toward white or toward black. Every step is measured as the
300
+ * depth draws it, so at 256 colours and at sixteen the candidates are the
301
+ * entries the depth draws, and the side the ink moves to can differ between
302
+ * depths. A floor measured on rounded colours is not monotone along a ray, so
303
+ * the walk enumerates and never bisects. A side with no colour to measure is
304
+ * left alone.
305
+ *
306
+ * [LAW:one-source-of-truth] The colour returned is the one measured — the
307
+ * entry the depth draws, not the step that rounds to it — so a reader of the
308
+ * cell at another depth (an export reads a recording in truecolor) sees the
309
+ * colour that cleared the floor, never a step one unit off its ground.
310
+ *
311
+ * [LAW:single-enforcer] The one place a join cell's visibility is decided:
312
+ * nothing outside the joiner predicts whether a seam will show.
313
+ */
314
+ function standOff(ink, ground, depth) {
315
+ if (ink === undefined || ground === undefined)
316
+ return ink;
317
+ const drawn = (color, at) => new Style({ color, bgcolor: ground }).drawnColors(at);
318
+ const { color: shown, bgcolor: under } = drawn(ink, depth);
319
+ if (standsOff(shown, under))
320
+ return ink;
321
+ // The ray starts from the ink as it is laid on its ground, before a depth
322
+ // rounds it; ink the terminal's theme draws starts from the colour the
323
+ // default terminal theme gives it. Ink that vanishes is drawn in the same
324
+ // kind as its ground — both fixed, or both the theme's — and every step is
325
+ // drawn in that kind too, so it is measured as the ink was. Ink the depth
326
+ // draws as the theme's (a slot, or a mix reaching one at truecolor) steps
327
+ // between slots: an RGB beside a colour only the terminal knows could be
328
+ // that very colour. Ink the depth fixes (that same mix is a cube entry at
329
+ // 256) steps between fixed colours, for the same reason turned round.
330
+ const laid = drawn(ink).color;
331
+ const kind = shown.fixedValue === undefined ? ColorDepth.STANDARD : ColorDepth.TRUECOLOR;
332
+ for (const step of lightnessRay(Oklch.fromRgba(laid.getTruecolor()))) {
333
+ const candidate = drawn(ColorSpec.fromRgba(step.toRgba()).downgrade(kind), depth).color;
334
+ if (standsOff(candidate, under))
335
+ return candidate;
336
+ }
337
+ // [LAW:no-silent-failure] Unreachable: the ray runs out to black and to
338
+ // white, and no ground lies within the floor of both, nor is one slot both.
339
+ throw new Error(`no colour on the lightness ray of ${ink.name} stands off ${ground.name} at depth ${ColorDepth[depth]}`);
338
340
  }
339
341
  function resolvePaint(paint, edges, options) {
340
342
  if ("edge" in paint) {
@@ -352,14 +354,97 @@ function resolvePaint(paint, edges, options) {
352
354
  // A mix with no colour at one end has nothing to blend toward.
353
355
  return a && b && ColorSpec.blend(a, b, t);
354
356
  }
357
+ // [LAW:single-enforcer] The one resolution of a join cell's paints: at join
358
+ // time, and again whenever an effect moves the neighbours it reads
359
+ // (`JoinedCell`).
355
360
  function cellStyle(cell, edges, options) {
356
361
  // [LAW:dataflow-not-control-flow] "ground" is the absence of a colour on its
357
362
  // side; as the fg it is reverse video over the bg colour, so the glyph's ink
358
363
  // is the terminal's own background.
359
364
  if (cell.fg === "ground")
360
365
  return new Style({ color: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options), reverse: true });
361
- const color = resolvePaint(cell.fg, edges, options);
362
- return new Style({ color, bgcolor: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options) });
366
+ const bgcolor = cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options);
367
+ // [LAW:dataflow-not-control-flow] Every cell's ink is held off its ground;
368
+ // the gap measured between them decides how far it moves, usually not at all.
369
+ return new Style({ color: standOff(resolvePaint(cell.fg, edges, options), bgcolor, options.colorSystem ?? ColorDepth.TRUECOLOR), bgcolor });
370
+ }
371
+ /**
372
+ * A cell a `CellJoiner` drew, as what it is: one function of its two
373
+ * neighbours' edges. It holds the cell's paints, the neighbour each side of
374
+ * them reads, and the edges and render it was resolved from, and it is stamped
375
+ * on the cell as the owner of its innermost anchor level.
376
+ *
377
+ * [LAW:one-source-of-truth] A seam's colours are its neighbours', so nothing
378
+ * paints them but this resolution. An effect paints a strip's items and never
379
+ * a join cell that `follows` them; once the items are painted, each such cell
380
+ * is handed what moved at the item cells beside it and resolved again through
381
+ * `cellStyle`, the path the join was drawn by, lift included
382
+ * (`../renderables/effect.ts`). A seam then follows its neighbours wherever an
383
+ * effect takes them, a mix of the two included, and an effect that paints
384
+ * both one colour leaves the seam's ink lifted off it. A cell painted from
385
+ * roles and the terminal's ground alone, as `plain`'s separator is, follows
386
+ * nobody, and an effect paints it as it paints any cell.
387
+ */
388
+ export class JoinedCell {
389
+ cell;
390
+ parts;
391
+ neighbours;
392
+ edges;
393
+ options;
394
+ /** What the cell's paints read on each side: that neighbour and its edge as the cell was resolved from it, or `null` for a side they read nothing of. */
395
+ reads;
396
+ /** Whether the cell's paints read either neighbour, so it follows them under an effect. */
397
+ follows;
398
+ /** The cell as drawn from its edges: the one resolution, at join time and after an effect alike. */
399
+ style;
400
+ constructor(cell, parts, neighbours, edges, options) {
401
+ this.cell = cell;
402
+ this.parts = parts;
403
+ this.neighbours = neighbours;
404
+ this.edges = edges;
405
+ this.options = options;
406
+ const read = (side) => {
407
+ if (parts[side].size === 0)
408
+ return null;
409
+ const [owner, edge] = [neighbours[side], edges[side]];
410
+ // [LAW:no-silent-failure] Unreachable: a join draws a part only beside
411
+ // the neighbours its cells may name: a cap naming the one it is drawn
412
+ // without is refused when parsed (`parsePaint`).
413
+ if (owner === null || edge === undefined)
414
+ throw new Error(`a join cell paints with its ${side} neighbour's edge, and the join has no ${side} neighbour`);
415
+ return { owner, edge };
416
+ };
417
+ this.reads = { left: read("left"), right: read("right") };
418
+ this.follows = this.reads.left !== null || this.reads.right !== null;
419
+ this.style = cellStyle(cell, edges, options);
420
+ }
421
+ /**
422
+ * The cell resolved again once each side's edge has moved as `moves` says:
423
+ * its `colors`, as `cellStyle` draws them, and `style`, the cell's own, with
424
+ * the record of that stamped where this one was, so an effect drawn over
425
+ * this one moves on from these edges. Resolved, lift included, at `depth`,
426
+ * the depth the effect writes the frame at. None when nothing moved of what
427
+ * the cell paints with: a seam of arrows in two grounds is as it was drawn
428
+ * while an effect moves only the lettering beside it, and its lift is not
429
+ * walked again for a frame that cannot change it.
430
+ */
431
+ rederived(moves, style, depth) {
432
+ const reaches = (side) => (moves[side]?.color !== undefined && this.parts[side].has("fg")) || (moves[side]?.bgcolor !== undefined && this.parts[side].has("bg"));
433
+ if (!reaches("left") && !reaches("right"))
434
+ return undefined;
435
+ const moved = (edge, move) => move === undefined ? edge : new Style({ color: move.color ?? edge?.color, bgcolor: move.bgcolor ?? edge?.bgcolor });
436
+ const edges = { left: moved(this.edges.left, moves.left), right: moved(this.edges.right, moves.right) };
437
+ const next = new JoinedCell(this.cell, this.parts, this.neighbours, edges, { ...this.options, colorSystem: depth });
438
+ const restamp = (level) => ({ ...level, owner: level.owner === this ? next : level.owner, inner: level.inner && restamp(level.inner) });
439
+ return { colors: next.style, style: style.withAnchor(style.anchor && restamp(style.anchor)) };
440
+ }
441
+ }
442
+ /** The record a `CellJoiner` stamped on the cell `anchor` names, or none for a cell no `CellJoiner` drew. */
443
+ export function joinedCell(anchor) {
444
+ for (let level = anchor; level; level = level.inner)
445
+ if (level.owner instanceof JoinedCell)
446
+ return level.owner;
447
+ return undefined;
363
448
  }
364
449
  const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
365
450
  /**
@@ -370,9 +455,10 @@ const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyph
370
455
  * Which part a join draws is read off its two neighbours. A neighbour is
371
456
  * missing at the strip's ends, and counts as missing — an endpoint — when the
372
457
  * 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
458
+ * present the join is the seam; with only the right, the lead; with only the
459
+ * left, the tail; with neither, nothing. Every cell is drawn with its ink held
460
+ * off its ground at `options.colorSystem` (`standOff`), measured per cell, so a
461
+ * seam stepping in thirds shows every step however close its grounds. A cap
376
462
  * that paints with its neighbour's ground is drawn only beside a neighbour
377
463
  * with one, so caps that need grounds never stop a seam that needs none.
378
464
  *
@@ -391,7 +477,7 @@ export class CellJoiner {
391
477
  const forms = this._forms;
392
478
  return {
393
479
  *render(options) {
394
- const { shape: drawn, reads } = drawable(options, forms.glyph, forms.ascii, (form) => shapeGlyphs(form.shape));
480
+ const { cells, reads } = drawable(options, forms.glyph, forms.ascii, (form) => shapeGlyphs(form.shape));
395
481
  const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
396
482
  const leftBg = paintableBg(edges.left?.bgcolor);
397
483
  const rightBg = paintableBg(edges.right?.bgcolor);
@@ -400,25 +486,16 @@ export class CellJoiner {
400
486
  const leads = hasRight && (!reads.lead || rightBg !== undefined);
401
487
  const tails = hasLeft && (!reads.tail || leftBg !== undefined);
402
488
  const neighbours = { left, right };
403
- // On screen a cell's ink is its `fg` paint and its ground its `bg`,
404
- // reverse video or not (`cellStyle`).
405
- const ownerOf = (color) => (color !== "ground" && "edge" in color ? neighbours[color.edge] : null);
406
- const styled = (part) => drawn[part].map((cell, at) => {
407
- const owners = new ColorOwners(ownerOf(cell.fg), ownerOf(cell.bg));
408
- return new Segment(pickGlyph(cell, part, index, at), cellStyle(cell, edges, options).withAnchor({ owner: owners, row: 0, col: 0, inner: undefined }));
489
+ const styled = (part) => cells[part].map(({ cell, parts }, at) => {
490
+ const joined = new JoinedCell(cell, parts, neighbours, edges, options);
491
+ return new Segment(pickGlyph(cell, part, index, at), joined.style.withAnchor({ owner: joined, row: 0, col: 0, inner: undefined }));
409
492
  });
410
493
  if (!hasLeft || !hasRight) {
411
494
  if (leads || tails)
412
495
  yield* styled(leads ? "lead" : "tail");
413
496
  return;
414
497
  }
415
- // A seam is lost only when every cell of it is: one cell rounded into
416
- // its ground at a shallow depth still leaves the others to show the
417
- // step, and measuring per cell, not across the two grounds, divides a
418
- // seam stepping in thirds where each third would not be seen.
419
- const seam = styled("seam");
420
- const lost = drawn.divider !== drawn.seam && seam.every((segment) => segment.style !== undefined && vanishes(segment.style, options.colorSystem));
421
- yield* lost ? styled("divider") : seam;
498
+ yield* styled("seam");
422
499
  },
423
500
  };
424
501
  }
@@ -432,52 +509,35 @@ const GROUND = "ground";
432
509
  const LEAD = { cap: "lead" };
433
510
  const TAIL = { cap: "tail" };
434
511
  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));
512
+ const POWERLINE = { glyph: "\ue0b0", ascii: ">" };
513
+ const SLANT = { glyph: "\ue0bc", ascii: "/" };
514
+ const BACKSLANT = { glyph: "\ue0b8", ascii: "\\" };
515
+ const FLAME = { glyph: "\ue0c0", ascii: "}" };
516
+ const PIXEL = { glyph: "\ue0c4", ascii: "#" };
517
+ const ICE = { glyph: "\ue0c8", ascii: "]" };
518
+ const BUBBLE = { glyph: "\ue0b4", ascii: ")" };
519
+ // `glyph` in the left ground over the right.
520
+ const arrow = (glyph) => ({ seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }] });
521
+ const arrowSeam = (f) => both(arrow(f.glyph), arrow(f.ascii));
449
522
  // Three arrows stepping through the two thirds between the grounds.
450
- function prism(glyph, thin) {
523
+ function prism(glyph) {
451
524
  const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
452
- const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
453
525
  return {
454
526
  seam: [
455
527
  { glyph, fg: LEFT_BG, bg: third(1 / 3) },
456
528
  { glyph, fg: third(1 / 3), bg: third(2 / 3) },
457
529
  { glyph, fg: third(2 / 3), bg: RIGHT_BG },
458
530
  ],
459
- divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
460
531
  };
461
532
  }
462
533
  // The runs parted: the caps close the one and open the other, `between` —
463
534
  // cells on the terminal's ground — set between them.
464
- function parted(between) {
465
- const seam = [TAIL, ...between, LEAD];
466
- return { seam, divider: seam };
467
- }
535
+ const parted = (between) => ({ seam: [TAIL, ...between, LEAD] });
468
536
  // One cell drawn over the two grounds' midpoint, so it reads as neither item's.
469
- function midpoint(glyph, fg) {
470
- const seam = [{ glyph, fg, bg: HALF }];
471
- return { seam, divider: seam };
472
- }
473
- // A ragged tear in the left ground over the midpoint; where that ink would
474
- // vanish into the midpoint — grounds a few steps apart, or one ground — the
475
- // same tear in the left item's text.
476
- function torn(glyphs) {
477
- return { seam: midpoint({ oneOf: glyphs }, LEFT_BG).seam, divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
478
- }
479
- const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
480
- const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM };
537
+ const midpoint = (glyph, fg) => ({ seam: [{ glyph, fg, bg: HALF }] });
538
+ // A ragged tear in the left ground over the midpoint.
539
+ const torn = (glyphs) => midpoint({ oneOf: glyphs }, LEFT_BG);
540
+ const PLAIN = { seam: [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }] };
481
541
  const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
482
542
  const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
483
543
  /**
@@ -487,7 +547,7 @@ const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
487
547
  */
488
548
  export const SEAMS = Object.freeze({
489
549
  powerline: parseSeam(arrowSeam(POWERLINE)),
490
- prism: parseSeam(both(prism(POWERLINE.glyph, POWERLINE.thin), prism(POWERLINE.ascii, POWERLINE.asciiThin))),
550
+ prism: parseSeam(both(prism(POWERLINE.glyph), prism(POWERLINE.ascii))),
491
551
  slant: parseSeam(arrowSeam(SLANT)),
492
552
  backslant: parseSeam(arrowSeam(BACKSLANT)),
493
553
  flame: parseSeam(arrowSeam(FLAME)),
@@ -572,12 +632,13 @@ function gradient(steps, glyph) {
572
632
  // samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
573
633
  // so no sample equals either neighbour's ground.
574
634
  const samples = 2 * steps;
575
- const seam = Array.from({ length: steps }, (_, i) => ({
576
- glyph,
577
- fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
578
- bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
579
- }));
580
- return { seam, divider: seam };
635
+ return {
636
+ seam: Array.from({ length: steps }, (_, i) => ({
637
+ glyph,
638
+ fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
639
+ bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
640
+ })),
641
+ };
581
642
  }
582
643
  /**
583
644
  * A ramp of `steps` cells from the left ground to the right. Each cell is the
@@ -11,6 +11,13 @@ import { type Anchor } from "./anchor.js";
11
11
  */
12
12
  export declare const ATTRIBUTE_NAMES: readonly ["bold", "dim", "italic", "underline", "blink", "blink2", "reverse", "conceal", "strike", "underline2", "frame", "encircle", "overline"];
13
13
  export type AttributeName = (typeof ATTRIBUTE_NAMES)[number];
14
+ /**
15
+ * Each text attribute as a style sets it: on, off, or unset — and an unset
16
+ * one lets a style laid beneath (`Style.add`) show through.
17
+ */
18
+ export type Attributes = {
19
+ readonly [A in AttributeName]: boolean | undefined;
20
+ };
14
21
  export declare const ATTRIBUTE_SGR: Record<AttributeName, number>;
15
22
  /**
16
23
  * Canonical short-attribute aliases (`b` → `bold`, `i` → `italic`, …).
@@ -18,7 +25,7 @@ export declare const ATTRIBUTE_SGR: Record<AttributeName, number>;
18
25
  * bindings — adding an alias here makes it available everywhere.
19
26
  * [LAW:one-source-of-truth]
20
27
  */
21
- export declare const ATTRIBUTE_SHORT_ALIASES: Record<string, AttributeName>;
28
+ export declare const ATTRIBUTE_SHORT_ALIASES: Readonly<Record<string, AttributeName>>;
22
29
  export interface StyleOptions {
23
30
  color?: string | ColorSpec;
24
31
  bgcolor?: string | ColorSpec;
@@ -38,7 +45,18 @@ export interface StyleOptions {
38
45
  link?: string;
39
46
  meta?: Record<string, unknown>;
40
47
  anchor?: Anchor;
41
- nextChange?: number;
48
+ /**
49
+ * The hold the style's cells are in, both ends or neither: a cell saying
50
+ * only when it next changes would claim, by default, that it always was,
51
+ * and a timeline run backwards past its change would never wake for it.
52
+ * [LAW:types-are-the-program] Absent, the cells never change.
53
+ */
54
+ hold?: Hold;
55
+ }
56
+ /** A hold: since when a sample has been what it is, and until when it stays so, on one clock. */
57
+ export interface Hold {
58
+ readonly lastChange: number;
59
+ readonly nextChange: number;
42
60
  }
43
61
  export declare class StyleSyntaxError extends Error {
44
62
  constructor(message: string, options?: ErrorOptions);
@@ -60,7 +78,7 @@ export declare class Style {
60
78
  readonly encircle: boolean | undefined;
61
79
  readonly overline: boolean | undefined;
62
80
  readonly link: string | undefined;
63
- readonly meta: Record<string, unknown> | undefined;
81
+ readonly meta: Readonly<Record<string, unknown>> | undefined;
64
82
  /**
65
83
  * Where this style's cells sat in the output of whatever drew them; see
66
84
  * `./anchor.ts`. It draws nothing, and it makes two styles that draw alike
@@ -68,17 +86,19 @@ export declare class Style {
68
86
  */
69
87
  readonly anchor: Anchor | undefined;
70
88
  /**
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
89
+ * The hold this style's cells are in, on the timeline of whoever drew the
90
+ * frame: since when what they show has been what it is (`lastChange`), and
91
+ * when it next changes (`nextChange`) — both `t` for a cell drawn by
92
+ * something that moves every frame, and `-Infinity`/`Infinity` for one
74
93
  * 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.
94
+ * stamps its cells with it (`placedRender`, `Effected`), the stamp rides up
95
+ * through every container as the anchor does (`./anchor.ts`), and whoever
96
+ * holds the composed frame reads it off (`Segment.nextChange`, and
97
+ * `Segment.lastChange` for a host running its timeline backwards), so a
98
+ * host knows when to draw again without asking each container. Two styles
99
+ * laid together are in the intersection of their holds.
80
100
  */
81
- readonly nextChange: number;
101
+ readonly hold: Hold;
82
102
  constructor(options?: StyleOptions);
83
103
  get isNull(): boolean;
84
104
  get transparentBackground(): boolean;
@@ -97,6 +117,19 @@ export declare class Style {
97
117
  * The same look, drawn from the place `anchor` names, or from nowhere.
98
118
  */
99
119
  withAnchor(anchor: Anchor | undefined): Style;
120
+ /** The style's text attributes, each as it sets it. */
121
+ get attributes(): Attributes;
122
+ /**
123
+ * The same style with each attribute `attributes` names set as it says —
124
+ * on, off, or unset — and every other field as it was.
125
+ */
126
+ withAttributes(attributes: Partial<Attributes>): Style;
127
+ /**
128
+ * This style with its motion taken out, what reduced motion draws: `blink`
129
+ * and `blink2`, the motion a terminal makes on its own, unset, and every
130
+ * other field as it was. A style that does not blink is its own.
131
+ */
132
+ reduced(): Style;
100
133
  /**
101
134
  * This style on the cell `cells` to the right: its anchor shifted along its
102
135
  * row, or the style itself when it has no anchor.
@@ -130,7 +163,7 @@ export declare class Style {
130
163
  /**
131
164
  * The look as a canonical string that round-trips through `Style.parse()`:
132
165
  * colours, attributes and the link, which is all a style string can spell.
133
- * What a style carries beyond its look — `meta`, `anchor`, `nextChange` —
166
+ * What a style carries beyond its look — `meta`, `anchor`, `hold` —
134
167
  * has no spelling and is not here, so two styles that differ only there
135
168
  * read alike; `equals` tells them apart.
136
169
  */
@@ -198,7 +231,7 @@ export declare class Theme {
198
231
  resolve(style: TextStyle): Style;
199
232
  has(name: string): boolean;
200
233
  }
201
- export declare const DEFAULT_STYLES: Record<string, Style>;
234
+ export declare const DEFAULT_STYLES: Readonly<Record<string, Style>>;
202
235
  /**
203
236
  * The theme a render resolves names against when nothing supplied one — what
204
237
  * a renderable sees outside a `Console`, as in `renderToString`.