@promptctl/rich-js 0.23.0 → 0.24.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/dist/core/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/easing.d.ts +31 -0
- package/dist/core/easing.js +106 -1
- package/dist/core/segment.d.ts +10 -0
- package/dist/core/segment.js +15 -0
- package/dist/core/strip.d.ts +104 -27
- package/dist/core/strip.js +231 -124
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +32 -3
- package/dist/core/text.js +4 -1
- package/dist/host/app.js +2 -4
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +5 -4
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/effect.d.ts +78 -10
- package/dist/renderables/effect.js +143 -39
- package/dist/renderables/effects.d.ts +73 -26
- package/dist/renderables/effects.js +412 -92
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +15 -7
- package/dist/renderables/frames.d.ts +95 -0
- package/dist/renderables/frames.js +106 -0
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +68 -34
- package/dist/renderables/markdown.d.ts +5 -2
- package/dist/renderables/markdown.js +210 -19
- package/dist/renderables/progress.d.ts +1 -1
- package/dist/renderables/progress.js +3 -1
- package/dist/renderables/progressBar.js +3 -3
- package/dist/renderables/spinner.d.ts +62 -28
- package/dist/renderables/spinner.js +99 -52
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +6 -2
- package/package.json +1 -1
package/dist/core/strip.js
CHANGED
|
@@ -194,59 +194,106 @@ function parsePaint(raw, absent, refuse) {
|
|
|
194
194
|
}
|
|
195
195
|
const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
|
|
196
196
|
const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
// 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" };
|
|
199
|
+
function parseCell(cell, form, part, refuse) {
|
|
200
|
+
const glyph = cell.glyph;
|
|
201
|
+
const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
|
|
202
|
+
if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
|
|
203
|
+
refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
|
|
204
|
+
}
|
|
205
|
+
if (cell.fg === "ground" && cell.bg === "ground")
|
|
206
|
+
refuse("has ground on both sides");
|
|
207
|
+
const glyphs = glyphsOf(cell);
|
|
208
|
+
if (glyphs.length === 0)
|
|
209
|
+
refuse("has an empty oneOf");
|
|
210
|
+
if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
|
|
211
|
+
refuse("has oneOf members of different widths");
|
|
212
|
+
if (form === "ascii" && !glyphs.every(isAscii))
|
|
213
|
+
refuse("is not ASCII");
|
|
214
|
+
return Object.freeze({
|
|
215
|
+
glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
|
|
216
|
+
fg: parseColor(cell.fg, ABSENT[part], refuse),
|
|
217
|
+
bg: parseColor(cell.bg, ABSENT[part], refuse),
|
|
204
218
|
});
|
|
205
219
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
// `raw[form][part]` as frozen cells, each checked by `parseOne`, or refused
|
|
221
|
+
// naming where it sits.
|
|
222
|
+
function parseCells(raw, form, part, parseOne) {
|
|
223
|
+
const forms = raw;
|
|
224
|
+
const where = AUTHORED_IN[part];
|
|
225
|
+
if (typeof forms?.[form] !== "object" || forms[form] === null)
|
|
226
|
+
throw new Error(`${where}: has no ${form} form`);
|
|
227
|
+
const cells = forms[form][part];
|
|
228
|
+
if (!Array.isArray(cells))
|
|
229
|
+
throw new Error(`${where}: ${form} ${part} is not a list of cells`);
|
|
230
|
+
return Object.freeze(cells.map((cell, i) => {
|
|
215
231
|
const refuse = (why) => {
|
|
216
|
-
throw new Error(
|
|
232
|
+
throw new Error(`${where}: ${form} ${part}[${i}] ${why}`);
|
|
217
233
|
};
|
|
218
|
-
|
|
219
|
-
const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
|
|
220
|
-
if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
|
|
221
|
-
refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
|
|
222
|
-
}
|
|
223
|
-
if (cell.fg === "ground" && cell.bg === "ground")
|
|
224
|
-
refuse("has ground on both sides");
|
|
225
|
-
const glyphs = glyphsOf(cell);
|
|
226
|
-
if (glyphs.length === 0)
|
|
227
|
-
refuse("has an empty oneOf");
|
|
228
|
-
if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
|
|
229
|
-
refuse("has oneOf members of different widths");
|
|
230
|
-
if (form === "ascii" && !glyphs.every(isAscii))
|
|
231
|
-
refuse("is not ASCII");
|
|
232
|
-
return Object.freeze({
|
|
233
|
-
glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
|
|
234
|
-
fg: parseColor(cell.fg, ABSENT[part], refuse),
|
|
235
|
-
bg: parseColor(cell.bg, ABSENT[part], refuse),
|
|
236
|
-
});
|
|
234
|
+
return typeof cell === "object" && cell !== null ? parseOne(cell, refuse) : refuse("is not a cell");
|
|
237
235
|
}));
|
|
238
|
-
const seam = parseCells("seam");
|
|
239
|
-
// A divider authored as the seam itself stays the seam, so a render can see
|
|
240
|
-
// it has no choice to make.
|
|
241
|
-
const divider = raw.divider === raw.seam ? seam : parseCells("divider");
|
|
242
|
-
if (partWidth(divider) !== partWidth(seam))
|
|
243
|
-
throw new Error(`CellJoiner: ${form} divider is not the width of the seam`);
|
|
244
|
-
return Object.freeze({ seam, divider, lead: parseCells("lead"), tail: parseCells("tail") });
|
|
245
236
|
}
|
|
246
|
-
|
|
247
|
-
const
|
|
237
|
+
const asCell = (form, part) => (cell, refuse) => parseCell(cell, form, part, refuse);
|
|
238
|
+
const asSeamCell = (form, part) => (cell, refuse) => {
|
|
239
|
+
if (!("cap" in cell))
|
|
240
|
+
return parseCell(cell, form, part, refuse);
|
|
241
|
+
const { cap } = cell;
|
|
242
|
+
if (Object.keys(cell).length !== 1)
|
|
243
|
+
refuse("names a cap and draws cells of its own; a cell is one or the other");
|
|
244
|
+
return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
|
|
245
|
+
};
|
|
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
|
+
/**
|
|
255
|
+
* [LAW:parse-dont-validate] The one place a seam is checked, so every rule
|
|
256
|
+
* the cell model states is refused at construction and the render reads cells
|
|
257
|
+
* that already hold them. A seam is often authored as data — a consumer's
|
|
258
|
+
* config — so the rules the types state are checked here too, and what comes
|
|
259
|
+
* back is a frozen copy the caller can no longer change.
|
|
260
|
+
*/
|
|
261
|
+
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
|
+
};
|
|
272
|
+
return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
|
|
273
|
+
}
|
|
274
|
+
/** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
|
|
275
|
+
function parseCaps(raw) {
|
|
276
|
+
const parseForm = (form) => Object.freeze({ lead: parseCells(raw, form, "lead", asCell(form, "lead")), tail: parseCells(raw, form, "tail", asCell(form, "tail")) });
|
|
277
|
+
return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
|
|
278
|
+
}
|
|
279
|
+
// One form of the seam joined with that form of the caps.
|
|
280
|
+
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 };
|
|
284
|
+
}
|
|
285
|
+
// Whether any of `cells` paints with `side`'s ground.
|
|
286
|
+
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")));
|
|
248
287
|
function formOf(shape) {
|
|
249
|
-
|
|
288
|
+
const between = [...shape.seam, ...shape.divider];
|
|
289
|
+
return {
|
|
290
|
+
shape,
|
|
291
|
+
reads: {
|
|
292
|
+
seam: { left: readsGround(between, "left"), right: readsGround(between, "right") },
|
|
293
|
+
lead: readsGround(shape.lead, "right"),
|
|
294
|
+
tail: readsGround(shape.tail, "left"),
|
|
295
|
+
},
|
|
296
|
+
};
|
|
250
297
|
}
|
|
251
298
|
// A bg with a real colour to paint. `undefined` (no bg) and the terminal
|
|
252
299
|
// DEFAULT colour (transparent — identical to the terminal background) are the
|
|
@@ -312,26 +359,29 @@ function cellStyle(cell, edges, options) {
|
|
|
312
359
|
}
|
|
313
360
|
const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
|
|
314
361
|
/**
|
|
315
|
-
* The one joiner: what it draws
|
|
316
|
-
*
|
|
362
|
+
* The one joiner: what it draws is a `JoinSeam` between neighbours and a
|
|
363
|
+
* `JoinCaps` at the ends of each run, picked independently, so a new joiner is
|
|
364
|
+
* a pair of values, never a class.
|
|
317
365
|
*
|
|
318
366
|
* Which part a join draws is read off its two neighbours. A neighbour is
|
|
319
367
|
* missing at the strip's ends, and counts as missing — an endpoint — when the
|
|
320
|
-
*
|
|
321
|
-
* join is the seam, or the divider where every seam cell's ink is
|
|
322
|
-
* indistinct from its ground at `options.colorSystem`; with only the
|
|
323
|
-
* the left, the tail; with neither, nothing.
|
|
368
|
+
* 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
|
|
372
|
+
* that paints with its neighbour's ground is drawn only beside a neighbour
|
|
373
|
+
* with one, so caps that need grounds never stop a seam that needs none.
|
|
324
374
|
*
|
|
325
|
-
*
|
|
375
|
+
* An ASCII-only output draws the `ascii` form of both, whole.
|
|
326
376
|
*/
|
|
327
377
|
export class CellJoiner {
|
|
328
|
-
|
|
329
|
-
|
|
378
|
+
seam;
|
|
379
|
+
caps;
|
|
330
380
|
_forms;
|
|
331
|
-
constructor(
|
|
332
|
-
this.
|
|
333
|
-
this.
|
|
334
|
-
this._forms = { glyph: formOf(this.
|
|
381
|
+
constructor(seam, caps) {
|
|
382
|
+
this.seam = parseSeam(seam);
|
|
383
|
+
this.caps = parseCaps(caps);
|
|
384
|
+
this._forms = { glyph: formOf(joinForms(this.seam.glyph, this.caps.glyph)), ascii: formOf(joinForms(this.seam.ascii, this.caps.ascii)) };
|
|
335
385
|
}
|
|
336
386
|
join(left, right, { index }) {
|
|
337
387
|
const forms = this._forms;
|
|
@@ -341,8 +391,10 @@ export class CellJoiner {
|
|
|
341
391
|
const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
|
|
342
392
|
const leftBg = paintableBg(edges.left?.bgcolor);
|
|
343
393
|
const rightBg = paintableBg(edges.right?.bgcolor);
|
|
344
|
-
const hasLeft = left !== null && (!reads.left || leftBg !== undefined);
|
|
345
|
-
const hasRight = right !== null && (!reads.right || rightBg !== undefined);
|
|
394
|
+
const hasLeft = left !== null && (!reads.seam.left || leftBg !== undefined);
|
|
395
|
+
const hasRight = right !== null && (!reads.seam.right || rightBg !== undefined);
|
|
396
|
+
const leads = hasRight && (!reads.lead || rightBg !== undefined);
|
|
397
|
+
const tails = hasLeft && (!reads.tail || leftBg !== undefined);
|
|
346
398
|
const neighbours = { left, right };
|
|
347
399
|
// On screen a cell's ink is its `fg` paint and its ground its `bg`,
|
|
348
400
|
// reverse video or not (`cellStyle`).
|
|
@@ -352,8 +404,8 @@ export class CellJoiner {
|
|
|
352
404
|
return new Segment(pickGlyph(cell, part, index, at), cellStyle(cell, edges, options).withAnchor({ owner: owners, row: 0, col: 0, inner: undefined }));
|
|
353
405
|
});
|
|
354
406
|
if (!hasLeft || !hasRight) {
|
|
355
|
-
if (
|
|
356
|
-
yield* styled(
|
|
407
|
+
if (leads || tails)
|
|
408
|
+
yield* styled(leads ? "lead" : "tail");
|
|
357
409
|
return;
|
|
358
410
|
}
|
|
359
411
|
// A seam is lost only when every cell of it is: one cell rounded into
|
|
@@ -367,30 +419,34 @@ export class CellJoiner {
|
|
|
367
419
|
};
|
|
368
420
|
}
|
|
369
421
|
}
|
|
370
|
-
// --- The bundled
|
|
422
|
+
// --- The bundled seams and caps ---
|
|
371
423
|
const LEFT_BG = { edge: "left", part: "bg" };
|
|
372
424
|
const RIGHT_BG = { edge: "right", part: "bg" };
|
|
373
425
|
const LEFT_FG = { edge: "left", part: "fg" };
|
|
374
426
|
const HALF = { mix: [LEFT_BG, RIGHT_BG, 1 / 2] };
|
|
375
427
|
const GROUND = "ground";
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
428
|
+
const LEAD = { cap: "lead" };
|
|
429
|
+
const TAIL = { cap: "tail" };
|
|
430
|
+
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));
|
|
445
|
+
// Three arrows stepping through the two thirds between the grounds.
|
|
389
446
|
function prism(glyph, thin) {
|
|
390
447
|
const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
|
|
391
448
|
const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
|
|
392
449
|
return {
|
|
393
|
-
...edge(glyph, thin),
|
|
394
450
|
seam: [
|
|
395
451
|
{ glyph, fg: LEFT_BG, bg: third(1 / 3) },
|
|
396
452
|
{ glyph, fg: third(1 / 3), bg: third(2 / 3) },
|
|
@@ -399,63 +455,114 @@ function prism(glyph, thin) {
|
|
|
399
455
|
divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
|
|
400
456
|
};
|
|
401
457
|
}
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
// terminal's, with `between` — cells on the terminal's ground — between runs.
|
|
408
|
-
function pill(open, close, between) {
|
|
409
|
-
const seam = [{ glyph: close, fg: LEFT_BG, bg: GROUND }, ...between, { glyph: open, fg: RIGHT_BG, bg: GROUND }];
|
|
410
|
-
return { seam, divider: seam, lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }], tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }] };
|
|
458
|
+
// The runs parted: the caps close the one and open the other, `between` —
|
|
459
|
+
// cells on the terminal's ground — set between them.
|
|
460
|
+
function parted(between) {
|
|
461
|
+
const seam = [TAIL, ...between, LEAD];
|
|
462
|
+
return { seam, divider: seam };
|
|
411
463
|
}
|
|
412
|
-
|
|
413
|
-
const metro = (open, close, rail) => pill(open, close, [{ glyph: rail, fg: { role: "strip.rail" }, bg: GROUND }]);
|
|
414
|
-
// One cell drawn over the two grounds' midpoint, so it reads as neither item's;
|
|
415
|
-
// nothing at the ends, where there is no second ground to meet.
|
|
464
|
+
// One cell drawn over the two grounds' midpoint, so it reads as neither item's.
|
|
416
465
|
function midpoint(glyph, fg) {
|
|
417
466
|
const seam = [{ glyph, fg, bg: HALF }];
|
|
418
|
-
return { seam, divider: seam
|
|
467
|
+
return { seam, divider: seam };
|
|
419
468
|
}
|
|
420
469
|
// A ragged tear in the left ground over the midpoint; where that ink would
|
|
421
470
|
// vanish into the midpoint — grounds a few steps apart, or one ground — the
|
|
422
471
|
// same tear in the left item's text.
|
|
423
472
|
function torn(glyphs) {
|
|
424
|
-
return {
|
|
473
|
+
return { seam: midpoint({ oneOf: glyphs }, LEFT_BG).seam, divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
|
|
425
474
|
}
|
|
426
475
|
const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
|
|
427
|
-
const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM
|
|
476
|
+
const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM };
|
|
477
|
+
const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
|
|
478
|
+
const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
|
|
428
479
|
/**
|
|
429
|
-
* The bundled
|
|
430
|
-
*
|
|
480
|
+
* The bundled seams, by name: what a joiner draws between two neighbours.
|
|
481
|
+
* What each draws is `docs/strip.md`'s "Seams"; any of them ends in any of
|
|
482
|
+
* `CAPS`.
|
|
483
|
+
*/
|
|
484
|
+
export const SEAMS = Object.freeze({
|
|
485
|
+
powerline: parseSeam(arrowSeam(POWERLINE)),
|
|
486
|
+
prism: parseSeam(both(prism(POWERLINE.glyph, POWERLINE.thin), prism(POWERLINE.ascii, POWERLINE.asciiThin))),
|
|
487
|
+
slant: parseSeam(arrowSeam(SLANT)),
|
|
488
|
+
backslant: parseSeam(arrowSeam(BACKSLANT)),
|
|
489
|
+
flame: parseSeam(arrowSeam(FLAME)),
|
|
490
|
+
pixel: parseSeam(arrowSeam(PIXEL)),
|
|
491
|
+
ice: parseSeam(arrowSeam(ICE)),
|
|
492
|
+
bubble: parseSeam(arrowSeam(BUBBLE)),
|
|
493
|
+
capsule: parseSeam(both(parted([SPACE]), parted([SPACE]))),
|
|
494
|
+
wrapper: parseSeam(both(parted([]), parted([]))),
|
|
495
|
+
metro: parseSeam(both(parted([rail("──")]), parted([rail("--")]))),
|
|
496
|
+
stitch: parseSeam(both(midpoint("┊", LEFT_FG), midpoint(":", LEFT_FG))),
|
|
497
|
+
torn: parseSeam(both(torn(["▚▞", "▞▚", "▘▗", "▝▖", "▖▝", "▗▘"]), torn(["/\\", "\\/"]))),
|
|
498
|
+
plain: parseSeam(both(PLAIN, PLAIN)),
|
|
499
|
+
});
|
|
500
|
+
// The family's glyph reversed out of the terminal's ground at the lead — a
|
|
501
|
+
// notch whose ink is the terminal's background — and over that ground at the
|
|
502
|
+
// tail, so a run reads as one chevron. ASCII opens each run flat: an ASCII
|
|
503
|
+
// glyph reversed out of the terminal's ground is a block of nothing.
|
|
504
|
+
const flowCaps = (f) => both({ lead: [{ glyph: f.glyph, fg: GROUND, bg: RIGHT_BG }], tail: [{ glyph: f.glyph, fg: LEFT_BG, bg: GROUND }] }, { lead: [], tail: [{ glyph: f.ascii, fg: LEFT_BG, bg: GROUND }] });
|
|
505
|
+
// `open` and `close`, each in its run's ground over the terminal's.
|
|
506
|
+
const pill = (open, close) => ({
|
|
507
|
+
lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }],
|
|
508
|
+
tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }],
|
|
509
|
+
});
|
|
510
|
+
const FLAT = { lead: [], tail: [] };
|
|
511
|
+
/**
|
|
512
|
+
* The bundled caps, by name: what a joiner draws where a run begins and ends.
|
|
513
|
+
* What each draws is `docs/strip.md`'s "Caps"; any of them ends any of
|
|
514
|
+
* `SEAMS`.
|
|
515
|
+
*/
|
|
516
|
+
export const CAPS = Object.freeze({
|
|
517
|
+
powerline: parseCaps(flowCaps(POWERLINE)),
|
|
518
|
+
slant: parseCaps(flowCaps(SLANT)),
|
|
519
|
+
backslant: parseCaps(flowCaps(BACKSLANT)),
|
|
520
|
+
flame: parseCaps(flowCaps(FLAME)),
|
|
521
|
+
pixel: parseCaps(flowCaps(PIXEL)),
|
|
522
|
+
ice: parseCaps(flowCaps(ICE)),
|
|
523
|
+
bubble: parseCaps(flowCaps(BUBBLE)),
|
|
524
|
+
capsule: parseCaps(both(pill("\ue0b6", "\ue0b4"), pill("(", ")"))),
|
|
525
|
+
wrapper: parseCaps(both(pill("\ue0b2", "\ue0b0"), pill("<", ">"))),
|
|
526
|
+
flat: parseCaps(both(FLAT, FLAT)),
|
|
527
|
+
});
|
|
528
|
+
/**
|
|
529
|
+
* The bundled joiners, by name: each a seam from `SEAMS` ended in caps from
|
|
530
|
+
* `CAPS`. What each draws is `docs/strip.md`'s "Built-in joiners"; a name from
|
|
531
|
+
* config is read with `joinerNamed`.
|
|
431
532
|
*/
|
|
432
533
|
export const JOINERS = Object.freeze({
|
|
433
|
-
powerline:
|
|
434
|
-
prism:
|
|
435
|
-
slant:
|
|
436
|
-
backslant:
|
|
437
|
-
flame:
|
|
438
|
-
pixel:
|
|
439
|
-
ice:
|
|
440
|
-
bubble:
|
|
441
|
-
capsule: new CellJoiner(capsule
|
|
442
|
-
wrapper: new CellJoiner(
|
|
443
|
-
metro: new CellJoiner(metro
|
|
444
|
-
stitch: new CellJoiner(
|
|
445
|
-
torn: new CellJoiner(torn
|
|
446
|
-
plain: new CellJoiner(
|
|
534
|
+
powerline: new CellJoiner(SEAMS.powerline, CAPS.powerline),
|
|
535
|
+
prism: new CellJoiner(SEAMS.prism, CAPS.powerline),
|
|
536
|
+
slant: new CellJoiner(SEAMS.slant, CAPS.slant),
|
|
537
|
+
backslant: new CellJoiner(SEAMS.backslant, CAPS.backslant),
|
|
538
|
+
flame: new CellJoiner(SEAMS.flame, CAPS.flame),
|
|
539
|
+
pixel: new CellJoiner(SEAMS.pixel, CAPS.pixel),
|
|
540
|
+
ice: new CellJoiner(SEAMS.ice, CAPS.ice),
|
|
541
|
+
bubble: new CellJoiner(SEAMS.bubble, CAPS.bubble),
|
|
542
|
+
capsule: new CellJoiner(SEAMS.capsule, CAPS.capsule),
|
|
543
|
+
wrapper: new CellJoiner(SEAMS.wrapper, CAPS.wrapper),
|
|
544
|
+
metro: new CellJoiner(SEAMS.metro, CAPS.capsule),
|
|
545
|
+
stitch: new CellJoiner(SEAMS.stitch, CAPS.flat),
|
|
546
|
+
torn: new CellJoiner(SEAMS.torn, CAPS.flat),
|
|
547
|
+
plain: new CellJoiner(SEAMS.plain, CAPS.flat),
|
|
447
548
|
});
|
|
448
549
|
/**
|
|
449
|
-
* The
|
|
450
|
-
*
|
|
451
|
-
* and the rest of `Object.prototype` are
|
|
550
|
+
* The joiner `spec` names — a joiner read from config, parsed. Throws on a
|
|
551
|
+
* name its table lacks, naming the ones there are; an own-key test, so
|
|
552
|
+
* `toString` and the rest of `Object.prototype` are no one's name.
|
|
452
553
|
*/
|
|
453
|
-
export function joinerNamed(
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
554
|
+
export function joinerNamed(spec) {
|
|
555
|
+
if (typeof spec === "string")
|
|
556
|
+
return named(JOINERS, "joiner", "joiners", spec);
|
|
557
|
+
if (typeof spec !== "object" || spec === null)
|
|
558
|
+
throw new Error(`a joiner is a name or { seam, caps }, not ${JSON.stringify(spec)}`);
|
|
559
|
+
return new CellJoiner(named(SEAMS, "seam", "seams", spec.seam), named(CAPS, "caps", "caps", spec.caps));
|
|
560
|
+
}
|
|
561
|
+
function named(table, kind, kinds, name) {
|
|
562
|
+
if (!Object.hasOwn(table, name))
|
|
563
|
+
throw new Error(`no ${kind} named ${JSON.stringify(name)}; the ${kinds} are ${Object.keys(table).join(", ")}`);
|
|
564
|
+
return table[name];
|
|
457
565
|
}
|
|
458
|
-
const isJoinerName = (name) => Object.hasOwn(JOINERS, name);
|
|
459
566
|
function gradient(steps, glyph) {
|
|
460
567
|
// Midpoint sampling across `2 * steps` half-cell positions: cell i takes
|
|
461
568
|
// samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
|
|
@@ -466,15 +573,15 @@ function gradient(steps, glyph) {
|
|
|
466
573
|
fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
|
|
467
574
|
bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
|
|
468
575
|
}));
|
|
469
|
-
return { seam, divider: seam
|
|
576
|
+
return { seam, divider: seam };
|
|
470
577
|
}
|
|
471
578
|
/**
|
|
472
579
|
* A ramp of `steps` cells from the left ground to the right. Each cell is the
|
|
473
580
|
* half block U+258C, ink on its left half and ground on its right, so it
|
|
474
|
-
* carries two samples; ASCII draws a space, keeping the grounds alone.
|
|
475
|
-
*
|
|
476
|
-
* ramp
|
|
581
|
+
* carries two samples; ASCII draws a space, keeping the grounds alone. Beside
|
|
582
|
+
* a neighbour with no ground there is no colour to ramp toward, so that join
|
|
583
|
+
* draws the caps the ramp is ended in.
|
|
477
584
|
*/
|
|
478
|
-
export function
|
|
479
|
-
return
|
|
585
|
+
export function gradientSeam(steps = 4) {
|
|
586
|
+
return parseSeam(both(gradient(steps, "▌"), gradient(steps, " ")));
|
|
480
587
|
}
|
package/dist/core/style.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface StyleOptions {
|
|
|
38
38
|
link?: string;
|
|
39
39
|
meta?: Record<string, unknown>;
|
|
40
40
|
anchor?: Anchor;
|
|
41
|
+
nextChange?: number;
|
|
41
42
|
}
|
|
42
43
|
export declare class StyleSyntaxError extends Error {
|
|
43
44
|
constructor(message: string, options?: ErrorOptions);
|
|
@@ -66,6 +67,18 @@ export declare class Style {
|
|
|
66
67
|
* unequal, because they came from different places.
|
|
67
68
|
*/
|
|
68
69
|
readonly anchor: Anchor | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* When what this style's cells show next changes, in seconds on the
|
|
72
|
+
* timeline of whoever drew the frame — `t` itself for a cell drawn by
|
|
73
|
+
* something that moves every frame, and `Infinity`, the default, for one
|
|
74
|
+
* that never changes. It draws nothing. Whatever draws for a handed time
|
|
75
|
+
* stamps its cells with it (`Frames`, `Effected`), the stamp rides up
|
|
76
|
+
* through every container as the anchor does (`./anchor.ts`), and
|
|
77
|
+
* whoever holds the composed frame reads the soonest off it
|
|
78
|
+
* (`Segment.nextChange`), so a host knows when to draw again without
|
|
79
|
+
* asking each container. Two styles laid together change at the sooner.
|
|
80
|
+
*/
|
|
81
|
+
readonly nextChange: number;
|
|
69
82
|
constructor(options?: StyleOptions);
|
|
70
83
|
get isNull(): boolean;
|
|
71
84
|
get transparentBackground(): boolean;
|
|
@@ -115,7 +128,11 @@ export declare class Style {
|
|
|
115
128
|
*/
|
|
116
129
|
toSgrCodes(colorSystem: ColorDepth): string;
|
|
117
130
|
/**
|
|
118
|
-
*
|
|
131
|
+
* The look as a canonical string that round-trips through `Style.parse()`:
|
|
132
|
+
* colours, attributes and the link, which is all a style string can spell.
|
|
133
|
+
* What a style carries beyond its look — `meta`, `anchor`, `nextChange` —
|
|
134
|
+
* has no spelling and is not here, so two styles that differ only there
|
|
135
|
+
* read alike; `equals` tells them apart.
|
|
119
136
|
*/
|
|
120
137
|
toString(): string;
|
|
121
138
|
static null(): Style;
|
package/dist/core/style.js
CHANGED
|
@@ -94,7 +94,26 @@ export class Style {
|
|
|
94
94
|
* unequal, because they came from different places.
|
|
95
95
|
*/
|
|
96
96
|
anchor;
|
|
97
|
+
/**
|
|
98
|
+
* When what this style's cells show next changes, in seconds on the
|
|
99
|
+
* timeline of whoever drew the frame — `t` itself for a cell drawn by
|
|
100
|
+
* something that moves every frame, and `Infinity`, the default, for one
|
|
101
|
+
* that never changes. It draws nothing. Whatever draws for a handed time
|
|
102
|
+
* stamps its cells with it (`Frames`, `Effected`), the stamp rides up
|
|
103
|
+
* through every container as the anchor does (`./anchor.ts`), and
|
|
104
|
+
* whoever holds the composed frame reads the soonest off it
|
|
105
|
+
* (`Segment.nextChange`), so a host knows when to draw again without
|
|
106
|
+
* asking each container. Two styles laid together change at the sooner.
|
|
107
|
+
*/
|
|
108
|
+
nextChange;
|
|
97
109
|
constructor(options = {}) {
|
|
110
|
+
const nextChange = options.nextChange ?? Number.POSITIVE_INFINITY;
|
|
111
|
+
// [LAW:parse-dont-validate] A time or never, settled where the style is
|
|
112
|
+
// made: NaN and -Infinity fail here, so no reader of a frame meets one.
|
|
113
|
+
if (!(nextChange > Number.NEGATIVE_INFINITY)) {
|
|
114
|
+
throw new RangeError(`Style nextChange must be a time in seconds or Infinity, got ${nextChange}`);
|
|
115
|
+
}
|
|
116
|
+
this.nextChange = nextChange;
|
|
98
117
|
this.color = resolveColor(options.color);
|
|
99
118
|
this.bgcolor = resolveBackground(options.bgcolor);
|
|
100
119
|
this.bold = options.bold;
|
|
@@ -132,7 +151,8 @@ export class Style {
|
|
|
132
151
|
this.overline === undefined &&
|
|
133
152
|
this.link === undefined &&
|
|
134
153
|
this.meta === undefined &&
|
|
135
|
-
this.anchor === undefined
|
|
154
|
+
this.anchor === undefined &&
|
|
155
|
+
this.nextChange === Number.POSITIVE_INFINITY);
|
|
136
156
|
}
|
|
137
157
|
get transparentBackground() {
|
|
138
158
|
return this.bgcolor === undefined || this.bgcolor.isDefault;
|
|
@@ -204,6 +224,8 @@ export class Style {
|
|
|
204
224
|
? { ...this.meta, ...other.meta }
|
|
205
225
|
: other.meta ?? this.meta,
|
|
206
226
|
anchor: other.anchor ?? this.anchor,
|
|
227
|
+
// Cells under both change when either does: the sooner.
|
|
228
|
+
nextChange: Math.min(this.nextChange, other.nextChange),
|
|
207
229
|
});
|
|
208
230
|
}
|
|
209
231
|
equals(other) {
|
|
@@ -225,7 +247,8 @@ export class Style {
|
|
|
225
247
|
this.encircle === other.encircle &&
|
|
226
248
|
this.overline === other.overline &&
|
|
227
249
|
this.link === other.link &&
|
|
228
|
-
anchorsEqual(this.anchor, other.anchor)
|
|
250
|
+
anchorsEqual(this.anchor, other.anchor) &&
|
|
251
|
+
this.nextChange === other.nextChange);
|
|
229
252
|
}
|
|
230
253
|
/**
|
|
231
254
|
* The colours this style puts on screen at `colorSystem`: each flattened
|
|
@@ -273,7 +296,11 @@ export class Style {
|
|
|
273
296
|
].join(";");
|
|
274
297
|
}
|
|
275
298
|
/**
|
|
276
|
-
*
|
|
299
|
+
* The look as a canonical string that round-trips through `Style.parse()`:
|
|
300
|
+
* colours, attributes and the link, which is all a style string can spell.
|
|
301
|
+
* What a style carries beyond its look — `meta`, `anchor`, `nextChange` —
|
|
302
|
+
* has no spelling and is not here, so two styles that differ only there
|
|
303
|
+
* read alike; `equals` tells them apart.
|
|
277
304
|
*/
|
|
278
305
|
toString() {
|
|
279
306
|
if (this.isNull)
|
|
@@ -678,6 +705,8 @@ export const DEFAULT_STYLES = {
|
|
|
678
705
|
"markdown.hr": Style.parse("yellow"),
|
|
679
706
|
"markdown.link": Style.parse("bright_blue"),
|
|
680
707
|
"markdown.link_url": Style.parse("blue"),
|
|
708
|
+
"markdown.table.border": Style.parse("cyan"),
|
|
709
|
+
"markdown.table.header": Style.parse("not bold cyan"),
|
|
681
710
|
// Progress
|
|
682
711
|
"progress.description": NULL_STYLE,
|
|
683
712
|
"progress.percentage": Style.parse("magenta"),
|
package/dist/core/text.js
CHANGED
|
@@ -73,7 +73,10 @@ function sanitizeStyleLink(style) {
|
|
|
73
73
|
* [LAW:single-enforcer] Nor does a RichText keep an anchor (`./anchor.ts`). It
|
|
74
74
|
* lays its text out anew, so a stamp naming where a cell sat in the output it
|
|
75
75
|
* came from would name the wrong cell once it wraps, slices or overlaps; the
|
|
76
|
-
* owner that renders the RichText is the one that stamps its cells.
|
|
76
|
+
* owner that renders the RichText is the one that stamps its cells. A
|
|
77
|
+
* `nextChange` stays: when a span's cells next change is true of them
|
|
78
|
+
* wherever the text lays them, so a frame appended into a line still says
|
|
79
|
+
* when it turns.
|
|
77
80
|
*/
|
|
78
81
|
function admitStyle(style) {
|
|
79
82
|
if (typeof style === "string")
|
package/dist/host/app.js
CHANGED
|
@@ -91,13 +91,11 @@ export class App {
|
|
|
91
91
|
mouseHandlers = new Set();
|
|
92
92
|
wheelHandlers = new Set();
|
|
93
93
|
quitKeys;
|
|
94
|
-
input
|
|
95
|
-
key: (press) => this.key(press),
|
|
96
|
-
mouse: (event) => this.mouse(event),
|
|
97
|
-
});
|
|
94
|
+
input;
|
|
98
95
|
settle = () => { };
|
|
99
96
|
constructor(options) {
|
|
100
97
|
this.host = options.host;
|
|
98
|
+
this.input = new InputParser({ key: (press) => this.key(press), mouse: (event) => this.mouse(event) }, options.host.inputChunks);
|
|
101
99
|
this.surface = options.surface;
|
|
102
100
|
this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
|
|
103
101
|
this.view = options.view;
|
package/dist/host/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { BrowserTerminalHost } from "./terminal-host.js";
|
|
2
|
-
export type { TerminalHost, TerminalSize, DataHandler, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
2
|
+
export type { TerminalHost, TerminalSize, DataHandler, InputChunks, ResizeHandler, ExitHandler, BrowserTerminalHostOptions, XtermTerminal, XtermDisposable, XtermResizeEvent, } from "./terminal-host.js";
|
|
3
3
|
export { hostEnvironment } from "./host-environment.js";
|
|
4
4
|
export { App } from "./app.js";
|
|
5
5
|
export type { AppMouseEvent, AppOptions, AppPhase, Wheel } from "./app.js";
|