@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.
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +10 -0
- package/dist/core/color.d.ts +9 -0
- package/dist/core/color.js +23 -1
- package/dist/core/strip.d.ts +81 -29
- package/dist/core/strip.js +190 -66
- package/dist/core/style.js +1 -0
- package/dist/host/input.d.ts +5 -2
- package/dist/host/input.js +29 -10
- package/dist/index.d.ts +6 -6
- package/dist/index.js +3 -3
- package/dist/renderables/effect.d.ts +90 -17
- package/dist/renderables/effect.js +114 -43
- package/dist/renderables/effects.d.ts +93 -50
- package/dist/renderables/effects.js +215 -150
- package/dist/renderables/flexStrip.d.ts +1 -1
- package/dist/renderables/flexStrip.js +10 -9
- package/dist/renderables/progressBar.js +3 -2
- package/dist/widgets/hit.js +2 -4
- package/package.json +6 -2
package/dist/core/anchor.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/anchor.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/color.d.ts
CHANGED
|
@@ -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.
|
package/dist/core/color.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/core/strip.d.ts
CHANGED
|
@@ -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
|
|
114
|
-
*
|
|
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
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
|
142
|
-
*
|
|
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
|
|
156
|
-
*
|
|
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
|
|
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
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
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
|
package/dist/core/strip.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
198
|
-
// is what is drawn — `Style.drawnColors`, the
|
|
199
|
-
//
|
|
200
|
-
// black, both downgraded to the depth the
|
|
201
|
-
//
|
|
202
|
-
// colour whose RGB is the terminal theme's own (ANSI
|
|
203
|
-
// one) has no value here, so two of them are the same
|
|
204
|
-
// expression: the same palette slot — its `number`,
|
|
205
|
-
// depth that spells it ("red", "color(1)", an EIGHT_BIT
|
|
206
|
-
// slot) — or the same blend, whose name spells each end
|
|
207
|
-
// same reason.
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
252
|
-
*
|
|
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
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
{ glyph:
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
*
|
|
329
|
-
*
|
|
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
|
|
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),
|
package/dist/core/style.js
CHANGED
|
@@ -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"),
|
package/dist/host/input.d.ts
CHANGED
|
@@ -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).
|
|
20
|
-
*
|
|
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;
|