@promptctl/rich-js 0.24.1 → 0.25.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 (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 +21 -29
  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 +13 -16
  47. package/dist/core/strip.js +119 -110
  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 +132 -54
  73. package/dist/renderables/effect.js +188 -115
  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
@@ -28,7 +28,7 @@
28
28
  import type { Anchor } from "./anchor.js";
29
29
  import { Segment } from "./segment.js";
30
30
  import { Style } from "./style.js";
31
- import { type Composite, type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
31
+ import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
32
32
  /**
33
33
  * Items in a `Strip` expose their per-edge style so joiners can paint the
34
34
  * transition glyph at each boundary.
@@ -97,12 +97,11 @@ export declare function drawJoin<T extends StyledRenderable>(joiner: Joiner<T>,
97
97
  * item.
98
98
  */
99
99
  export declare function drawItem(item: StyledRenderable): Renderable;
100
- export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable, Measurable, Composite {
100
+ export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable, Measurable {
101
101
  readonly items: readonly T[];
102
102
  readonly joiner: Joiner<T>;
103
103
  constructor(items: readonly T[], joiner: Joiner<T>);
104
104
  render(options: RenderOptions): Iterable<Segment>;
105
- contents(): Iterable<Renderable>;
106
105
  /**
107
106
  * [LAW:one-source-of-truth] The width of the one row it draws, and that is
108
107
  * both bounds: the row never wraps and no item is offered less than the whole
@@ -167,15 +166,12 @@ export type SeamCell = JoinCell | {
167
166
  readonly cap: "lead" | "tail";
168
167
  };
169
168
  /**
170
- * What a joiner draws between two neighbours, in one form:
171
- * • `seam` — the cells between them;
172
- * • `divider` — in the seam's place where every seam cell's ink is drawn
173
- * indistinct from its own ground (see `SEAM_MIN_DELTA_E`), so the seam
174
- * would vanish; the same width as `seam`.
169
+ * What a joiner draws between two neighbours, in one form: `seam`, the cells
170
+ * between them. Each is drawn with its ink held off its own ground
171
+ * (`SEAM_MIN_DELTA_E`, `standOff`).
175
172
  */
176
173
  export interface SeamForm {
177
174
  readonly seam: readonly SeamCell[];
178
- readonly divider: readonly SeamCell[];
179
175
  }
180
176
  /**
181
177
  * What a joiner draws at the ends of a run, in one form:
@@ -201,10 +197,10 @@ export interface JoinCaps {
201
197
  readonly ascii: CapsForm;
202
198
  }
203
199
  /**
204
- * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
205
- * seam to be seen. Below it the eye cannot tell the ink from the ground,
206
- * so the joiner draws the divider instead. Twice the ~.02 threshold of a visible difference, because a seam is
207
- * one cell wide.
200
+ * The least ΔE_OK a join cell's ink must stand off its own ground by to be
201
+ * seen. Below it the eye cannot tell the ink from the ground, so the joiner
202
+ * lifts the ink to it (`standOff`). Twice the ~.02 threshold of a visible
203
+ * difference, because a seam is one cell wide.
208
204
  */
209
205
  export declare const SEAM_MIN_DELTA_E = 0.04;
210
206
  /**
@@ -215,9 +211,10 @@ export declare const SEAM_MIN_DELTA_E = 0.04;
215
211
  * Which part a join draws is read off its two neighbours. A neighbour is
216
212
  * missing at the strip's ends, and counts as missing — an endpoint — when the
217
213
  * seam paints with its ground anywhere and it has none to paint. With both
218
- * present the join is the seam, or the divider where every seam cell's ink is
219
- * drawn indistinct from its ground at `options.colorSystem`; with only the
220
- * right, the lead; with only the left, the tail; with neither, nothing. A cap
214
+ * present the join is the seam; with only the right, the lead; with only the
215
+ * left, the tail; with neither, nothing. Every cell is drawn with its ink held
216
+ * off its ground at `options.colorSystem` (`standOff`), measured per cell, so a
217
+ * seam stepping in thirds shows every step however close its grounds. A cap
221
218
  * that paints with its neighbour's ground is drawn only beside a neighbour
222
219
  * with one, so caps that need grounds never stop a seam that needs none.
223
220
  *
@@ -131,10 +131,6 @@ 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
- }
138
134
  /**
139
135
  * [LAW:one-source-of-truth] The width of the one row it draws, and that is
140
136
  * both bounds: the row never wraps and no item is offered less than the whole
@@ -149,12 +145,12 @@ export class Strip {
149
145
  return { minimum: width, maximum: width };
150
146
  }
151
147
  }
152
- const PARTS = ["seam", "divider", "lead", "tail"];
148
+ const PARTS = ["seam", "lead", "tail"];
153
149
  /**
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.
150
+ * The least ΔE_OK a join cell's ink must stand off its own ground by to be
151
+ * seen. Below it the eye cannot tell the ink from the ground, so the joiner
152
+ * lifts the ink to it (`standOff`). Twice the ~.02 threshold of a visible
153
+ * difference, because a seam is one cell wide.
158
154
  */
159
155
  export const SEAM_MIN_DELTA_E = 0.04;
160
156
  function* paintsIn(paint) {
@@ -174,7 +170,6 @@ const pickGlyph = (cell, part, index, at) => {
174
170
  return glyphs[Math.floor((parseInt(fnv1a(`${part}:${index}:${at}`), 16) / 2 ** 32) * glyphs.length)];
175
171
  };
176
172
  const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
177
- const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
178
173
  // `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
179
174
  // name, because the part is also drawn where that neighbour does not exist.
180
175
  function parsePaint(raw, absent, refuse) {
@@ -197,9 +192,9 @@ function parsePaint(raw, absent, refuse) {
197
192
  return refuse(`has a colour that is not a Paint: ${JSON.stringify(raw)}`);
198
193
  }
199
194
  const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
200
- const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
195
+ const ABSENT = { seam: undefined, lead: "left", tail: "right" };
201
196
  // 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" };
197
+ const AUTHORED_IN = { seam: "seam", lead: "caps", tail: "caps" };
203
198
  function parseCell(cell, form, part, refuse) {
204
199
  const glyph = cell.glyph;
205
200
  const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
@@ -247,14 +242,6 @@ const asSeamCell = (form, part) => (cell, refuse) => {
247
242
  refuse("names a cap and draws cells of its own; a cell is one or the other");
248
243
  return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
249
244
  };
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
245
  /**
259
246
  * [LAW:parse-dont-validate] The one place a seam is checked, so every rule
260
247
  * the cell model states is refused at construction and the render reads cells
@@ -263,16 +250,7 @@ const seamWidth = (cells) => [
263
250
  * back is a frozen copy the caller can no longer change.
264
251
  */
265
252
  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
- };
253
+ const parseForm = (form) => Object.freeze({ seam: parseCells(raw, form, "seam", asSeamCell(form, "seam")) });
276
254
  return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
277
255
  }
278
256
  /** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
@@ -282,18 +260,15 @@ function parseCaps(raw) {
282
260
  }
283
261
  // One form of the seam joined with that form of the caps.
284
262
  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 };
263
+ return { seam: seam.seam.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell])), lead: caps.lead, tail: caps.tail };
288
264
  }
289
265
  // Whether any of `cells` paints with `side`'s ground.
290
266
  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
267
  function formOf(shape) {
292
- const between = [...shape.seam, ...shape.divider];
293
268
  return {
294
269
  shape,
295
270
  reads: {
296
- seam: { left: readsGround(between, "left"), right: readsGround(between, "right") },
271
+ seam: { left: readsGround(shape.seam, "left"), right: readsGround(shape.seam, "right") },
297
272
  lead: readsGround(shape.lead, "right"),
298
273
  tail: readsGround(shape.tail, "left"),
299
274
  },
@@ -312,29 +287,82 @@ function paintableBg(bg) {
312
287
  function drawnGround(bg) {
313
288
  return bg.flattenAlpha(SURFACE_BLACK);
314
289
  }
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;
290
+ // Whether ink stands off its ground, both as a depth draws them, by the floor a
291
+ // seam is seen at. A colour whose RGB is the terminal theme's own (ANSI 0–15, a
292
+ // blend reaching one) has no value here, so two of them stand apart unless they
293
+ // are one expression: the same palette slot — its `number`, never the name or
294
+ // the depth that spells it ("red", "color(1)", an EIGHT_BIT spec on 0–15 are
295
+ // one slot) — or the same blend, whose name spells each end by its slot for the
296
+ // same reason. At sixteen colours, then, the floor is a different slot. The
297
+ // terminal's default colour is nobody's to measure, and stands apart.
298
+ function standsOff(ink, ground) {
299
+ if (ink.isDefault || ground.isDefault)
300
+ return true;
301
+ const [a, b] = [ink.fixedValue, ground.fixedValue];
302
+ return a !== undefined && b !== undefined
303
+ ? Oklch.fromRgba(a).deltaE(Oklch.fromRgba(b)) >= SEAM_MIN_DELTA_E
304
+ : ink.number !== undefined ? ink.number !== ground.number : ink.name !== ground.name;
305
+ }
306
+ // A step along a lightness ray: finer than the steps between the colours a
307
+ // truecolor writer draws, so the walk passes over none of them.
308
+ const LIFT_STEP = 0.001;
309
+ // The steps along `origin`'s lightness ray, its chroma and hue held, in the
310
+ // order the lift tries them: the smaller move first, and at an equal move the
311
+ // lighter, out to black and to white.
312
+ function* lightnessRay(origin) {
313
+ for (let k = 1; k * LIFT_STEP <= 1; k++) {
314
+ for (const l of [origin.l + k * LIFT_STEP, origin.l - k * LIFT_STEP]) {
315
+ if (l >= 0 && l <= 1)
316
+ yield new Oklch(l, origin.c, origin.h);
317
+ }
318
+ }
319
+ }
320
+ /**
321
+ * `ink`, a glyph's colour on `ground`, held at least `SEAM_MIN_DELTA_E` off it
322
+ * as `depth` draws both (`Style.drawnColors`). Ink that already stands off is
323
+ * returned as it is. Ink that does not becomes the drawn form of the nearest
324
+ * step along its own OKLCH lightness ray, chroma and hue held, that stands off:
325
+ * the smaller step, toward white or toward black. Every step is measured as the
326
+ * depth draws it, so at 256 colours and at sixteen the candidates are the
327
+ * entries the depth draws, and the side the ink moves to can differ between
328
+ * depths. A floor measured on rounded colours is not monotone along a ray, so
329
+ * the walk enumerates and never bisects. A side with no colour to measure is
330
+ * left alone.
331
+ *
332
+ * [LAW:one-source-of-truth] The colour returned is the one measured — the
333
+ * entry the depth draws, not the step that rounds to it — so a reader of the
334
+ * cell at another depth (an export reads a recording in truecolor) sees the
335
+ * colour that cleared the floor, never a step one unit off its ground.
336
+ *
337
+ * [LAW:single-enforcer] The one place a join cell's visibility is decided:
338
+ * nothing outside the joiner predicts whether a seam will show.
339
+ */
340
+ function standOff(ink, ground, depth) {
341
+ if (ink === undefined || ground === undefined)
342
+ return ink;
343
+ const drawn = (color, at) => new Style({ color, bgcolor: ground }).drawnColors(at);
344
+ const { color: shown, bgcolor: under } = drawn(ink, depth);
345
+ if (standsOff(shown, under))
346
+ return ink;
347
+ // The ray starts from the ink as it is laid on its ground, before a depth
348
+ // rounds it; ink the terminal's theme draws starts from the colour the
349
+ // default terminal theme gives it. Ink that vanishes is drawn in the same
350
+ // kind as its ground — both fixed, or both the theme's — and every step is
351
+ // drawn in that kind too, so it is measured as the ink was. Ink the depth
352
+ // draws as the theme's (a slot, or a mix reaching one at truecolor) steps
353
+ // between slots: an RGB beside a colour only the terminal knows could be
354
+ // that very colour. Ink the depth fixes (that same mix is a cube entry at
355
+ // 256) steps between fixed colours, for the same reason turned round.
356
+ const laid = drawn(ink).color;
357
+ const kind = shown.fixedValue === undefined ? ColorDepth.STANDARD : ColorDepth.TRUECOLOR;
358
+ for (const step of lightnessRay(Oklch.fromRgba(laid.getTruecolor()))) {
359
+ const candidate = drawn(ColorSpec.fromRgba(step.toRgba()).downgrade(kind), depth).color;
360
+ if (standsOff(candidate, under))
361
+ return candidate;
362
+ }
363
+ // [LAW:no-silent-failure] Unreachable: the ray runs out to black and to
364
+ // white, and no ground lies within the floor of both, nor is one slot both.
365
+ throw new Error(`no colour on the lightness ray of ${ink.name} stands off ${ground.name} at depth ${ColorDepth[depth]}`);
338
366
  }
339
367
  function resolvePaint(paint, edges, options) {
340
368
  if ("edge" in paint) {
@@ -358,8 +386,10 @@ function cellStyle(cell, edges, options) {
358
386
  // is the terminal's own background.
359
387
  if (cell.fg === "ground")
360
388
  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) });
389
+ const bgcolor = cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options);
390
+ // [LAW:dataflow-not-control-flow] Every cell's ink is held off its ground;
391
+ // the gap measured between them decides how far it moves, usually not at all.
392
+ return new Style({ color: standOff(resolvePaint(cell.fg, edges, options), bgcolor, options.colorSystem ?? ColorDepth.TRUECOLOR), bgcolor });
363
393
  }
364
394
  const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
365
395
  /**
@@ -370,9 +400,10 @@ const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyph
370
400
  * Which part a join draws is read off its two neighbours. A neighbour is
371
401
  * missing at the strip's ends, and counts as missing — an endpoint — when the
372
402
  * 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
403
+ * present the join is the seam; with only the right, the lead; with only the
404
+ * left, the tail; with neither, nothing. Every cell is drawn with its ink held
405
+ * off its ground at `options.colorSystem` (`standOff`), measured per cell, so a
406
+ * seam stepping in thirds shows every step however close its grounds. A cap
376
407
  * that paints with its neighbour's ground is drawn only beside a neighbour
377
408
  * with one, so caps that need grounds never stop a seam that needs none.
378
409
  *
@@ -412,13 +443,7 @@ export class CellJoiner {
412
443
  yield* styled(leads ? "lead" : "tail");
413
444
  return;
414
445
  }
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;
446
+ yield* styled("seam");
422
447
  },
423
448
  };
424
449
  }
@@ -432,52 +457,35 @@ const GROUND = "ground";
432
457
  const LEAD = { cap: "lead" };
433
458
  const TAIL = { cap: "tail" };
434
459
  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));
460
+ const POWERLINE = { glyph: "\ue0b0", ascii: ">" };
461
+ const SLANT = { glyph: "\ue0bc", ascii: "/" };
462
+ const BACKSLANT = { glyph: "\ue0b8", ascii: "\\" };
463
+ const FLAME = { glyph: "\ue0c0", ascii: "}" };
464
+ const PIXEL = { glyph: "\ue0c4", ascii: "#" };
465
+ const ICE = { glyph: "\ue0c8", ascii: "]" };
466
+ const BUBBLE = { glyph: "\ue0b4", ascii: ")" };
467
+ // `glyph` in the left ground over the right.
468
+ const arrow = (glyph) => ({ seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }] });
469
+ const arrowSeam = (f) => both(arrow(f.glyph), arrow(f.ascii));
449
470
  // Three arrows stepping through the two thirds between the grounds.
450
- function prism(glyph, thin) {
471
+ function prism(glyph) {
451
472
  const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
452
- const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
453
473
  return {
454
474
  seam: [
455
475
  { glyph, fg: LEFT_BG, bg: third(1 / 3) },
456
476
  { glyph, fg: third(1 / 3), bg: third(2 / 3) },
457
477
  { glyph, fg: third(2 / 3), bg: RIGHT_BG },
458
478
  ],
459
- divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
460
479
  };
461
480
  }
462
481
  // The runs parted: the caps close the one and open the other, `between` —
463
482
  // 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
- }
483
+ const parted = (between) => ({ seam: [TAIL, ...between, LEAD] });
468
484
  // 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 };
485
+ const midpoint = (glyph, fg) => ({ seam: [{ glyph, fg, bg: HALF }] });
486
+ // A ragged tear in the left ground over the midpoint.
487
+ const torn = (glyphs) => midpoint({ oneOf: glyphs }, LEFT_BG);
488
+ const PLAIN = { seam: [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }] };
481
489
  const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
482
490
  const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
483
491
  /**
@@ -487,7 +495,7 @@ const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
487
495
  */
488
496
  export const SEAMS = Object.freeze({
489
497
  powerline: parseSeam(arrowSeam(POWERLINE)),
490
- prism: parseSeam(both(prism(POWERLINE.glyph, POWERLINE.thin), prism(POWERLINE.ascii, POWERLINE.asciiThin))),
498
+ prism: parseSeam(both(prism(POWERLINE.glyph), prism(POWERLINE.ascii))),
491
499
  slant: parseSeam(arrowSeam(SLANT)),
492
500
  backslant: parseSeam(arrowSeam(BACKSLANT)),
493
501
  flame: parseSeam(arrowSeam(FLAME)),
@@ -572,12 +580,13 @@ function gradient(steps, glyph) {
572
580
  // samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
573
581
  // so no sample equals either neighbour's ground.
574
582
  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 };
583
+ return {
584
+ seam: Array.from({ length: steps }, (_, i) => ({
585
+ glyph,
586
+ fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
587
+ bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
588
+ })),
589
+ };
581
590
  }
582
591
  /**
583
592
  * 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`.