@promptctl/rich-js 0.20.0 → 0.22.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.
@@ -43,3 +43,8 @@ export interface Anchor {
43
43
  * every level of the nesting — each level names the same cell.
44
44
  */
45
45
  export declare function shiftAnchor(anchor: Anchor, cells: number): Anchor;
46
+ /**
47
+ * Whether `owner` drew the cell `anchor` names, itself or through an owner
48
+ * nested in its output.
49
+ */
50
+ export declare function drawnBy(anchor: Anchor | undefined, owner: object): boolean;
@@ -37,3 +37,13 @@ export function shiftAnchor(anchor, cells) {
37
37
  inner: anchor.inner && shiftAnchor(anchor.inner, cells),
38
38
  };
39
39
  }
40
+ /**
41
+ * Whether `owner` drew the cell `anchor` names, itself or through an owner
42
+ * nested in its output.
43
+ */
44
+ export function drawnBy(anchor, owner) {
45
+ for (let level = anchor; level; level = level.inner)
46
+ if (level.owner === owner)
47
+ return true;
48
+ return false;
49
+ }
@@ -259,6 +259,15 @@ export declare class ColorSpec {
259
259
  * on a ground (`flattenAlpha`).
260
260
  */
261
261
  static matchOn(value: ColorRgba, depth: ColorDepth, theme: TerminalTheme, foreground: boolean): ColorSpec;
262
+ /**
263
+ * The spec a slot is written in once its colour moves from `was`, what its
264
+ * written spec draws on `theme`, to the opaque `now`: `matchOn`'s — or
265
+ * none, where the written spec is as near `now` as that match. A move the
266
+ * depth cannot show is no move, so below truecolor a colour moved a little
267
+ * off any colour the depth can write — a theme slot, the default colour,
268
+ * the cube — keeps the spec it was written in.
269
+ */
270
+ static matchMove(was: ColorRgba, now: ColorRgba, depth: ColorDepth, theme: TerminalTheme, foreground: boolean): ColorSpec | undefined;
262
271
  /**
263
272
  * Parse a color string. Memoized (`Memo`): a string parsed again while
264
273
  * remembered returns the same instance.
@@ -581,7 +581,17 @@ export class ColorSpec {
581
581
  throw new ColorParseError(`A blend's fraction is in [0, 1]; got ${t}`);
582
582
  const a = from.fixedValue;
583
583
  const b = to.fixedValue;
584
- return a !== undefined && b !== undefined ? ColorSpec.fromRgba(blendRgb(a, b, t)) : new ColorBlend(from, to, t);
584
+ if (a !== undefined && b !== undefined)
585
+ return ColorSpec.fromRgba(blendRgb(a, b, t));
586
+ // A blend that draws as one of its ends is that end — not a blend whose
587
+ // name no comparison will ever match back to it: at t 0 or 1, or between
588
+ // one slot of one palette spelled twice.
589
+ if (t === 0)
590
+ return from;
591
+ if (t === 1)
592
+ return to;
593
+ const oneSlot = from.type === to.type && (from.number !== undefined ? from.number === to.number : from.name === to.name);
594
+ return oneSlot ? from : new ColorBlend(from, to, t);
585
595
  }
586
596
  /**
587
597
  * The spec that draws `value` nearest to it at `depth` on a terminal known
@@ -614,6 +624,18 @@ export class ColorSpec {
614
624
  ];
615
625
  return candidates.reduce((best, next) => (rgbDistance(next[1], value) < rgbDistance(best[1], value) ? next : best))[0];
616
626
  }
627
+ /**
628
+ * The spec a slot is written in once its colour moves from `was`, what its
629
+ * written spec draws on `theme`, to the opaque `now`: `matchOn`'s — or
630
+ * none, where the written spec is as near `now` as that match. A move the
631
+ * depth cannot show is no move, so below truecolor a colour moved a little
632
+ * off any colour the depth can write — a theme slot, the default colour,
633
+ * the cube — keeps the spec it was written in.
634
+ */
635
+ static matchMove(was, now, depth, theme, foreground) {
636
+ const match = ColorSpec.matchOn(now, depth, theme, foreground);
637
+ return rgbDistance(was, now) <= rgbDistance(match.getTruecolor(theme, foreground), now) ? undefined : match;
638
+ }
617
639
  /**
618
640
  * Parse a color string. Memoized (`Memo`): a string parsed again while
619
641
  * remembered returns the same instance.
@@ -25,6 +25,7 @@
25
25
  * single-style invariant on the cell type — the terminal supports per-column
26
26
  * styling, so the cell type does too.
27
27
  */
28
+ import type { Anchor } from "./anchor.js";
28
29
  import { Segment } from "./segment.js";
29
30
  import { Style } from "./style.js";
30
31
  import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
@@ -44,14 +45,58 @@ import { type Measurable, type Renderable, type RenderOptions } from "./protocol
44
45
  export interface StyledRenderable extends Renderable {
45
46
  edgeStyle(side: "left" | "right", options: RenderOptions): Style;
46
47
  }
48
+ /**
49
+ * Where a join sits in its strip. `index` counts joins from the strip's start:
50
+ * the join before item `i` is `i`, and the end after the last of `n` items is
51
+ * `n`. It names the seam, not the line it lands on, so where a `FlexStrip`
52
+ * wraps, the end cap and the next line's lead both carry the index of the seam
53
+ * the wrap replaced — and every seam that survives a wrap keeps its own.
54
+ */
55
+ export interface JoinContext {
56
+ readonly index: number;
57
+ }
47
58
  export interface Joiner<T extends StyledRenderable = StyledRenderable> {
48
59
  /**
49
60
  * `left === null` marks the start endpoint; `right === null` marks the end
50
61
  * endpoint. The joiner decides what an endpoint looks like — typically
51
62
  * fg-only with no bg so the strip blends into the terminal background.
52
63
  */
53
- join(left: T | null, right: T | null): Renderable;
64
+ join(left: T | null, right: T | null, context: JoinContext): Renderable;
54
65
  }
66
+ /**
67
+ * The owner every cell a strip's joiner draws is stamped with
68
+ * (`Segment.anchorLines`), whatever the joiner: a seam is told by what drew
69
+ * it, never by its glyph, which an ASCII joiner draws otherwise and a lead may
70
+ * not draw at all. An effect reads it to leave a seam out of the words it
71
+ * keeps legible (`drawnOf`, `../renderables/effect.ts`).
72
+ *
73
+ * Inside it, every cell a `CellJoiner` draws names whose colour each of its
74
+ * two sides shows (`colorsOwnedBy`). One seam cell can show two neighbours —
75
+ * a powerline arrow is inked in the fill it points out of, over the fill it
76
+ * points into — so a cell has no single owner; each side has one.
77
+ */
78
+ export declare const JOIN: object;
79
+ /**
80
+ * Which of a cell's two colours on screen are `owner`'s: both, for a cell
81
+ * `owner` drew, itself or through an owner nested in its output; for a cell a
82
+ * `CellJoiner` drew, the sides shown in `owner`'s colour.
83
+ */
84
+ export declare function colorsOwnedBy(anchor: Anchor | undefined, owner: object): {
85
+ readonly fg: boolean;
86
+ readonly bg: boolean;
87
+ };
88
+ /**
89
+ * [LAW:single-enforcer] What `joiner` draws at one join, every cell stamped
90
+ * as `JOIN`'s: the one way `Strip` and `FlexStrip` draw a join.
91
+ */
92
+ export declare function drawJoin<T extends StyledRenderable>(joiner: Joiner<T>, left: T | null, right: T | null, context: JoinContext): Renderable;
93
+ /**
94
+ * [LAW:single-enforcer] What `item` draws, every cell stamped as the item's,
95
+ * so whoever holds the composed row reads which item drew a cell however the
96
+ * row was laid out or wrapped: the one way `Strip` and `FlexStrip` draw an
97
+ * item.
98
+ */
99
+ export declare function drawItem(item: StyledRenderable): Renderable;
55
100
  export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable, Measurable {
56
101
  readonly items: readonly T[];
57
102
  readonly joiner: Joiner<T>;
@@ -98,7 +143,10 @@ export type CellColor = Paint | "ground";
98
143
  /**
99
144
  * One styled run of a join. A `oneOf` glyph is a set any member of which may
100
145
  * stand in the cell; every member has the same width, because consumers
101
- * measure a join by rendering it.
146
+ * measure a join by rendering it. Which member stands is hashed from the
147
+ * join's part, its `index` and the cell's place in it, so it holds still from
148
+ * frame to frame while the items before it do, and differs from seam to seam
149
+ * even between the same two colours.
102
150
  */
103
151
  export interface JoinCell {
104
152
  readonly glyph: string | {
@@ -110,8 +158,9 @@ export interface JoinCell {
110
158
  /**
111
159
  * The cells a joiner draws at each kind of join:
112
160
  * • `seam` — between two neighbours;
113
- * • `divider` — in the seam's place where the two grounds are drawn as one
114
- * colour (see `SEAM_MIN_DELTA_E`), so the seam would vanish into them;
161
+ * • `divider` — in the seam's place where every seam cell's ink is drawn
162
+ * indistinct from its own ground (see `SEAM_MIN_DELTA_E`), so the seam
163
+ * would vanish;
115
164
  * the same width as `seam`;
116
165
  * • `lead` — where a run is entered from nothing: the strip's start, or a
117
166
  * neighbour with no ground for the seam to paint. It cannot name `left`;
@@ -124,10 +173,9 @@ export interface JoinShape {
124
173
  readonly tail: readonly JoinCell[];
125
174
  }
126
175
  /**
127
- * The least ΔE_OK two neighbouring backgrounds must differ by for a seam drawn
128
- * between them to be seen. Below it the seam is drawn in a colour the eye
129
- * cannot tell from its own background, so the joiner draws the divider
130
- * instead. Twice the ~.02 threshold of a visible difference, because a seam is
176
+ * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
177
+ * seam to be seen. Below it the eye cannot tell the ink from the ground,
178
+ * so the joiner draws the divider instead. Twice the ~.02 threshold of a visible difference, because a seam is
131
179
  * one cell wide.
132
180
  */
133
181
  export declare const SEAM_MIN_DELTA_E = 0.04;
@@ -138,8 +186,8 @@ export declare const SEAM_MIN_DELTA_E = 0.04;
138
186
  * Which part a join draws is read off its two neighbours. A neighbour is
139
187
  * missing at the strip's ends, and counts as missing — an endpoint — when the
140
188
  * shape paints with its ground anywhere and it has none to paint. With both present the
141
- * join is the seam, or the divider where the two grounds are drawn as one
142
- * colour at `options.colorSystem`; with only the right, the lead; with only
189
+ * join is the seam, or the divider where every seam cell's ink is drawn
190
+ * indistinct from its ground at `options.colorSystem`; with only the right, the lead; with only
143
191
  * the left, the tail; with neither, nothing.
144
192
  *
145
193
  * `ascii` is the shape an ASCII-only output draws instead, whole.
@@ -149,30 +197,34 @@ export declare class CellJoiner<T extends StyledRenderable = StyledRenderable> i
149
197
  readonly ascii: JoinShape;
150
198
  private readonly _forms;
151
199
  constructor(shape: JoinShape, ascii: JoinShape);
152
- join(left: T | null, right: T | null): Renderable;
200
+ join(left: T | null, right: T | null, { index }: JoinContext): Renderable;
153
201
  }
154
202
  /**
155
- * The powerline joiner: U+E0B0 (right-arrow) in the left ground over the
156
- * right, divided by U+E0B1 (thin right-arrow) in the left item's text. The
157
- * lead is the arrow with the left ground replaced by the terminal's — drawn
158
- * in reverse video it leaves a notch whose base is the cell's left edge, inked
159
- * in the terminal's background colour. That ink is opaque, so under a
160
- * translucent terminal the notch does not let what is behind it through. The
161
- * tail is the arrow over the terminal's ground.
162
- * ASCII draws `>` everywhere and begins each run flat.
163
- */
164
- export declare const POWERLINE_JOINER: CellJoiner<StyledRenderable>;
165
- /**
166
- * Rounded pills: each run opened by U+E0B6 and closed by U+E0B4, both in its
167
- * ground over the terminal's, a space between pills. ASCII draws `(` and `)`.
203
+ * The bundled joiners, by name. What each draws is `docs/strip.md`'s
204
+ * "Built-in joiners"; a name from config is read with `joinerNamed`.
168
205
  */
169
- export declare const CAPSULE_JOINER: CellJoiner<StyledRenderable>;
206
+ export declare const JOINERS: Readonly<{
207
+ powerline: CellJoiner<StyledRenderable>;
208
+ prism: CellJoiner<StyledRenderable>;
209
+ slant: CellJoiner<StyledRenderable>;
210
+ backslant: CellJoiner<StyledRenderable>;
211
+ flame: CellJoiner<StyledRenderable>;
212
+ pixel: CellJoiner<StyledRenderable>;
213
+ ice: CellJoiner<StyledRenderable>;
214
+ bubble: CellJoiner<StyledRenderable>;
215
+ capsule: CellJoiner<StyledRenderable>;
216
+ wrapper: CellJoiner<StyledRenderable>;
217
+ metro: CellJoiner<StyledRenderable>;
218
+ stitch: CellJoiner<StyledRenderable>;
219
+ torn: CellJoiner<StyledRenderable>;
220
+ plain: CellJoiner<StyledRenderable>;
221
+ }>;
170
222
  /**
171
- * ` | ` between every pair of items, in the theme's `strip.separator` colour;
172
- * nothing at the ends. Its shape names no neighbour, so it joins items with no
173
- * ground too.
223
+ * The bundled joiner `name` names — a name read from config, parsed. Throws on
224
+ * any other string, naming the ones there are; an own-key test, so `toString`
225
+ * and the rest of `Object.prototype` are not joiners.
174
226
  */
175
- export declare const PLAIN_JOINER: CellJoiner<StyledRenderable>;
227
+ export declare function joinerNamed(name: string): CellJoiner;
176
228
  /**
177
229
  * A ramp of `steps` cells from the left ground to the right. Each cell is the
178
230
  * half block U+258C, ink on its left half and ground on its right, so it
@@ -30,7 +30,65 @@ import { Style } from "./style.js";
30
30
  import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
31
31
  import { Oklch } from "./oklch.js";
32
32
  import { cellLen } from "./cells.js";
33
+ import { fnv1a } from "./fnv1a.js";
33
34
  import { drawable, getStyle, isAscii } from "./protocol.js";
35
+ /**
36
+ * The owner every cell a strip's joiner draws is stamped with
37
+ * (`Segment.anchorLines`), whatever the joiner: a seam is told by what drew
38
+ * it, never by its glyph, which an ASCII joiner draws otherwise and a lead may
39
+ * not draw at all. An effect reads it to leave a seam out of the words it
40
+ * keeps legible (`drawnOf`, `../renderables/effect.ts`).
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.
46
+ */
47
+ 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 `owner`'s: both, for a cell
63
+ * `owner` drew, itself or through an owner nested in its output; for a cell a
64
+ * `CellJoiner` drew, the sides shown in `owner`'s colour.
65
+ */
66
+ export function colorsOwnedBy(anchor, owner) {
67
+ for (let level = anchor; level; level = level.inner) {
68
+ if (level.owner === owner)
69
+ return { fg: true, bg: true };
70
+ if (level.owner instanceof ColorOwners)
71
+ return { fg: level.owner.fg === owner, bg: level.owner.bg === owner };
72
+ }
73
+ return { fg: false, bg: false };
74
+ }
75
+ /**
76
+ * [LAW:single-enforcer] What `joiner` draws at one join, every cell stamped
77
+ * as `JOIN`'s: the one way `Strip` and `FlexStrip` draw a join.
78
+ */
79
+ export function drawJoin(joiner, left, right, context) {
80
+ const join = joiner.join(left, right, context);
81
+ return { render: (options) => Segment.anchorLines([[...join.render(options)]], JOIN)[0] };
82
+ }
83
+ /**
84
+ * [LAW:single-enforcer] What `item` draws, every cell stamped as the item's,
85
+ * so whoever holds the composed row reads which item drew a cell however the
86
+ * row was laid out or wrapped: the one way `Strip` and `FlexStrip` draw an
87
+ * item.
88
+ */
89
+ export function drawItem(item) {
90
+ return { render: (options) => Segment.anchorLines([[...item.render(options)]], item)[0] };
91
+ }
34
92
  // --- Strip ---
35
93
  export class Strip {
36
94
  items;
@@ -52,12 +110,12 @@ export class Strip {
52
110
  // `left` fills the canvas, and the first item would pad over the rest of
53
111
  // the row. Where the row sits in the width is its container's to say.
54
112
  const row = { ...options, justify: undefined };
55
- yield* this.joiner.join(null, items[0]).render(row);
113
+ yield* drawJoin(this.joiner, null, items[0], { index: 0 }).render(row);
56
114
  for (let i = 0; i < items.length; i++) {
57
115
  const item = items[i];
58
- yield* item.render(row);
116
+ yield* drawItem(item).render(row);
59
117
  const next = i + 1 < items.length ? items[i + 1] : null;
60
- yield* this.joiner.join(item, next).render(row);
118
+ yield* drawJoin(this.joiner, item, next, { index: i + 1 }).render(row);
61
119
  }
62
120
  // [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
63
121
  // it emits, including the last, and the other block-level renderables
@@ -87,10 +145,9 @@ export class Strip {
87
145
  }
88
146
  const PARTS = ["seam", "divider", "lead", "tail"];
89
147
  /**
90
- * The least ΔE_OK two neighbouring backgrounds must differ by for a seam drawn
91
- * between them to be seen. Below it the seam is drawn in a colour the eye
92
- * cannot tell from its own background, so the joiner draws the divider
93
- * instead. Twice the ~.02 threshold of a visible difference, because a seam is
148
+ * The least ΔE_OK a seam cell's ink must differ from its own ground by for the
149
+ * seam to be seen. Below it the eye cannot tell the ink from the ground,
150
+ * so the joiner draws the divider instead. Twice the ~.02 threshold of a visible difference, because a seam is
94
151
  * one cell wide.
95
152
  */
96
153
  export const SEAM_MIN_DELTA_E = 0.04;
@@ -102,6 +159,14 @@ function* paintsIn(paint) {
102
159
  }
103
160
  }
104
161
  const glyphsOf = (cell) => (typeof cell.glyph === "string" ? [cell.glyph] : cell.glyph.oneOf);
162
+ // An integer hash, so every engine picks the same member, scaled from its high
163
+ // bits — FNV-1a's low bits barely mix — so the pick is in range by
164
+ // construction. The part is in the key so a wrap's tail and lead, which share
165
+ // an index, are picked independently.
166
+ const pickGlyph = (cell, part, index, at) => {
167
+ const glyphs = glyphsOf(cell);
168
+ return glyphs[Math.floor((parseInt(fnv1a(`${part}:${index}:${at}`), 16) / 2 ** 32) * glyphs.length)];
169
+ };
105
170
  const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
106
171
  const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
107
172
  // `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
@@ -194,19 +259,22 @@ function paintableBg(bg) {
194
259
  function drawnGround(bg) {
195
260
  return bg.flattenAlpha(SURFACE_BLACK);
196
261
  }
197
- // A seam the eye cannot tell from the ground it is drawn on. What is measured
198
- // is what is drawn — `Style.drawnColors`, the colours the writer encodes: the
199
- // left ground flattened onto the right and the right onto the terminal's
200
- // black, both downgraded to the depth the render encodes at, so two grounds
201
- // 256 colours round to one cube entry are the one entry they render as. A
202
- // colour whose RGB is the terminal theme's own (ANSI 0–15, a blend reaching
203
- // one) has no value here, so two of them are the same only when they are one
204
- // expression: the same palette slot — its `number`, never the name or the
205
- // depth that spells it ("red", "color(1)", an EIGHT_BIT spec on 0–15 are one
206
- // slot) — or the same blend, whose name spells each end by its slot for the
207
- // same reason.
208
- function vanishes(leftBg, rightBg, colorSystem) {
209
- const { color, bgcolor } = new Style({ color: drawnGround(leftBg), bgcolor: rightBg }).drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
262
+ // A cell whose ink the eye cannot tell from its ground, read off the style it
263
+ // is drawn in. What is measured is what is drawn — `Style.drawnColors`, the
264
+ // colours the writer encodes: the ink flattened onto the cell's own ground and
265
+ // the ground onto the terminal's black, both downgraded to the depth the
266
+ // render encodes at, so two colours that depth rounds to one entry are the one
267
+ // entry they render as. A colour whose RGB is the terminal theme's own (ANSI
268
+ // 0–15, a blend reaching one) has no value here, so two of them are the same
269
+ // only when they are one expression: the same palette slot — its `number`,
270
+ // never the name or the depth that spells it ("red", "color(1)", an EIGHT_BIT
271
+ // spec on 0–15 are one slot) — or the same blend, whose name spells each end
272
+ // by its slot for the same reason. A cell on the terminal's ground, or in
273
+ // reverse video over it, has a colour nobody knows on one side, and is seen.
274
+ function vanishes(style, colorSystem) {
275
+ if (style.reverse)
276
+ return false;
277
+ const { color, bgcolor } = style.drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
210
278
  if (color === undefined || bgcolor === undefined)
211
279
  return false;
212
280
  const av = color.fixedValue;
@@ -248,8 +316,8 @@ const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyph
248
316
  * Which part a join draws is read off its two neighbours. A neighbour is
249
317
  * missing at the strip's ends, and counts as missing — an endpoint — when the
250
318
  * shape paints with its ground anywhere and it has none to paint. With both present the
251
- * join is the seam, or the divider where the two grounds are drawn as one
252
- * colour at `options.colorSystem`; with only the right, the lead; with only
319
+ * join is the seam, or the divider where every seam cell's ink is drawn
320
+ * indistinct from its ground at `options.colorSystem`; with only the right, the lead; with only
253
321
  * the left, the tail; with neither, nothing.
254
322
  *
255
323
  * `ascii` is the shape an ASCII-only output draws instead, whole.
@@ -263,7 +331,7 @@ export class CellJoiner {
263
331
  this.ascii = parseShape(ascii, "ascii");
264
332
  this._forms = { glyph: formOf(this.shape), ascii: formOf(this.ascii) };
265
333
  }
266
- join(left, right) {
334
+ join(left, right, { index }) {
267
335
  const forms = this._forms;
268
336
  return {
269
337
  *render(options) {
@@ -273,63 +341,119 @@ export class CellJoiner {
273
341
  const rightBg = paintableBg(edges.right?.bgcolor);
274
342
  const hasLeft = left !== null && (!reads.left || leftBg !== undefined);
275
343
  const hasRight = right !== null && (!reads.right || rightBg !== undefined);
276
- const divides = drawn.divider !== drawn.seam && leftBg !== undefined && rightBg !== undefined && vanishes(leftBg, rightBg, options.colorSystem);
277
- const cells = hasLeft && hasRight ? (divides ? drawn.divider : drawn.seam) : hasRight ? drawn.lead : hasLeft ? drawn.tail : [];
278
- // A oneOf draws its first member until a join knows its seam index
279
- // (rich-joiners-9ntp.bex).
280
- for (const cell of cells)
281
- yield new Segment(glyphsOf(cell)[0], cellStyle(cell, edges, options));
344
+ const neighbours = { left, right };
345
+ // On screen a cell's ink is its `fg` paint and its ground its `bg`,
346
+ // reverse video or not (`cellStyle`).
347
+ const ownerOf = (color) => (color !== "ground" && "edge" in color ? neighbours[color.edge] : null);
348
+ const styled = (part) => drawn[part].map((cell, at) => {
349
+ const owners = new ColorOwners(ownerOf(cell.fg), ownerOf(cell.bg));
350
+ return new Segment(pickGlyph(cell, part, index, at), cellStyle(cell, edges, options).withAnchor({ owner: owners, row: 0, col: 0, inner: undefined }));
351
+ });
352
+ if (!hasLeft || !hasRight) {
353
+ if (hasLeft || hasRight)
354
+ yield* styled(hasRight ? "lead" : "tail");
355
+ return;
356
+ }
357
+ // A seam is lost only when every cell of it is: one cell rounded into
358
+ // its ground at a shallow depth still leaves the others to show the
359
+ // step, and measuring per cell, not across the two grounds, divides a
360
+ // seam stepping in thirds where each third would not be seen.
361
+ const seam = styled("seam");
362
+ const lost = drawn.divider !== drawn.seam && seam.every((segment) => segment.style !== undefined && vanishes(segment.style, options.colorSystem));
363
+ yield* lost ? styled("divider") : seam;
282
364
  },
283
365
  };
284
366
  }
285
367
  }
286
- // --- The four joiners as shapes ---
368
+ // --- The bundled joiners ---
287
369
  const LEFT_BG = { edge: "left", part: "bg" };
288
370
  const RIGHT_BG = { edge: "right", part: "bg" };
289
371
  const LEFT_FG = { edge: "left", part: "fg" };
372
+ const HALF = { mix: [LEFT_BG, RIGHT_BG, 1 / 2] };
290
373
  const GROUND = "ground";
291
- /**
292
- * The powerline joiner: U+E0B0 (right-arrow) in the left ground over the
293
- * right, divided by U+E0B1 (thin right-arrow) in the left item's text. The
294
- * lead is the arrow with the left ground replaced by the terminal's — drawn
295
- * in reverse video it leaves a notch whose base is the cell's left edge, inked
296
- * in the terminal's background colour. That ink is opaque, so under a
297
- * translucent terminal the notch does not let what is behind it through. The
298
- * tail is the arrow over the terminal's ground.
299
- * ASCII draws `>` everywhere and begins each run flat.
300
- */
301
- export const POWERLINE_JOINER = new CellJoiner({
302
- seam: [{ glyph: "", fg: LEFT_BG, bg: RIGHT_BG }],
303
- divider: [{ glyph: "", fg: LEFT_FG, bg: LEFT_BG }],
304
- lead: [{ glyph: "", fg: GROUND, bg: RIGHT_BG }],
305
- tail: [{ glyph: "", fg: LEFT_BG, bg: GROUND }],
306
- }, {
307
- seam: [{ glyph: ">", fg: LEFT_BG, bg: RIGHT_BG }],
308
- divider: [{ glyph: ">", fg: LEFT_FG, bg: LEFT_BG }],
309
- lead: [],
310
- tail: [{ glyph: ">", fg: LEFT_BG, bg: GROUND }],
311
- });
312
- function capsule(open, close) {
313
- const seam = [
314
- { glyph: close, fg: LEFT_BG, bg: GROUND },
315
- { glyph: " ", fg: LEFT_FG, bg: GROUND },
316
- { glyph: open, fg: RIGHT_BG, bg: GROUND },
317
- ];
374
+ // A one-cell edge in the powerline family: `glyph` in the left ground over the
375
+ // right; divided by `thin` in the left item's text; led by the same glyph with
376
+ // the left ground replaced by the terminal's — in reverse video, a notch whose
377
+ // ink is the terminal's background — and tailed by it over the terminal's.
378
+ function edge(glyph, thin) {
379
+ return {
380
+ seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
381
+ divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
382
+ lead: [{ glyph, fg: GROUND, bg: RIGHT_BG }],
383
+ tail: [{ glyph, fg: LEFT_BG, bg: GROUND }],
384
+ };
385
+ }
386
+ // Three powerline arrows stepping through the two thirds between the grounds.
387
+ function prism(glyph, thin) {
388
+ const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
389
+ const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
390
+ return {
391
+ ...edge(glyph, thin),
392
+ seam: [
393
+ { glyph, fg: LEFT_BG, bg: third(1 / 3) },
394
+ { glyph, fg: third(1 / 3), bg: third(2 / 3) },
395
+ { glyph, fg: third(2 / 3), bg: RIGHT_BG },
396
+ ],
397
+ divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
398
+ };
399
+ }
400
+ // A powerline-family joiner: `shape` over `glyph`, its ASCII form the same
401
+ // shape over `ascii`, beginning each run flat — an ASCII arrow reversed out of
402
+ // the terminal's ground is a block of nothing.
403
+ const arrows = (shape, glyph, thin, ascii, asciiThin) => new CellJoiner(shape(glyph, thin), { ...shape(ascii, asciiThin), lead: [] });
404
+ // Each run closed by `close` and opened by `open`, both in its ground over the
405
+ // terminal's, with `between` — cells on the terminal's ground — between runs.
406
+ function pill(open, close, between) {
407
+ const seam = [{ glyph: close, fg: LEFT_BG, bg: GROUND }, ...between, { glyph: open, fg: RIGHT_BG, bg: GROUND }];
318
408
  return { seam, divider: seam, lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }], tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }] };
319
409
  }
320
- /**
321
- * Rounded pills: each run opened by U+E0B6 and closed by U+E0B4, both in its
322
- * ground over the terminal's, a space between pills. ASCII draws `(` and `)`.
323
- */
324
- export const CAPSULE_JOINER = new CellJoiner(capsule("", ""), capsule("(", ")"));
410
+ const capsule = (open, close) => pill(open, close, [{ glyph: " ", fg: LEFT_FG, bg: GROUND }]);
411
+ const metro = (open, close, rail) => pill(open, close, [{ glyph: rail, fg: { role: "strip.rail" }, bg: GROUND }]);
412
+ // One cell drawn over the two grounds' midpoint, so it reads as neither item's;
413
+ // nothing at the ends, where there is no second ground to meet.
414
+ function midpoint(glyph, fg) {
415
+ const seam = [{ glyph, fg, bg: HALF }];
416
+ return { seam, divider: seam, lead: [], tail: [] };
417
+ }
418
+ // A ragged tear in the left ground over the midpoint; where that ink would
419
+ // vanish into the midpoint — grounds a few steps apart, or one ground — the
420
+ // same tear in the left item's text.
421
+ function torn(glyphs) {
422
+ return { ...midpoint({ oneOf: glyphs }, LEFT_BG), divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
423
+ }
325
424
  const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
326
425
  const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM, lead: [], tail: [] };
327
426
  /**
328
- * ` | ` between every pair of items, in the theme's `strip.separator` colour;
329
- * nothing at the ends. Its shape names no neighbour, so it joins items with no
330
- * ground too.
427
+ * The bundled joiners, by name. What each draws is `docs/strip.md`'s
428
+ * "Built-in joiners"; a name from config is read with `joinerNamed`.
331
429
  */
332
- export const PLAIN_JOINER = new CellJoiner(PLAIN, PLAIN);
430
+ export const JOINERS = Object.freeze({
431
+ powerline: arrows(edge, "\ue0b0", "\ue0b1", ">", ">"),
432
+ prism: arrows(prism, "\ue0b0", "\ue0b1", ">", ">"),
433
+ slant: arrows(edge, "\ue0bc", "\ue0bd", "/", "/"),
434
+ backslant: arrows(edge, "\ue0b8", "\ue0b9", "\\", "\\"),
435
+ flame: arrows(edge, "\ue0c0", "\ue0c1", "}", "}"),
436
+ pixel: arrows(edge, "\ue0c4", "\ue0b1", "#", ">"),
437
+ ice: arrows(edge, "\ue0c8", "\ue0b1", "]", ">"),
438
+ bubble: arrows(edge, "\ue0b4", "\ue0b5", ")", ")"),
439
+ capsule: new CellJoiner(capsule("\ue0b6", "\ue0b4"), capsule("(", ")")),
440
+ wrapper: new CellJoiner(pill("\ue0b2", "\ue0b0", []), pill("<", ">", [])),
441
+ metro: new CellJoiner(metro("\ue0b6", "\ue0b4", "\u2500\u2500"), metro("(", ")", "--")),
442
+ stitch: new CellJoiner(midpoint("\u250a", LEFT_FG), midpoint(":", LEFT_FG)),
443
+ torn: new CellJoiner(torn(["\u259a\u259e", "\u259e\u259a", "\u2598\u2597", "\u259d\u2596", "\u2596\u259d", "\u2597\u2598"]), torn(["/\\", "\\/"])),
444
+ plain: new CellJoiner(PLAIN, PLAIN),
445
+ });
446
+ /**
447
+ * The bundled joiner `name` names — a name read from config, parsed. Throws on
448
+ * any other string, naming the ones there are; an own-key test, so `toString`
449
+ * and the rest of `Object.prototype` are not joiners.
450
+ */
451
+ export function joinerNamed(name) {
452
+ if (!isJoinerName(name))
453
+ throw new Error(`no joiner named ${JSON.stringify(name)}; the joiners are ${Object.keys(JOINERS).join(", ")}`);
454
+ return JOINERS[name];
455
+ }
456
+ const isJoinerName = (name) => Object.hasOwn(JOINERS, name);
333
457
  function gradient(steps, glyph) {
334
458
  // Midpoint sampling across `2 * steps` half-cell positions: cell i takes
335
459
  // samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
@@ -646,6 +646,7 @@ export const DEFAULT_STYLES = {
646
646
  "rule.text": NULL_STYLE,
647
647
  // Strip
648
648
  "strip.separator": Style.parse("bright_black"),
649
+ "strip.rail": Style.parse("bright_black"),
649
650
  // JSON
650
651
  "json.brace": Style.parse("bold"),
651
652
  "json.bool_true": Style.parse("italic bright_green"),
@@ -16,8 +16,9 @@
16
16
  *
17
17
  * Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
18
18
  * CSI sequence). When the buffer is drained but ends with a lone ESC, the
19
- * parser defers emission via setTimeout(0). The next chunk cancels the timer
20
- * if it extends the sequence. `flush()` drains it synchronously.
19
+ * parser defers emission via setTimeout(0). A next chunk opening with `[` or
20
+ * `O` extends the sequence; any other next chunk emits the ESC as Escape
21
+ * first. `flush()` drains it synchronously.
21
22
  */
22
23
  /** A key as the terminal reported it: plain data, with no say over who hears it. */
23
24
  export interface KeyPress {
@@ -73,6 +74,8 @@ export declare class InputParser {
73
74
  flush(): void;
74
75
  /** Drop what is buffered and any pending lone ESC: the parser as constructed. */
75
76
  reset(): void;
77
+ /** The parser's one hold: a lone ESC that may yet open a sequence. */
78
+ private holdingEscape;
76
79
  private cancelEscape;
77
80
  private drain;
78
81
  private consumeOne;