@promptctl/rich-js 0.18.1 → 0.20.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 +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
package/dist/core/strip.js
CHANGED
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { Segment } from "./segment.js";
|
|
29
29
|
import { Style } from "./style.js";
|
|
30
|
-
import { ColorDepth, ColorSpec,
|
|
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 { drawable, getStyle, isAscii } from "./protocol.js";
|
|
33
34
|
// --- Strip ---
|
|
34
35
|
export class Strip {
|
|
35
36
|
items;
|
|
@@ -46,12 +47,17 @@ export class Strip {
|
|
|
46
47
|
// start-cap, item, mid-join, item, ..., item, end-cap. Variability lives
|
|
47
48
|
// in `items` and in what the joiner emits at each position — never in
|
|
48
49
|
// whether a join runs.
|
|
49
|
-
|
|
50
|
+
//
|
|
51
|
+
// The row sits its items side by side, so none is handed a justify: a
|
|
52
|
+
// `left` fills the canvas, and the first item would pad over the rest of
|
|
53
|
+
// the row. Where the row sits in the width is its container's to say.
|
|
54
|
+
const row = { ...options, justify: undefined };
|
|
55
|
+
yield* this.joiner.join(null, items[0]).render(row);
|
|
50
56
|
for (let i = 0; i < items.length; i++) {
|
|
51
57
|
const item = items[i];
|
|
52
|
-
yield* item.render(
|
|
58
|
+
yield* item.render(row);
|
|
53
59
|
const next = i + 1 < items.length ? items[i + 1] : null;
|
|
54
|
-
yield* this.joiner.join(item, next).render(
|
|
60
|
+
yield* this.joiner.join(item, next).render(row);
|
|
55
61
|
}
|
|
56
62
|
// [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
|
|
57
63
|
// it emits, including the last, and the other block-level renderables
|
|
@@ -61,245 +67,288 @@ export class Strip {
|
|
|
61
67
|
// under composition: printed alone, Console's own "close whatever the
|
|
62
68
|
// renderable left open" step (console.ts) papered over it, and a `Group`
|
|
63
69
|
// of a Strip and a RichText ran the RichText onto the Strip's line
|
|
64
|
-
// instead of below it.
|
|
65
|
-
//
|
|
66
|
-
// rich-flexstrip-5kf.4cq for the open question of formalizing that split.)
|
|
70
|
+
// instead of below it. `LINE_ENDS` in test/seam/line-ends.ts is the list
|
|
71
|
+
// of which renderables end their own line and which are fragments.
|
|
67
72
|
yield Segment.line();
|
|
68
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* [LAW:one-source-of-truth] The width of the one row it draws, and that is
|
|
76
|
+
* both bounds: the row never wraps and no item is offered less than the whole
|
|
77
|
+
* width, so there is no narrower strip to report as a minimum. Read off the
|
|
78
|
+
* drawing rather than summed from the items' measurements, which describe
|
|
79
|
+
* how an item could be drawn, not how the walk draws it.
|
|
80
|
+
*/
|
|
81
|
+
measure(options) {
|
|
82
|
+
let width = 0;
|
|
83
|
+
for (const segment of this.render(options))
|
|
84
|
+
width += segment.cellLength;
|
|
85
|
+
return { minimum: width, maximum: width };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const PARTS = ["seam", "divider", "lead", "tail"];
|
|
89
|
+
/**
|
|
90
|
+
* The least ΔE_OK two neighbouring backgrounds must differ by for a seam drawn
|
|
91
|
+
* between them to be seen. Below it the seam is drawn in a colour the eye
|
|
92
|
+
* cannot tell from its own background, so the joiner draws the divider
|
|
93
|
+
* instead. Twice the ~.02 threshold of a visible difference, because a seam is
|
|
94
|
+
* one cell wide.
|
|
95
|
+
*/
|
|
96
|
+
export const SEAM_MIN_DELTA_E = 0.04;
|
|
97
|
+
function* paintsIn(paint) {
|
|
98
|
+
yield paint;
|
|
99
|
+
if ("mix" in paint) {
|
|
100
|
+
yield* paintsIn(paint.mix[0]);
|
|
101
|
+
yield* paintsIn(paint.mix[1]);
|
|
102
|
+
}
|
|
69
103
|
}
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
const glyphsOf = (cell) => (typeof cell.glyph === "string" ? [cell.glyph] : cell.glyph.oneOf);
|
|
105
|
+
const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
|
|
106
|
+
const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
|
|
107
|
+
// `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
|
|
108
|
+
// name, because the part is also drawn where that neighbour does not exist.
|
|
109
|
+
function parsePaint(raw, absent, refuse) {
|
|
110
|
+
if (raw === "ground")
|
|
111
|
+
return refuse("mixes ground, whose colour is unknown");
|
|
112
|
+
const p = (typeof raw === "object" && raw !== null ? raw : {});
|
|
113
|
+
if ((p.edge === "left" || p.edge === "right") && (p.part === "bg" || p.part === "fg")) {
|
|
114
|
+
if (p.edge === absent)
|
|
115
|
+
refuse(`names the ${absent} neighbour, which this part is drawn without`);
|
|
116
|
+
return Object.freeze({ edge: p.edge, part: p.part });
|
|
117
|
+
}
|
|
118
|
+
if (typeof p.role === "string")
|
|
119
|
+
return Object.freeze({ role: p.role });
|
|
120
|
+
if (Array.isArray(p.mix) && p.mix.length === 3) {
|
|
121
|
+
const [from, to, t] = p.mix;
|
|
122
|
+
if (typeof t !== "number" || !(t >= 0 && t <= 1))
|
|
123
|
+
refuse(`mixes at ${String(t)}, outside 0..1`);
|
|
124
|
+
return Object.freeze({ mix: Object.freeze([parsePaint(from, absent, refuse), parsePaint(to, absent, refuse), t]) });
|
|
125
|
+
}
|
|
126
|
+
return refuse(`has a colour that is not a Paint: ${JSON.stringify(raw)}`);
|
|
79
127
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
128
|
+
const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
|
|
129
|
+
const ABSENT = { seam: undefined, divider: undefined, lead: "left", tail: "right" };
|
|
130
|
+
function parseList(raw, where) {
|
|
131
|
+
if (!Array.isArray(raw))
|
|
132
|
+
throw new Error(`CellJoiner: ${where} is not a list of cells`);
|
|
133
|
+
return raw.map((cell, i) => {
|
|
134
|
+
if (typeof cell !== "object" || cell === null)
|
|
135
|
+
throw new Error(`CellJoiner: ${where}[${i}] is not a cell`);
|
|
136
|
+
return cell;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* [LAW:parse-dont-validate] The one place a shape is checked, so every rule
|
|
141
|
+
* the cell model states is refused at construction and the render reads a
|
|
142
|
+
* shape that already holds them. A shape is often authored as data — a
|
|
143
|
+
* consumer's config — so the rules the types state are checked here too, and
|
|
144
|
+
* what comes back is a frozen copy the caller can no longer change.
|
|
145
|
+
*/
|
|
146
|
+
function parseShape(raw, form) {
|
|
147
|
+
const parseCells = (part) => Object.freeze(parseList(raw[part], `${form} ${part}`).map((cell, i) => {
|
|
148
|
+
const refuse = (why) => {
|
|
149
|
+
throw new Error(`CellJoiner: ${form} ${part}[${i}] ${why}`);
|
|
150
|
+
};
|
|
151
|
+
const glyph = cell.glyph;
|
|
152
|
+
const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
|
|
153
|
+
if (typeof glyph !== "string" && !(Array.isArray(oneOf) && oneOf.every((g) => typeof g === "string"))) {
|
|
154
|
+
refuse(`has a glyph that is neither a string nor a oneOf of strings: ${JSON.stringify(glyph)}`);
|
|
155
|
+
}
|
|
156
|
+
if (cell.fg === "ground" && cell.bg === "ground")
|
|
157
|
+
refuse("has ground on both sides");
|
|
158
|
+
const glyphs = glyphsOf(cell);
|
|
159
|
+
if (glyphs.length === 0)
|
|
160
|
+
refuse("has an empty oneOf");
|
|
161
|
+
if (glyphs.some((glyph) => cellLen(glyph) !== cellWidth(cell)))
|
|
162
|
+
refuse("has oneOf members of different widths");
|
|
163
|
+
if (form === "ascii" && !glyphs.every(isAscii))
|
|
164
|
+
refuse("is not ASCII");
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
glyph: typeof cell.glyph === "string" ? cell.glyph : Object.freeze({ oneOf: Object.freeze([...glyphs]) }),
|
|
167
|
+
fg: parseColor(cell.fg, ABSENT[part], refuse),
|
|
168
|
+
bg: parseColor(cell.bg, ABSENT[part], refuse),
|
|
169
|
+
});
|
|
170
|
+
}));
|
|
171
|
+
const seam = parseCells("seam");
|
|
172
|
+
// A divider authored as the seam itself stays the seam, so a render can see
|
|
173
|
+
// it has no choice to make.
|
|
174
|
+
const divider = raw.divider === raw.seam ? seam : parseCells("divider");
|
|
175
|
+
if (partWidth(divider) !== partWidth(seam))
|
|
176
|
+
throw new Error(`CellJoiner: ${form} divider is not the width of the seam`);
|
|
177
|
+
return Object.freeze({ seam, divider, lead: parseCells("lead"), tail: parseCells("tail") });
|
|
178
|
+
}
|
|
179
|
+
// Whether any cell of `shape` paints with `side`'s ground.
|
|
180
|
+
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")));
|
|
181
|
+
function formOf(shape) {
|
|
182
|
+
return { shape, reads: { left: readsGround(shape, "left"), right: readsGround(shape, "right") } };
|
|
86
183
|
}
|
|
87
184
|
// A bg with a real colour to paint. `undefined` (no bg) and the terminal
|
|
88
185
|
// DEFAULT colour (transparent — identical to the terminal background) are the
|
|
89
|
-
// two representations of "nothing to paint"
|
|
90
|
-
//
|
|
186
|
+
// two representations of "nothing to paint", so an explicit `… on default`
|
|
187
|
+
// cannot smuggle a seam.
|
|
91
188
|
function paintableBg(bg) {
|
|
92
189
|
return bg !== undefined && !bg.isDefault ? bg : undefined;
|
|
93
190
|
}
|
|
94
|
-
// A
|
|
95
|
-
//
|
|
96
|
-
// enters.
|
|
191
|
+
// A neighbour's ground as the writer draws it (`Style.drawnColors`, onto the
|
|
192
|
+
// terminal's black), so a translucent ground is not composited a second time
|
|
193
|
+
// over whatever a join cell enters.
|
|
97
194
|
function drawnGround(bg) {
|
|
98
|
-
return
|
|
195
|
+
return bg.flattenAlpha(SURFACE_BLACK);
|
|
99
196
|
}
|
|
100
|
-
// A
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*/
|
|
114
|
-
export const SEAM_MIN_DELTA_E = 0.04;
|
|
115
|
-
// An arrow the eye cannot tell from the ground it is drawn on. What is
|
|
116
|
-
// measured is what is drawn — `Style.drawnColors`, the colours the writer
|
|
117
|
-
// encodes: the arrow's colour flattened onto its ground and the ground onto
|
|
118
|
-
// the terminal's black, both downgraded to the depth the render encodes at,
|
|
119
|
-
// so two grounds 256 colours round to one cube entry are the one entry they
|
|
120
|
-
// render as. A colour whose RGB is the terminal theme's own (ANSI 0–15) has no
|
|
121
|
-
// value here, so two of them are the same only when they are the same palette
|
|
122
|
-
// slot — its `number`, never the name or the depth that spells it ("red",
|
|
123
|
-
// "color(1)", an EIGHT_BIT spec on 0–15 are one slot).
|
|
124
|
-
function vanishes(arrow, colorSystem) {
|
|
125
|
-
// No colour emitted draws nothing to tell apart; measure what was handed.
|
|
126
|
-
const { color, bgcolor } = arrow.drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
|
|
197
|
+
// A seam the eye cannot tell from the ground it is drawn on. What is measured
|
|
198
|
+
// is what is drawn — `Style.drawnColors`, the colours the writer encodes: the
|
|
199
|
+
// left ground flattened onto the right and the right onto the terminal's
|
|
200
|
+
// black, both downgraded to the depth the render encodes at, so two grounds
|
|
201
|
+
// 256 colours round to one cube entry are the one entry they render as. A
|
|
202
|
+
// colour whose RGB is the terminal theme's own (ANSI 0–15, a blend reaching
|
|
203
|
+
// one) has no value here, so two of them are the same only when they are one
|
|
204
|
+
// expression: the same palette slot — its `number`, never the name or the
|
|
205
|
+
// depth that spells it ("red", "color(1)", an EIGHT_BIT spec on 0–15 are one
|
|
206
|
+
// slot) — or the same blend, whose name spells each end by its slot for the
|
|
207
|
+
// same reason.
|
|
208
|
+
function vanishes(leftBg, rightBg, colorSystem) {
|
|
209
|
+
const { color, bgcolor } = new Style({ color: drawnGround(leftBg), bgcolor: rightBg }).drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
|
|
127
210
|
if (color === undefined || bgcolor === undefined)
|
|
128
211
|
return false;
|
|
129
212
|
const av = color.fixedValue;
|
|
130
213
|
const bv = bgcolor.fixedValue;
|
|
131
214
|
return av !== undefined && bv !== undefined
|
|
132
215
|
? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
|
|
133
|
-
: color.number === bgcolor.number;
|
|
216
|
+
: color.number !== undefined ? color.number === bgcolor.number : color.name === bgcolor.name;
|
|
217
|
+
}
|
|
218
|
+
function resolvePaint(paint, edges, options) {
|
|
219
|
+
if ("edge" in paint) {
|
|
220
|
+
const edge = edges[paint.edge];
|
|
221
|
+
if (paint.part === "fg")
|
|
222
|
+
return edge?.color;
|
|
223
|
+
const bg = paintableBg(edge?.bgcolor);
|
|
224
|
+
return bg && drawnGround(bg);
|
|
225
|
+
}
|
|
226
|
+
if ("role" in paint)
|
|
227
|
+
return getStyle(options, paint.role).color;
|
|
228
|
+
const [from, to, t] = paint.mix;
|
|
229
|
+
const a = resolvePaint(from, edges, options);
|
|
230
|
+
const b = resolvePaint(to, edges, options);
|
|
231
|
+
// A mix with no colour at one end has nothing to blend toward.
|
|
232
|
+
return a && b && ColorSpec.blend(a, b, t);
|
|
134
233
|
}
|
|
234
|
+
function cellStyle(cell, edges, options) {
|
|
235
|
+
// [LAW:dataflow-not-control-flow] "ground" is the absence of a colour on its
|
|
236
|
+
// side; as the fg it is reverse video over the bg colour, so the glyph's ink
|
|
237
|
+
// is the terminal's own background.
|
|
238
|
+
if (cell.fg === "ground")
|
|
239
|
+
return new Style({ color: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options), reverse: true });
|
|
240
|
+
const color = resolvePaint(cell.fg, edges, options);
|
|
241
|
+
return new Style({ color, bgcolor: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options) });
|
|
242
|
+
}
|
|
243
|
+
const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
|
|
135
244
|
/**
|
|
136
|
-
* The
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* the
|
|
141
|
-
* with
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
245
|
+
* The one joiner: what it draws at each join is the `JoinShape` it is given,
|
|
246
|
+
* so a new joiner is a value, never a class.
|
|
247
|
+
*
|
|
248
|
+
* Which part a join draws is read off its two neighbours. A neighbour is
|
|
249
|
+
* missing at the strip's ends, and counts as missing — an endpoint — when the
|
|
250
|
+
* shape paints with its ground anywhere and it has none to paint. With both present the
|
|
251
|
+
* join is the seam, or the divider where the two grounds are drawn as one
|
|
252
|
+
* colour at `options.colorSystem`; with only the right, the lead; with only
|
|
253
|
+
* the left, the tail; with neither, nothing.
|
|
254
|
+
*
|
|
255
|
+
* `ascii` is the shape an ASCII-only output draws instead, whole.
|
|
145
256
|
*/
|
|
146
|
-
export
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
glyph: ">",
|
|
155
|
-
divider: ">",
|
|
156
|
-
lead: "",
|
|
157
|
-
tail: ">",
|
|
158
|
-
});
|
|
159
|
-
const powerlineGlyphs = ({ glyph, divider, lead, tail }) => glyph + divider + lead + tail;
|
|
160
|
-
export class PowerlineJoiner {
|
|
161
|
-
_glyphs;
|
|
162
|
-
constructor(options = POWERLINE_JOINER_GLYPHS) {
|
|
163
|
-
this._glyphs = options;
|
|
257
|
+
export class CellJoiner {
|
|
258
|
+
shape;
|
|
259
|
+
ascii;
|
|
260
|
+
_forms;
|
|
261
|
+
constructor(shape, ascii) {
|
|
262
|
+
this.shape = parseShape(shape, "glyph");
|
|
263
|
+
this.ascii = parseShape(ascii, "ascii");
|
|
264
|
+
this._forms = { glyph: formOf(this.shape), ascii: formOf(this.ascii) };
|
|
164
265
|
}
|
|
165
266
|
join(left, right) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// that share a background. "Equal" is perceptual (SEAM_MIN_DELTA_E) and
|
|
184
|
-
// measured on the colours the terminal draws at `options.colorSystem`: an
|
|
185
|
-
// arrow a hair off its background is as invisible as one exactly on it,
|
|
186
|
-
// and two backgrounds 256 colours round to one entry are one background.
|
|
187
|
-
// Background colour is paint, not structure; its ABSENCE (nothing to paint)
|
|
188
|
-
// is the only thing that elides the separator, and that is paint logic.
|
|
189
|
-
// "Absent" = no bg OR the terminal default (transparent) — paintableBg folds
|
|
190
|
-
// both to undefined so an explicit `… on default` cannot smuggle a separator.
|
|
191
|
-
const glyphs = this._glyphs;
|
|
192
|
-
return deferred(function* (options) {
|
|
193
|
-
const { glyph, divider, lead, tail } = drawable(options, glyphs, ASCII_POWERLINE_GLYPHS, powerlineGlyphs);
|
|
194
|
-
const leftEdge = left?.edgeStyle("right", options);
|
|
195
|
-
const leftBg = paintableBg(leftEdge?.bgcolor);
|
|
196
|
-
const rightBg = paintableBg(right?.edgeStyle("left", options).bgcolor);
|
|
197
|
-
if (leftBg === undefined) {
|
|
198
|
-
if (rightBg !== undefined)
|
|
199
|
-
yield* cap(lead, rightBg);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (rightBg === undefined) {
|
|
203
|
-
yield* cap(tail, leftBg);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
const arrow = new Style({ color: drawnGround(leftBg), bgcolor: rightBg });
|
|
207
|
-
// The divider is the left item's text on the left item's own ground,
|
|
208
|
-
// so it reads exactly as well as that item's text does, at any depth.
|
|
209
|
-
yield vanishes(arrow, options.colorSystem)
|
|
210
|
-
? new Segment(divider, new Style({ color: leftEdge?.color, bgcolor: leftBg }))
|
|
211
|
-
: new Segment(glyph, arrow);
|
|
212
|
-
});
|
|
267
|
+
const forms = this._forms;
|
|
268
|
+
return {
|
|
269
|
+
*render(options) {
|
|
270
|
+
const { shape: drawn, reads } = drawable(options, forms.glyph, forms.ascii, (form) => shapeGlyphs(form.shape));
|
|
271
|
+
const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
|
|
272
|
+
const leftBg = paintableBg(edges.left?.bgcolor);
|
|
273
|
+
const rightBg = paintableBg(edges.right?.bgcolor);
|
|
274
|
+
const hasLeft = left !== null && (!reads.left || leftBg !== undefined);
|
|
275
|
+
const hasRight = right !== null && (!reads.right || rightBg !== undefined);
|
|
276
|
+
const divides = drawn.divider !== drawn.seam && leftBg !== undefined && rightBg !== undefined && vanishes(leftBg, rightBg, options.colorSystem);
|
|
277
|
+
const cells = hasLeft && hasRight ? (divides ? drawn.divider : drawn.seam) : hasRight ? drawn.lead : hasLeft ? drawn.tail : [];
|
|
278
|
+
// A oneOf draws its first member until a join knows its seam index
|
|
279
|
+
// (rich-joiners-9ntp.bex).
|
|
280
|
+
for (const cell of cells)
|
|
281
|
+
yield new Segment(glyphsOf(cell)[0], cellStyle(cell, edges, options));
|
|
282
|
+
},
|
|
283
|
+
};
|
|
213
284
|
}
|
|
214
285
|
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
286
|
+
// --- The four joiners as shapes ---
|
|
287
|
+
const LEFT_BG = { edge: "left", part: "bg" };
|
|
288
|
+
const RIGHT_BG = { edge: "right", part: "bg" };
|
|
289
|
+
const LEFT_FG = { edge: "left", part: "fg" };
|
|
290
|
+
const GROUND = "ground";
|
|
291
|
+
/**
|
|
292
|
+
* The powerline joiner: U+E0B0 (right-arrow) in the left ground over the
|
|
293
|
+
* right, divided by U+E0B1 (thin right-arrow) in the left item's text. The
|
|
294
|
+
* lead is the arrow with the left ground replaced by the terminal's — drawn
|
|
295
|
+
* in reverse video it leaves a notch whose base is the cell's left edge, inked
|
|
296
|
+
* in the terminal's background colour. That ink is opaque, so under a
|
|
297
|
+
* translucent terminal the notch does not let what is behind it through. The
|
|
298
|
+
* tail is the arrow over the terminal's ground.
|
|
299
|
+
* ASCII draws `>` everywhere and begins each run flat.
|
|
300
|
+
*/
|
|
301
|
+
export const POWERLINE_JOINER = new CellJoiner({
|
|
302
|
+
seam: [{ glyph: "", fg: LEFT_BG, bg: RIGHT_BG }],
|
|
303
|
+
divider: [{ glyph: "", fg: LEFT_FG, bg: LEFT_BG }],
|
|
304
|
+
lead: [{ glyph: "", fg: GROUND, bg: RIGHT_BG }],
|
|
305
|
+
tail: [{ glyph: "", fg: LEFT_BG, bg: GROUND }],
|
|
306
|
+
}, {
|
|
307
|
+
seam: [{ glyph: ">", fg: LEFT_BG, bg: RIGHT_BG }],
|
|
308
|
+
divider: [{ glyph: ">", fg: LEFT_FG, bg: LEFT_BG }],
|
|
309
|
+
lead: [],
|
|
310
|
+
tail: [{ glyph: ">", fg: LEFT_BG, bg: GROUND }],
|
|
311
|
+
});
|
|
312
|
+
function capsule(open, close) {
|
|
313
|
+
const seam = [
|
|
314
|
+
{ glyph: close, fg: LEFT_BG, bg: GROUND },
|
|
315
|
+
{ glyph: " ", fg: LEFT_FG, bg: GROUND },
|
|
316
|
+
{ glyph: open, fg: RIGHT_BG, bg: GROUND },
|
|
317
|
+
];
|
|
318
|
+
return { seam, divider: seam, lead: [{ glyph: open, fg: RIGHT_BG, bg: GROUND }], tail: [{ glyph: close, fg: LEFT_BG, bg: GROUND }] };
|
|
246
319
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Rounded pills: each run opened by U+E0B6 and closed by U+E0B4, both in its
|
|
322
|
+
* ground over the terminal's, a space between pills. ASCII draws `(` and `)`.
|
|
323
|
+
*/
|
|
324
|
+
export const CAPSULE_JOINER = new CellJoiner(capsule("", ""), capsule("(", ")"));
|
|
325
|
+
const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
|
|
326
|
+
const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM, lead: [], tail: [] };
|
|
327
|
+
/**
|
|
328
|
+
* ` | ` between every pair of items, in the theme's `strip.separator` colour;
|
|
329
|
+
* nothing at the ends. Its shape names no neighbour, so it joins items with no
|
|
330
|
+
* ground too.
|
|
331
|
+
*/
|
|
332
|
+
export const PLAIN_JOINER = new CellJoiner(PLAIN, PLAIN);
|
|
333
|
+
function gradient(steps, glyph) {
|
|
334
|
+
// Midpoint sampling across `2 * steps` half-cell positions: cell i takes
|
|
335
|
+
// samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
|
|
336
|
+
// so no sample equals either neighbour's ground.
|
|
337
|
+
const samples = 2 * steps;
|
|
338
|
+
const seam = Array.from({ length: steps }, (_, i) => ({
|
|
339
|
+
glyph,
|
|
340
|
+
fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
|
|
341
|
+
bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
|
|
342
|
+
}));
|
|
343
|
+
return { seam, divider: seam, lead: [], tail: [] };
|
|
262
344
|
}
|
|
263
345
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* two
|
|
267
|
-
*
|
|
346
|
+
* A ramp of `steps` cells from the left ground to the right. Each cell is the
|
|
347
|
+
* half block U+258C, ink on its left half and ground on its right, so it
|
|
348
|
+
* carries two samples; ASCII draws a space, keeping the grounds alone. Nothing
|
|
349
|
+
* at the ends, or beside a neighbour with no ground — there is no colour to
|
|
350
|
+
* ramp toward.
|
|
268
351
|
*/
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
_steps;
|
|
272
|
-
constructor(options) {
|
|
273
|
-
this._steps = options?.steps ?? 4;
|
|
274
|
-
}
|
|
275
|
-
join(left, right) {
|
|
276
|
-
// [LAW:dataflow-not-control-flow] Endpoints have no opposite anchor to
|
|
277
|
-
// interpolate toward — the data (a missing neighbor) makes the gradient
|
|
278
|
-
// empty. Same for edges lacking a bgcolor: nothing to blend between.
|
|
279
|
-
if (left === null || right === null)
|
|
280
|
-
return EMPTY;
|
|
281
|
-
const steps = this._steps;
|
|
282
|
-
return deferred(function* (options) {
|
|
283
|
-
const lbg = left.edgeStyle("right", options).bgcolor;
|
|
284
|
-
const rbg = right.edgeStyle("left", options).bgcolor;
|
|
285
|
-
if (!lbg || !rbg)
|
|
286
|
-
return;
|
|
287
|
-
const lTrip = lbg.getTruecolor();
|
|
288
|
-
const rTrip = rbg.getTruecolor();
|
|
289
|
-
// An ASCII cell carries one sample, the background: the gradient keeps
|
|
290
|
-
// its colours at half the resolution.
|
|
291
|
-
const half = drawable(options, HALF_BLOCK, " ");
|
|
292
|
-
const samples = 2 * steps;
|
|
293
|
-
// Midpoint sampling across `2 * steps` half-cell positions: sample j has
|
|
294
|
-
// t = (j + 0.5) / samples. Cell i takes samples 2i (left half) and 2i+1
|
|
295
|
-
// (right half). No sample ever equals either anchor.
|
|
296
|
-
for (let i = 0; i < steps; i++) {
|
|
297
|
-
const tLeft = (2 * i + 0.5) / samples;
|
|
298
|
-
const tRight = (2 * i + 1.5) / samples;
|
|
299
|
-
const fg = ColorSpec.fromRgba(blendRgb(lTrip, rTrip, tLeft));
|
|
300
|
-
const bg = ColorSpec.fromRgba(blendRgb(lTrip, rTrip, tRight));
|
|
301
|
-
yield new Segment(half, new Style({ color: fg, bgcolor: bg }));
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
}
|
|
352
|
+
export function gradientJoiner(steps = 4) {
|
|
353
|
+
return new CellJoiner(gradient(steps, "▌"), gradient(steps, " "));
|
|
305
354
|
}
|
package/dist/core/style.d.ts
CHANGED
|
@@ -108,8 +108,7 @@ export declare class Style {
|
|
|
108
108
|
/**
|
|
109
109
|
* Returns the SGR parameter list this style emits (e.g. `"1;31;48;2;0;0;255"`),
|
|
110
110
|
* or `""` when the style has no SGR contribution. Excludes OSC 8 link bytes —
|
|
111
|
-
* links are not SGR. The segment encoder (`
|
|
112
|
-
* string as the group key for adjacent-same-style coalescing and wraps it in
|
|
111
|
+
* links are not SGR. The segment encoder (`segmentToString`) wraps it in
|
|
113
112
|
* `\x1b[...m`; styled text reaches the wire only through that encoder.
|
|
114
113
|
*
|
|
115
114
|
* [LAW:one-source-of-truth] One computation of SGR codes.
|
|
@@ -127,7 +126,7 @@ export declare class Style {
|
|
|
127
126
|
static chain(...styles: (Style | undefined)[]): Style;
|
|
128
127
|
static normalize(definition: string): string;
|
|
129
128
|
/**
|
|
130
|
-
* Parse a space-separated style definition.
|
|
129
|
+
* Parse a space-separated style definition. Memoized (`Memo`).
|
|
131
130
|
*
|
|
132
131
|
* A definition only: `"bold red"` parses, `"repr.number"` does not. Names
|
|
133
132
|
* belong to a `Theme`, and `Theme.resolve` is where one is looked up.
|
|
@@ -142,6 +141,26 @@ export declare class StyleStack {
|
|
|
142
141
|
push(style: Style): void;
|
|
143
142
|
pop(): Style;
|
|
144
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* A `Style` added over a style name or definition a render has yet to
|
|
146
|
+
* resolve. Which it is cannot be told before then: a theme's names shadow
|
|
147
|
+
* definitions (`Theme.resolve`), so the addition waits for the render whose
|
|
148
|
+
* theme says what `under` stands for, and draws as `under` resolved with
|
|
149
|
+
* `over` added on top.
|
|
150
|
+
*/
|
|
151
|
+
export interface LayeredStyle {
|
|
152
|
+
readonly under: string;
|
|
153
|
+
readonly over: Style;
|
|
154
|
+
}
|
|
155
|
+
/** A style as a RichText stores it, base or span: resolved when it is drawn. */
|
|
156
|
+
export type TextStyle = string | Style | LayeredStyle;
|
|
157
|
+
/**
|
|
158
|
+
* `over` added to `base`, the outer style winning as `Style.add` has it. A
|
|
159
|
+
* `Style` base takes the addition now, and so does `""`, the definition that
|
|
160
|
+
* adds nothing; any other string keeps it as a `LayeredStyle` for the render
|
|
161
|
+
* that resolves the string.
|
|
162
|
+
*/
|
|
163
|
+
export declare function layerStyle(base: TextStyle, over: Style): TextStyle;
|
|
145
164
|
export declare class Theme {
|
|
146
165
|
private readonly styles;
|
|
147
166
|
constructor(definitions?: Record<string, string>, options?: {
|
|
@@ -149,16 +168,17 @@ export declare class Theme {
|
|
|
149
168
|
});
|
|
150
169
|
get(name: string): Style | undefined;
|
|
151
170
|
/**
|
|
152
|
-
* The style a `
|
|
153
|
-
*
|
|
154
|
-
* `StyleSyntaxError` when the string is neither.
|
|
171
|
+
* The style a `TextStyle` stands for under this theme: a name this theme
|
|
172
|
+
* defines, otherwise a definition for `Style.parse`, which throws
|
|
173
|
+
* `StyleSyntaxError` when the string is neither. A `LayeredStyle` is its
|
|
174
|
+
* `under` resolved here with its `over` added on top.
|
|
155
175
|
*
|
|
156
176
|
* [LAW:one-source-of-truth] The theme is the one map from names to styles.
|
|
157
177
|
* The lookup stays outside `Style.parse`'s process-wide cache on purpose:
|
|
158
178
|
* cached there, the first console to resolve `repr.number` would decide it
|
|
159
179
|
* for every console after it, whatever theme each was given.
|
|
160
180
|
*/
|
|
161
|
-
resolve(style:
|
|
181
|
+
resolve(style: TextStyle): Style;
|
|
162
182
|
has(name: string): boolean;
|
|
163
183
|
}
|
|
164
184
|
export declare const DEFAULT_STYLES: Record<string, Style>;
|