@promptctl/rich-js 0.24.0 → 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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  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 +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -0,0 +1,53 @@
1
+ /**
2
+ * state — `State<T>`, the one place a renderable keeps state that changes after
3
+ * it is built, and the refusal of a write to it while something is drawn.
4
+ *
5
+ * A render changes nothing. React refuses a state update during render because
6
+ * state changes only through a setter, and the setter knows the render phase;
7
+ * this is that setter. `set` throws while a render or measure is in progress
8
+ * (`activePass`, owned by `render` and `measure` in `./protocol.ts`).
9
+ *
10
+ * Ownership is React's line too: a state made during a pass belongs to that
11
+ * pass, and the pass may set it. A render that builds a fresh value and fills
12
+ * it as it draws — a `Table` it adds rows to — changes nothing anyone held
13
+ * before it began. Only a state that existed before the pass is refused.
14
+ *
15
+ * `ListState` is the same for a list a renderable appends to — a table's rows,
16
+ * a text's spans — which a `State<readonly T[]>` would copy on every append.
17
+ *
18
+ * `refuseRenderWrite` is that refusal, written once: a widget's observables
19
+ * refuse a write through it too (`src/widgets/observable.ts`).
20
+ *
21
+ * `test/seam/render-purity.test.ts` is the static half: nothing else reachable
22
+ * from a renderable, or from module scope, may be writable.
23
+ */
24
+ import { type Pass } from "./protocol.js";
25
+ /** State was written during a render or measure that did not make it. */
26
+ export declare class RenderWriteError extends Error {
27
+ constructor();
28
+ }
29
+ export declare class State<T> {
30
+ #private;
31
+ constructor(value: T);
32
+ get(): T;
33
+ set(value: T): void;
34
+ }
35
+ /**
36
+ * A list kept as `State` keeps a value, appended to in place: a `State` of a
37
+ * readonly array copies it on every append, which makes filling a 10,000-row
38
+ * table quadratic. `get` hands out the list itself, typed readonly, so a
39
+ * reader that keeps it sees later appends; `set` replaces it, leaving a kept
40
+ * one as it was.
41
+ */
42
+ export declare class ListState<T> {
43
+ #private;
44
+ constructor(items?: Iterable<T>);
45
+ get(): readonly T[];
46
+ push(item: T): void;
47
+ set(items: Iterable<T>): void;
48
+ }
49
+ /**
50
+ * Throws unless state made in `owner` — the pass in progress when it was made,
51
+ * if any — may be written now: between passes, or in the pass that made it.
52
+ */
53
+ export declare function refuseRenderWrite(owner: Pass | undefined): void;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * state — `State<T>`, the one place a renderable keeps state that changes after
3
+ * it is built, and the refusal of a write to it while something is drawn.
4
+ *
5
+ * A render changes nothing. React refuses a state update during render because
6
+ * state changes only through a setter, and the setter knows the render phase;
7
+ * this is that setter. `set` throws while a render or measure is in progress
8
+ * (`activePass`, owned by `render` and `measure` in `./protocol.ts`).
9
+ *
10
+ * Ownership is React's line too: a state made during a pass belongs to that
11
+ * pass, and the pass may set it. A render that builds a fresh value and fills
12
+ * it as it draws — a `Table` it adds rows to — changes nothing anyone held
13
+ * before it began. Only a state that existed before the pass is refused.
14
+ *
15
+ * `ListState` is the same for a list a renderable appends to — a table's rows,
16
+ * a text's spans — which a `State<readonly T[]>` would copy on every append.
17
+ *
18
+ * `refuseRenderWrite` is that refusal, written once: a widget's observables
19
+ * refuse a write through it too (`src/widgets/observable.ts`).
20
+ *
21
+ * `test/seam/render-purity.test.ts` is the static half: nothing else reachable
22
+ * from a renderable, or from module scope, may be writable.
23
+ */
24
+ import { activePass } from "./protocol.js";
25
+ /** State was written during a render or measure that did not make it. */
26
+ export class RenderWriteError extends Error {
27
+ constructor() {
28
+ super("State was written during a render or measure. A render changes nothing: " +
29
+ "change state in an event handler, an update, or between frames.");
30
+ this.name = "RenderWriteError";
31
+ }
32
+ }
33
+ export class State {
34
+ #value;
35
+ #owner = activePass();
36
+ constructor(value) {
37
+ this.#value = value;
38
+ }
39
+ get() {
40
+ return this.#value;
41
+ }
42
+ set(value) {
43
+ refuseRenderWrite(this.#owner);
44
+ this.#value = value;
45
+ }
46
+ }
47
+ /**
48
+ * A list kept as `State` keeps a value, appended to in place: a `State` of a
49
+ * readonly array copies it on every append, which makes filling a 10,000-row
50
+ * table quadratic. `get` hands out the list itself, typed readonly, so a
51
+ * reader that keeps it sees later appends; `set` replaces it, leaving a kept
52
+ * one as it was.
53
+ */
54
+ export class ListState {
55
+ #items;
56
+ #owner = activePass();
57
+ constructor(items = []) {
58
+ this.#items = [...items];
59
+ }
60
+ get() {
61
+ return this.#items;
62
+ }
63
+ // One item, not a rest parameter: a caller spreading a long list into one overflows the call stack.
64
+ push(item) {
65
+ refuseRenderWrite(this.#owner);
66
+ this.#items.push(item);
67
+ }
68
+ set(items) {
69
+ refuseRenderWrite(this.#owner);
70
+ this.#items = [...items];
71
+ }
72
+ }
73
+ /**
74
+ * Throws unless state made in `owner` — the pass in progress when it was made,
75
+ * if any — may be written now: between passes, or in the pass that made it.
76
+ */
77
+ export function refuseRenderWrite(owner) {
78
+ const pass = activePass();
79
+ // [LAW:no-silent-failure] Refused loudly, as React refuses a setState in render.
80
+ if (pass !== undefined && pass !== owner)
81
+ throw new RenderWriteError();
82
+ }
@@ -166,15 +166,12 @@ export type SeamCell = JoinCell | {
166
166
  readonly cap: "lead" | "tail";
167
167
  };
168
168
  /**
169
- * What a joiner draws between two neighbours, in one form:
170
- * • `seam` — the cells between them;
171
- * • `divider` — in the seam's place where every seam cell's ink is drawn
172
- * indistinct from its own ground (see `SEAM_MIN_DELTA_E`), so the seam
173
- * 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`).
174
172
  */
175
173
  export interface SeamForm {
176
174
  readonly seam: readonly SeamCell[];
177
- readonly divider: readonly SeamCell[];
178
175
  }
179
176
  /**
180
177
  * What a joiner draws at the ends of a run, in one form:
@@ -200,10 +197,10 @@ export interface JoinCaps {
200
197
  readonly ascii: CapsForm;
201
198
  }
202
199
  /**
203
- * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
204
- * seam to be seen. Below it the eye cannot tell the ink from the ground,
205
- * so the joiner draws the divider instead. Twice the ~.02 threshold of a visible difference, because a seam is
206
- * 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.
207
204
  */
208
205
  export declare const SEAM_MIN_DELTA_E = 0.04;
209
206
  /**
@@ -214,9 +211,10 @@ export declare const SEAM_MIN_DELTA_E = 0.04;
214
211
  * Which part a join draws is read off its two neighbours. A neighbour is
215
212
  * missing at the strip's ends, and counts as missing — an endpoint — when the
216
213
  * seam paints with its ground anywhere and it has none to paint. With both
217
- * present the join is the seam, or the divider where every seam cell's ink is
218
- * drawn indistinct from its ground at `options.colorSystem`; with only the
219
- * right, the lead; with only the left, the tail; with neither, nothing. A cap
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
220
218
  * that paints with its neighbour's ground is drawn only beside a neighbour
221
219
  * with one, so caps that need grounds never stop a seam that needs none.
222
220
  *
@@ -31,7 +31,7 @@ import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
31
31
  import { Oklch } from "./oklch.js";
32
32
  import { cellLen } from "./cells.js";
33
33
  import { fnv1a } from "./fnv1a.js";
34
- import { drawable, getStyle, isAscii } from "./protocol.js";
34
+ import { drawable, getStyle, isAscii, render } from "./protocol.js";
35
35
  /**
36
36
  * The owner every cell a strip's joiner draws is stamped with
37
37
  * (`Segment.anchorLines`), whatever the joiner: a seam is told by what drew
@@ -80,7 +80,7 @@ export function colorsOwnedBy(anchor, owners) {
80
80
  */
81
81
  export function drawJoin(joiner, left, right, context) {
82
82
  const join = joiner.join(left, right, context);
83
- return { render: (options) => Segment.anchorLines([[...join.render(options)]], JOIN)[0] };
83
+ return { render: (options) => Segment.anchorLines([[...render(join, options)]], JOIN)[0] };
84
84
  }
85
85
  /**
86
86
  * [LAW:single-enforcer] What `item` draws, every cell stamped as the item's,
@@ -89,7 +89,7 @@ export function drawJoin(joiner, left, right, context) {
89
89
  * item.
90
90
  */
91
91
  export function drawItem(item) {
92
- return { render: (options) => Segment.anchorLines([[...item.render(options)]], item)[0] };
92
+ return { render: (options) => Segment.anchorLines([[...render(item, options)]], item)[0] };
93
93
  }
94
94
  // --- Strip ---
95
95
  export class Strip {
@@ -112,12 +112,12 @@ export class Strip {
112
112
  // `left` fills the canvas, and the first item would pad over the rest of
113
113
  // the row. Where the row sits in the width is its container's to say.
114
114
  const row = { ...options, justify: undefined };
115
- yield* drawJoin(this.joiner, null, items[0], { index: 0 }).render(row);
115
+ yield* render(drawJoin(this.joiner, null, items[0], { index: 0 }), row);
116
116
  for (let i = 0; i < items.length; i++) {
117
117
  const item = items[i];
118
- yield* drawItem(item).render(row);
118
+ yield* render(drawItem(item), row);
119
119
  const next = i + 1 < items.length ? items[i + 1] : null;
120
- yield* drawJoin(this.joiner, item, next, { index: i + 1 }).render(row);
120
+ yield* render(drawJoin(this.joiner, item, next, { index: i + 1 }), row);
121
121
  }
122
122
  // [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
123
123
  // it emits, including the last, and the other block-level renderables
@@ -140,17 +140,17 @@ export class Strip {
140
140
  */
141
141
  measure(options) {
142
142
  let width = 0;
143
- for (const segment of this.render(options))
143
+ for (const segment of render(this, options))
144
144
  width += segment.cellLength;
145
145
  return { minimum: width, maximum: width };
146
146
  }
147
147
  }
148
- const PARTS = ["seam", "divider", "lead", "tail"];
148
+ const PARTS = ["seam", "lead", "tail"];
149
149
  /**
150
- * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
151
- * seam to be seen. Below it the eye cannot tell the ink from the ground,
152
- * so the joiner draws the divider instead. Twice the ~.02 threshold of a visible difference, because a seam is
153
- * 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.
154
154
  */
155
155
  export const SEAM_MIN_DELTA_E = 0.04;
156
156
  function* paintsIn(paint) {
@@ -170,7 +170,6 @@ const pickGlyph = (cell, part, index, at) => {
170
170
  return glyphs[Math.floor((parseInt(fnv1a(`${part}:${index}:${at}`), 16) / 2 ** 32) * glyphs.length)];
171
171
  };
172
172
  const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
173
- const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
174
173
  // `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
175
174
  // name, because the part is also drawn where that neighbour does not exist.
176
175
  function parsePaint(raw, absent, refuse) {
@@ -193,9 +192,9 @@ function parsePaint(raw, absent, refuse) {
193
192
  return refuse(`has a colour that is not a Paint: ${JSON.stringify(raw)}`);
194
193
  }
195
194
  const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
196
- const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
195
+ const ABSENT = { seam: undefined, lead: "left", tail: "right" };
197
196
  // 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" };
197
+ const AUTHORED_IN = { seam: "seam", lead: "caps", tail: "caps" };
199
198
  function parseCell(cell, form, part, refuse) {
200
199
  const glyph = cell.glyph;
201
200
  const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
@@ -243,14 +242,6 @@ const asSeamCell = (form, part) => (cell, refuse) => {
243
242
  refuse("names a cap and draws cells of its own; a cell is one or the other");
244
243
  return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
245
244
  };
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
245
  /**
255
246
  * [LAW:parse-dont-validate] The one place a seam is checked, so every rule
256
247
  * the cell model states is refused at construction and the render reads cells
@@ -259,16 +250,7 @@ const seamWidth = (cells) => [
259
250
  * back is a frozen copy the caller can no longer change.
260
251
  */
261
252
  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
- };
253
+ const parseForm = (form) => Object.freeze({ seam: parseCells(raw, form, "seam", asSeamCell(form, "seam")) });
272
254
  return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
273
255
  }
274
256
  /** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
@@ -278,18 +260,15 @@ function parseCaps(raw) {
278
260
  }
279
261
  // One form of the seam joined with that form of the caps.
280
262
  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 };
263
+ return { seam: seam.seam.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell])), lead: caps.lead, tail: caps.tail };
284
264
  }
285
265
  // Whether any of `cells` paints with `side`'s ground.
286
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")));
287
267
  function formOf(shape) {
288
- const between = [...shape.seam, ...shape.divider];
289
268
  return {
290
269
  shape,
291
270
  reads: {
292
- seam: { left: readsGround(between, "left"), right: readsGround(between, "right") },
271
+ seam: { left: readsGround(shape.seam, "left"), right: readsGround(shape.seam, "right") },
293
272
  lead: readsGround(shape.lead, "right"),
294
273
  tail: readsGround(shape.tail, "left"),
295
274
  },
@@ -308,29 +287,82 @@ function paintableBg(bg) {
308
287
  function drawnGround(bg) {
309
288
  return bg.flattenAlpha(SURFACE_BLACK);
310
289
  }
311
- // A cell whose ink the eye cannot tell from its ground, read off the style it
312
- // is drawn in. What is measured is what is drawn — `Style.drawnColors`, the
313
- // colours the writer encodes: the ink flattened onto the cell's own ground and
314
- // the ground onto the terminal's black, both downgraded to the depth the
315
- // render encodes at, so two colours that depth rounds to one entry are the one
316
- // entry they render as. A colour whose RGB is the terminal theme's own (ANSI
317
- // 0–15, a blend reaching one) has no value here, so two of them are the same
318
- // only when they are one expression: the same palette slot — its `number`,
319
- // never the name or the depth that spells it ("red", "color(1)", an EIGHT_BIT
320
- // spec on 0–15 are one slot) — or the same blend, whose name spells each end
321
- // by its slot for the same reason. A cell on the terminal's ground, or in
322
- // reverse video over it, has a colour nobody knows on one side, and is seen.
323
- function vanishes(style, colorSystem) {
324
- if (style.reverse)
325
- return false;
326
- const { color, bgcolor } = style.drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
327
- if (color === undefined || bgcolor === undefined)
328
- return false;
329
- const av = color.fixedValue;
330
- const bv = bgcolor.fixedValue;
331
- return av !== undefined && bv !== undefined
332
- ? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
333
- : 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]}`);
334
366
  }
335
367
  function resolvePaint(paint, edges, options) {
336
368
  if ("edge" in paint) {
@@ -354,8 +386,10 @@ function cellStyle(cell, edges, options) {
354
386
  // is the terminal's own background.
355
387
  if (cell.fg === "ground")
356
388
  return new Style({ color: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options), reverse: true });
357
- const color = resolvePaint(cell.fg, edges, options);
358
- 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 });
359
393
  }
360
394
  const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
361
395
  /**
@@ -366,9 +400,10 @@ const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyph
366
400
  * Which part a join draws is read off its two neighbours. A neighbour is
367
401
  * missing at the strip's ends, and counts as missing — an endpoint — when the
368
402
  * 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
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
372
407
  * that paints with its neighbour's ground is drawn only beside a neighbour
373
408
  * with one, so caps that need grounds never stop a seam that needs none.
374
409
  *
@@ -408,13 +443,7 @@ export class CellJoiner {
408
443
  yield* styled(leads ? "lead" : "tail");
409
444
  return;
410
445
  }
411
- // A seam is lost only when every cell of it is: one cell rounded into
412
- // its ground at a shallow depth still leaves the others to show the
413
- // step, and measuring per cell, not across the two grounds, divides a
414
- // seam stepping in thirds where each third would not be seen.
415
- const seam = styled("seam");
416
- const lost = drawn.divider !== drawn.seam && seam.every((segment) => segment.style !== undefined && vanishes(segment.style, options.colorSystem));
417
- yield* lost ? styled("divider") : seam;
446
+ yield* styled("seam");
418
447
  },
419
448
  };
420
449
  }
@@ -428,52 +457,35 @@ const GROUND = "ground";
428
457
  const LEAD = { cap: "lead" };
429
458
  const TAIL = { cap: "tail" };
430
459
  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));
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));
445
470
  // Three arrows stepping through the two thirds between the grounds.
446
- function prism(glyph, thin) {
471
+ function prism(glyph) {
447
472
  const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
448
- const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
449
473
  return {
450
474
  seam: [
451
475
  { glyph, fg: LEFT_BG, bg: third(1 / 3) },
452
476
  { glyph, fg: third(1 / 3), bg: third(2 / 3) },
453
477
  { glyph, fg: third(2 / 3), bg: RIGHT_BG },
454
478
  ],
455
- divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
456
479
  };
457
480
  }
458
481
  // The runs parted: the caps close the one and open the other, `between` —
459
482
  // cells on the terminal's ground — set between them.
460
- function parted(between) {
461
- const seam = [TAIL, ...between, LEAD];
462
- return { seam, divider: seam };
463
- }
483
+ const parted = (between) => ({ seam: [TAIL, ...between, LEAD] });
464
484
  // One cell drawn over the two grounds' midpoint, so it reads as neither item's.
465
- function midpoint(glyph, fg) {
466
- const seam = [{ glyph, fg, bg: HALF }];
467
- return { seam, divider: seam };
468
- }
469
- // A ragged tear in the left ground over the midpoint; where that ink would
470
- // vanish into the midpoint — grounds a few steps apart, or one ground — the
471
- // same tear in the left item's text.
472
- function torn(glyphs) {
473
- return { seam: midpoint({ oneOf: glyphs }, LEFT_BG).seam, divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
474
- }
475
- const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
476
- 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 }] };
477
489
  const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
478
490
  const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
479
491
  /**
@@ -483,7 +495,7 @@ const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
483
495
  */
484
496
  export const SEAMS = Object.freeze({
485
497
  powerline: parseSeam(arrowSeam(POWERLINE)),
486
- prism: parseSeam(both(prism(POWERLINE.glyph, POWERLINE.thin), prism(POWERLINE.ascii, POWERLINE.asciiThin))),
498
+ prism: parseSeam(both(prism(POWERLINE.glyph), prism(POWERLINE.ascii))),
487
499
  slant: parseSeam(arrowSeam(SLANT)),
488
500
  backslant: parseSeam(arrowSeam(BACKSLANT)),
489
501
  flame: parseSeam(arrowSeam(FLAME)),
@@ -568,12 +580,13 @@ function gradient(steps, glyph) {
568
580
  // samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
569
581
  // so no sample equals either neighbour's ground.
570
582
  const samples = 2 * steps;
571
- const seam = Array.from({ length: steps }, (_, i) => ({
572
- glyph,
573
- fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
574
- bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
575
- }));
576
- 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
+ };
577
590
  }
578
591
  /**
579
592
  * A ramp of `steps` cells from the left ground to the right. Each cell is the