@promptctl/rich-js 0.19.0 → 0.21.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.
- package/README.md +3 -2
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +10 -0
- package/dist/core/color.d.ts +1 -0
- package/dist/core/color.js +14 -3
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/paint.js +13 -2
- package/dist/core/pretty.d.ts +23 -2
- package/dist/core/pretty.js +54 -18
- package/dist/core/protocol.d.ts +2 -0
- package/dist/core/protocol.js +3 -2
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +159 -76
- package/dist/core/strip.js +367 -215
- package/dist/core/style.js +10 -7
- package/dist/host/app.d.ts +68 -2
- package/dist/host/app.js +125 -15
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +87 -0
- package/dist/host/input.js +517 -0
- package/dist/host/terminal-host.d.ts +12 -0
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +7 -4
- package/dist/node/terminal-host.d.ts +7 -0
- package/dist/node/terminal-host.js +7 -0
- package/dist/renderables/effect.d.ts +89 -16
- package/dist/renderables/effect.js +115 -42
- package/dist/renderables/effects.d.ts +260 -0
- package/dist/renderables/effects.js +596 -0
- package/dist/renderables/embed.d.ts +11 -5
- package/dist/renderables/embed.js +13 -6
- package/dist/renderables/flexStrip.d.ts +1 -1
- package/dist/renderables/flexStrip.js +10 -9
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/progress.d.ts +6 -0
- package/dist/renderables/progress.js +11 -4
- package/dist/renderables/progressBar.d.ts +22 -2
- package/dist/renderables/progressBar.js +52 -7
- package/dist/renderables/rule.d.ts +2 -2
- package/dist/renderables/rule.js +12 -10
- package/dist/renderables/table.js +5 -7
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +59 -3
- package/dist/widgets/dropdown.js +3 -3
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/hit.js +2 -4
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +22 -7
- package/dist/widgets/widget-app.js +22 -12
- package/package.json +26 -18
package/dist/core/strip.js
CHANGED
|
@@ -29,7 +29,66 @@ import { Segment } from "./segment.js";
|
|
|
29
29
|
import { Style } from "./style.js";
|
|
30
30
|
import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
|
|
31
31
|
import { Oklch } from "./oklch.js";
|
|
32
|
-
import {
|
|
32
|
+
import { cellLen } from "./cells.js";
|
|
33
|
+
import { fnv1a } from "./fnv1a.js";
|
|
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
|
+
}
|
|
33
92
|
// --- Strip ---
|
|
34
93
|
export class Strip {
|
|
35
94
|
items;
|
|
@@ -51,12 +110,12 @@ export class Strip {
|
|
|
51
110
|
// `left` fills the canvas, and the first item would pad over the rest of
|
|
52
111
|
// the row. Where the row sits in the width is its container's to say.
|
|
53
112
|
const row = { ...options, justify: undefined };
|
|
54
|
-
yield* this.joiner
|
|
113
|
+
yield* drawJoin(this.joiner, null, items[0], { index: 0 }).render(row);
|
|
55
114
|
for (let i = 0; i < items.length; i++) {
|
|
56
115
|
const item = items[i];
|
|
57
|
-
yield* item.render(row);
|
|
116
|
+
yield* drawItem(item).render(row);
|
|
58
117
|
const next = i + 1 < items.length ? items[i + 1] : null;
|
|
59
|
-
yield* this.joiner
|
|
118
|
+
yield* drawJoin(this.joiner, item, next, { index: i + 1 }).render(row);
|
|
60
119
|
}
|
|
61
120
|
// [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
|
|
62
121
|
// it emits, including the last, and the other block-level renderables
|
|
@@ -84,65 +143,138 @@ export class Strip {
|
|
|
84
143
|
return { minimum: width, maximum: width };
|
|
85
144
|
}
|
|
86
145
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
const PARTS = ["seam", "divider", "lead", "tail"];
|
|
147
|
+
/**
|
|
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
|
|
151
|
+
* one cell wide.
|
|
152
|
+
*/
|
|
153
|
+
export const SEAM_MIN_DELTA_E = 0.04;
|
|
154
|
+
function* paintsIn(paint) {
|
|
155
|
+
yield paint;
|
|
156
|
+
if ("mix" in paint) {
|
|
157
|
+
yield* paintsIn(paint.mix[0]);
|
|
158
|
+
yield* paintsIn(paint.mix[1]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
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)];
|
|
92
169
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
170
|
+
const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
|
|
171
|
+
const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
|
|
172
|
+
// `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
|
|
173
|
+
// name, because the part is also drawn where that neighbour does not exist.
|
|
174
|
+
function parsePaint(raw, absent, refuse) {
|
|
175
|
+
if (raw === "ground")
|
|
176
|
+
return refuse("mixes ground, whose colour is unknown");
|
|
177
|
+
const p = (typeof raw === "object" && raw !== null ? raw : {});
|
|
178
|
+
if ((p.edge === "left" || p.edge === "right") && (p.part === "bg" || p.part === "fg")) {
|
|
179
|
+
if (p.edge === absent)
|
|
180
|
+
refuse(`names the ${absent} neighbour, which this part is drawn without`);
|
|
181
|
+
return Object.freeze({ edge: p.edge, part: p.part });
|
|
182
|
+
}
|
|
183
|
+
if (typeof p.role === "string")
|
|
184
|
+
return Object.freeze({ role: p.role });
|
|
185
|
+
if (Array.isArray(p.mix) && p.mix.length === 3) {
|
|
186
|
+
const [from, to, t] = p.mix;
|
|
187
|
+
if (typeof t !== "number" || !(t >= 0 && t <= 1))
|
|
188
|
+
refuse(`mixes at ${String(t)}, outside 0..1`);
|
|
189
|
+
return Object.freeze({ mix: Object.freeze([parsePaint(from, absent, refuse), parsePaint(to, absent, refuse), t]) });
|
|
190
|
+
}
|
|
191
|
+
return refuse(`has a colour that is not a Paint: ${JSON.stringify(raw)}`);
|
|
96
192
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
193
|
+
const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
|
|
194
|
+
const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
|
|
195
|
+
function parseList(raw, where) {
|
|
196
|
+
if (!Array.isArray(raw))
|
|
197
|
+
throw new Error(`CellJoiner: ${where} is not a list of cells`);
|
|
198
|
+
return raw.map((cell, i) => {
|
|
199
|
+
if (typeof cell !== "object" || cell === null)
|
|
200
|
+
throw new Error(`CellJoiner: ${where}[${i}] is not a cell`);
|
|
201
|
+
return cell;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* [LAW:parse-dont-validate] The one place a shape is checked, so every rule
|
|
206
|
+
* the cell model states is refused at construction and the render reads a
|
|
207
|
+
* shape that already holds them. A shape is often authored as data — a
|
|
208
|
+
* consumer's config — so the rules the types state are checked here too, and
|
|
209
|
+
* what comes back is a frozen copy the caller can no longer change.
|
|
210
|
+
*/
|
|
211
|
+
function parseShape(raw, form) {
|
|
212
|
+
const parseCells = (part) => Object.freeze(parseList(raw[part], `${form} ${part}`).map((cell, i) => {
|
|
213
|
+
const refuse = (why) => {
|
|
214
|
+
throw new Error(`CellJoiner: ${form} ${part}[${i}] ${why}`);
|
|
215
|
+
};
|
|
216
|
+
const glyph = cell.glyph;
|
|
217
|
+
const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
|
|
218
|
+
if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
|
|
219
|
+
refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
|
|
220
|
+
}
|
|
221
|
+
if (cell.fg === "ground" && cell.bg === "ground")
|
|
222
|
+
refuse("has ground on both sides");
|
|
223
|
+
const glyphs = glyphsOf(cell);
|
|
224
|
+
if (glyphs.length === 0)
|
|
225
|
+
refuse("has an empty oneOf");
|
|
226
|
+
if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
|
|
227
|
+
refuse("has oneOf members of different widths");
|
|
228
|
+
if (form === "ascii" && !glyphs.every(isAscii))
|
|
229
|
+
refuse("is not ASCII");
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
|
|
232
|
+
fg: parseColor(cell.fg, ABSENT[part], refuse),
|
|
233
|
+
bg: parseColor(cell.bg, ABSENT[part], refuse),
|
|
234
|
+
});
|
|
235
|
+
}));
|
|
236
|
+
const seam = parseCells("seam");
|
|
237
|
+
// A divider authored as the seam itself stays the seam, so a render can see
|
|
238
|
+
// it has no choice to make.
|
|
239
|
+
const divider = raw.divider === raw.seam ? seam : parseCells("divider");
|
|
240
|
+
if (partWidth(divider) !== partWidth(seam))
|
|
241
|
+
throw new Error(`CellJoiner: ${form} divider is not the width of the seam`);
|
|
242
|
+
return Object.freeze({ seam, divider, lead: parseCells("lead"), tail: parseCells("tail") });
|
|
243
|
+
}
|
|
244
|
+
// Whether any cell of `shape` paints with `side`'s ground.
|
|
245
|
+
const readsGround = (shape, side) => PARTS.flatMap((part) => shape[part]).some((cell) => [cell.fg, cell.bg].some((color) => color !== "ground" && [...paintsIn(color)].some((p) => "edge" in p && p.edge === side && p.part === "bg")));
|
|
246
|
+
function formOf(shape) {
|
|
247
|
+
return { shape, reads: { left: readsGround(shape, "left"), right: readsGround(shape, "right") } };
|
|
103
248
|
}
|
|
104
249
|
// A bg with a real colour to paint. `undefined` (no bg) and the terminal
|
|
105
250
|
// DEFAULT colour (transparent — identical to the terminal background) are the
|
|
106
|
-
// two representations of "nothing to paint"
|
|
107
|
-
//
|
|
251
|
+
// two representations of "nothing to paint", so an explicit `… on default`
|
|
252
|
+
// cannot smuggle a seam.
|
|
108
253
|
function paintableBg(bg) {
|
|
109
254
|
return bg !== undefined && !bg.isDefault ? bg : undefined;
|
|
110
255
|
}
|
|
111
|
-
// A
|
|
112
|
-
//
|
|
113
|
-
//
|
|
256
|
+
// A neighbour's ground as the writer draws it (`Style.drawnColors`, onto the
|
|
257
|
+
// terminal's black), so a translucent ground is not composited a second time
|
|
258
|
+
// over whatever a join cell enters.
|
|
114
259
|
function drawnGround(bg) {
|
|
115
260
|
return bg.flattenAlpha(SURFACE_BLACK);
|
|
116
261
|
}
|
|
117
|
-
// A
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// measured is what is drawn — `Style.drawnColors`, the colours the writer
|
|
134
|
-
// encodes: the arrow's colour flattened onto its ground and the ground onto
|
|
135
|
-
// the terminal's black, both downgraded to the depth the render encodes at,
|
|
136
|
-
// so two grounds 256 colours round to one cube entry are the one entry they
|
|
137
|
-
// render as. A colour whose RGB is the terminal theme's own (ANSI 0–15, a
|
|
138
|
-
// blend reaching one) has no value here, so two of them are the same only
|
|
139
|
-
// when they are one expression: the same palette slot — its `number`, never
|
|
140
|
-
// the name or the depth that spells it ("red", "color(1)", an EIGHT_BIT spec
|
|
141
|
-
// on 0–15 are one slot) — or the same blend, whose name spells each end by its
|
|
142
|
-
// slot for the same reason.
|
|
143
|
-
function vanishes(arrow, colorSystem) {
|
|
144
|
-
// No colour emitted draws nothing to tell apart; measure what was handed.
|
|
145
|
-
const { color, bgcolor } = arrow.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);
|
|
146
278
|
if (color === undefined || bgcolor === undefined)
|
|
147
279
|
return false;
|
|
148
280
|
const av = color.fixedValue;
|
|
@@ -151,176 +283,196 @@ function vanishes(arrow, colorSystem) {
|
|
|
151
283
|
? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
|
|
152
284
|
: color.number !== undefined ? color.number === bgcolor.number : color.name === bgcolor.name;
|
|
153
285
|
}
|
|
286
|
+
function resolvePaint(paint, edges, options) {
|
|
287
|
+
if ("edge" in paint) {
|
|
288
|
+
const edge = edges[paint.edge];
|
|
289
|
+
if (paint.part === "fg")
|
|
290
|
+
return edge?.color;
|
|
291
|
+
const bg = paintableBg(edge?.bgcolor);
|
|
292
|
+
return bg && drawnGround(bg);
|
|
293
|
+
}
|
|
294
|
+
if ("role" in paint)
|
|
295
|
+
return getStyle(options, paint.role).color;
|
|
296
|
+
const [from, to, t] = paint.mix;
|
|
297
|
+
const a = resolvePaint(from, edges, options);
|
|
298
|
+
const b = resolvePaint(to, edges, options);
|
|
299
|
+
// A mix with no colour at one end has nothing to blend toward.
|
|
300
|
+
return a && b && ColorSpec.blend(a, b, t);
|
|
301
|
+
}
|
|
302
|
+
function cellStyle(cell, edges, options) {
|
|
303
|
+
// [LAW:dataflow-not-control-flow] "ground" is the absence of a colour on its
|
|
304
|
+
// side; as the fg it is reverse video over the bg colour, so the glyph's ink
|
|
305
|
+
// is the terminal's own background.
|
|
306
|
+
if (cell.fg === "ground")
|
|
307
|
+
return new Style({ color: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options), reverse: true });
|
|
308
|
+
const color = resolvePaint(cell.fg, edges, options);
|
|
309
|
+
return new Style({ color, bgcolor: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options) });
|
|
310
|
+
}
|
|
311
|
+
const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
|
|
154
312
|
/**
|
|
155
|
-
* The
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* the
|
|
160
|
-
* with
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
313
|
+
* The one joiner: what it draws at each join is the `JoinShape` it is given,
|
|
314
|
+
* so a new joiner is a value, never a class.
|
|
315
|
+
*
|
|
316
|
+
* Which part a join draws is read off its two neighbours. A neighbour is
|
|
317
|
+
* missing at the strip's ends, and counts as missing — an endpoint — when the
|
|
318
|
+
* shape paints with its ground anywhere and it has none to paint. With both present the
|
|
319
|
+
* join is the seam, or the divider where every seam cell's ink is drawn
|
|
320
|
+
* indistinct from its ground at `options.colorSystem`; with only the right, the lead; with only
|
|
321
|
+
* the left, the tail; with neither, nothing.
|
|
322
|
+
*
|
|
323
|
+
* `ascii` is the shape an ASCII-only output draws instead, whole.
|
|
164
324
|
*/
|
|
165
|
-
export
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
glyph: ">",
|
|
174
|
-
divider: ">",
|
|
175
|
-
lead: "",
|
|
176
|
-
tail: ">",
|
|
177
|
-
});
|
|
178
|
-
const powerlineGlyphs = ({ glyph, divider, lead, tail }) => glyph + divider + lead + tail;
|
|
179
|
-
export class PowerlineJoiner {
|
|
180
|
-
_glyphs;
|
|
181
|
-
constructor(options = POWERLINE_JOINER_GLYPHS) {
|
|
182
|
-
this._glyphs = options;
|
|
325
|
+
export class CellJoiner {
|
|
326
|
+
shape;
|
|
327
|
+
ascii;
|
|
328
|
+
_forms;
|
|
329
|
+
constructor(shape, ascii) {
|
|
330
|
+
this.shape = parseShape(shape, "glyph");
|
|
331
|
+
this.ascii = parseShape(ascii, "ascii");
|
|
332
|
+
this._forms = { glyph: formOf(this.shape), ascii: formOf(this.ascii) };
|
|
183
333
|
}
|
|
184
|
-
join(left, right) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (leftBg === undefined) {
|
|
217
|
-
if (rightBg !== undefined)
|
|
218
|
-
yield* cap(lead, rightBg);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
if (rightBg === undefined) {
|
|
222
|
-
yield* cap(tail, leftBg);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const arrow = new Style({ color: drawnGround(leftBg), bgcolor: rightBg });
|
|
226
|
-
// The divider is the left item's text on the left item's own ground,
|
|
227
|
-
// so it reads exactly as well as that item's text does, at any depth.
|
|
228
|
-
yield vanishes(arrow, options.colorSystem)
|
|
229
|
-
? new Segment(divider, new Style({ color: leftEdge?.color, bgcolor: leftBg }))
|
|
230
|
-
: new Segment(glyph, arrow);
|
|
231
|
-
});
|
|
334
|
+
join(left, right, { index }) {
|
|
335
|
+
const forms = this._forms;
|
|
336
|
+
return {
|
|
337
|
+
*render(options) {
|
|
338
|
+
const { shape: drawn, reads } = drawable(options, forms.glyph, forms.ascii, (form) => shapeGlyphs(form.shape));
|
|
339
|
+
const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
|
|
340
|
+
const leftBg = paintableBg(edges.left?.bgcolor);
|
|
341
|
+
const rightBg = paintableBg(edges.right?.bgcolor);
|
|
342
|
+
const hasLeft = left !== null && (!reads.left || leftBg !== undefined);
|
|
343
|
+
const hasRight = right !== null && (!reads.right || rightBg !== undefined);
|
|
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;
|
|
364
|
+
},
|
|
365
|
+
};
|
|
232
366
|
}
|
|
233
367
|
}
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
if (right === null) {
|
|
253
|
-
yield new Segment(caps.right, bgAsFg(left.edgeStyle("right", options)));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
// Middle: close the left capsule, separator (unstyled), open the right.
|
|
257
|
-
yield new Segment(caps.right, bgAsFg(left.edgeStyle("right", options)));
|
|
258
|
-
if (separator.length > 0)
|
|
259
|
-
yield new Segment(separator);
|
|
260
|
-
yield new Segment(caps.left, bgAsFg(right.edgeStyle("left", options)));
|
|
261
|
-
}
|
|
262
|
-
join(left, right) {
|
|
263
|
-
return deferred((options) => this._emit(left, right, options));
|
|
264
|
-
}
|
|
368
|
+
// --- The bundled joiners ---
|
|
369
|
+
const LEFT_BG = { edge: "left", part: "bg" };
|
|
370
|
+
const RIGHT_BG = { edge: "right", part: "bg" };
|
|
371
|
+
const LEFT_FG = { edge: "left", part: "fg" };
|
|
372
|
+
const HALF = { mix: [LEFT_BG, RIGHT_BG, 1 / 2] };
|
|
373
|
+
const GROUND = "ground";
|
|
374
|
+
// A one-cell edge in the powerline family: `glyph` in the left ground over the
|
|
375
|
+
// right; divided by `thin` in the left item's text; led by the same glyph with
|
|
376
|
+
// the left ground replaced by the terminal's — in reverse video, a notch whose
|
|
377
|
+
// ink is the terminal's background — and tailed by it over the terminal's.
|
|
378
|
+
function edge(glyph, thin) {
|
|
379
|
+
return {
|
|
380
|
+
seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
|
|
381
|
+
divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
|
|
382
|
+
lead: [{ glyph, fg: GROUND, bg: RIGHT_BG }],
|
|
383
|
+
tail: [{ glyph, fg: LEFT_BG, bg: GROUND }],
|
|
384
|
+
};
|
|
265
385
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 }];
|
|
408
|
+
return { seam, divider: seam, lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }], tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }] };
|
|
281
409
|
}
|
|
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
|
+
}
|
|
424
|
+
const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
|
|
425
|
+
const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM, lead: [], tail: [] };
|
|
282
426
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* two colour samples — `2 * steps` samples in `steps` cells — so the gradient
|
|
286
|
-
* looks twice as smooth as one-colour-per-cell at the same width.
|
|
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`.
|
|
287
429
|
*/
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
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);
|
|
457
|
+
function gradient(steps, glyph) {
|
|
458
|
+
// Midpoint sampling across `2 * steps` half-cell positions: cell i takes
|
|
459
|
+
// samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
|
|
460
|
+
// so no sample equals either neighbour's ground.
|
|
461
|
+
const samples = 2 * steps;
|
|
462
|
+
const seam = Array.from({ length: steps }, (_, i) => ({
|
|
463
|
+
glyph,
|
|
464
|
+
fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
|
|
465
|
+
bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
|
|
466
|
+
}));
|
|
467
|
+
return { seam, divider: seam, lead: [], tail: [] };
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* A ramp of `steps` cells from the left ground to the right. Each cell is the
|
|
471
|
+
* half block U+258C, ink on its left half and ground on its right, so it
|
|
472
|
+
* carries two samples; ASCII draws a space, keeping the grounds alone. Nothing
|
|
473
|
+
* at the ends, or beside a neighbour with no ground — there is no colour to
|
|
474
|
+
* ramp toward.
|
|
475
|
+
*/
|
|
476
|
+
export function gradientJoiner(steps = 4) {
|
|
477
|
+
return new CellJoiner(gradient(steps, "▌"), gradient(steps, " "));
|
|
326
478
|
}
|