@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6
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 +21 -9
- package/THIRD_PARTY_NOTICES.md +39 -1
- package/dist/Text-BobFKi74.d.ts +452 -0
- package/dist/ansi.d.ts +122 -131
- package/dist/ansi.js +86 -6
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-DZ88EXJv.d.ts +21 -0
- package/dist/index.d.ts +143 -498
- package/dist/index.js +1026 -2244
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOSLQ8fF.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-aZr9O8h3.js +665 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-BzqAJi0n.js +1337 -0
- package/package.json +41 -9
- package/src/ansi/chalk.ts +5 -3
- package/src/ansi/escapes.ts +14 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +3 -1
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/tokenize.ts +3 -4
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +98 -10
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +0 -8
- package/src/components/CursorContext.ts +1 -1
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +22 -45
- package/src/components/Transform.tsx +1 -1
- package/src/dom.ts +11 -2
- package/src/global.d.ts +3 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/index.ts +45 -1
- package/src/ink.tsx +213 -153
- package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
- package/src/reconciler.ts +24 -3
- package/src/render-background.ts +36 -15
- package/src/render-border.ts +94 -61
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +21 -6
- package/src/render.ts +15 -6
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/squash-text-nodes.ts +2 -5
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +19 -14
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/wrap-text.ts +4 -0
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-Cr6xVFMa.js +0 -2330
- package/src/ansi/supports-color.ts +0 -207
- package/src/colorize.ts +0 -60
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { blend } from "#/color/sample.ts";
|
|
2
|
+
import { createCell, emptyCell, type Cell, type CellPatch, type CellStyle } from "#/screen/cell.ts";
|
|
3
|
+
import type { Rect } from "#/screen/geometry.ts";
|
|
4
|
+
|
|
5
|
+
export type Line = readonly Cell[];
|
|
6
|
+
|
|
7
|
+
/** A half-open range of changed columns in one row. */
|
|
8
|
+
export type DirtySpan = {
|
|
9
|
+
readonly y: number;
|
|
10
|
+
readonly start: number;
|
|
11
|
+
readonly end: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
A mutable two-dimensional terminal cell buffer. Cells themselves are immutable,
|
|
16
|
+
so current and next screens can compare them by value or safely share constants.
|
|
17
|
+
*/
|
|
18
|
+
export class Screen {
|
|
19
|
+
#width: number;
|
|
20
|
+
#height: number;
|
|
21
|
+
|
|
22
|
+
#rows: Cell[][];
|
|
23
|
+
#painted: boolean[][];
|
|
24
|
+
#dirty: Array<{ start: number; end: number } | undefined>;
|
|
25
|
+
|
|
26
|
+
constructor(width: number, height: number) {
|
|
27
|
+
validateDimensions(width, height);
|
|
28
|
+
|
|
29
|
+
this.#width = width;
|
|
30
|
+
this.#height = height;
|
|
31
|
+
this.#rows = Array.from({ length: height }, () => Array<Cell>(width).fill(emptyCell));
|
|
32
|
+
this.#painted = Array.from({ length: height }, () => Array<boolean>(width).fill(false));
|
|
33
|
+
this.#dirty = Array.from({ length: height }, () =>
|
|
34
|
+
width > 0 ? { start: 0, end: width } : undefined,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get width(): number {
|
|
39
|
+
return this.#width;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
The populated length of one row: the nominal width, or more when overflow
|
|
44
|
+
writes (`textWrap: "none"` content) extended it past the screen's width.
|
|
45
|
+
*/
|
|
46
|
+
rowLength(y: number): number {
|
|
47
|
+
return Math.max(this.#width, this.#rows[y]?.length ?? 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get height(): number {
|
|
51
|
+
return this.#height;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
bounds(): Rect {
|
|
55
|
+
return { x: 0, y: 0, width: this.width, height: this.height };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cellAt(x: number, y: number): Cell | undefined {
|
|
59
|
+
return this.#rows[y]?.[x];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether a drawable explicitly touched this position. */
|
|
63
|
+
isPainted(x: number, y: number): boolean {
|
|
64
|
+
return this.#painted[y]?.[x] ?? false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setCell(x: number, y: number, cell: Cell, options?: { overflow?: boolean }): void {
|
|
68
|
+
const row = this.#rows[y];
|
|
69
|
+
const overflow = options?.overflow === true;
|
|
70
|
+
if (!row || x < 0 || (!overflow && x >= this.width)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!Number.isInteger(cell.width) || cell.width < 1) {
|
|
75
|
+
throw new Error("Only leading cells with a positive width may be written");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const finalColumn = x + cell.width;
|
|
79
|
+
if (overflow && finalColumn > row.length) {
|
|
80
|
+
this.#growRow(row, y, finalColumn);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const capacity = overflow ? row.length : this.width;
|
|
84
|
+
const clipped = finalColumn > capacity;
|
|
85
|
+
const overwriteEnd = Math.min(finalColumn, capacity);
|
|
86
|
+
|
|
87
|
+
for (let column = x; column < overwriteEnd; column++) {
|
|
88
|
+
this.#clearGraphemeAt(row, y, column);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (clipped) {
|
|
92
|
+
for (let column = x; column < capacity; column++) {
|
|
93
|
+
row[column] = emptyCell;
|
|
94
|
+
}
|
|
95
|
+
this.#markDirty(y, x, capacity);
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
row[x] = cell;
|
|
101
|
+
this.#painted[y]![x] = true;
|
|
102
|
+
|
|
103
|
+
for (let column = x + 1; column < finalColumn; column++) {
|
|
104
|
+
row[column] = {
|
|
105
|
+
grapheme: "",
|
|
106
|
+
width: 0,
|
|
107
|
+
style: cell.style,
|
|
108
|
+
...(cell.hyperlink ? { hyperlink: cell.hyperlink } : {}),
|
|
109
|
+
};
|
|
110
|
+
this.#painted[y]![column] = true;
|
|
111
|
+
}
|
|
112
|
+
this.#markDirty(y, x, finalColumn);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
Composes independent cell channels over the existing cell. An undefined
|
|
117
|
+
patch is transparent; an explicit space in `content` paints a blank.
|
|
118
|
+
*/
|
|
119
|
+
composeCell(
|
|
120
|
+
x: number,
|
|
121
|
+
y: number,
|
|
122
|
+
patch: CellPatch | undefined,
|
|
123
|
+
options?: { overflow?: boolean },
|
|
124
|
+
): void {
|
|
125
|
+
if (!patch) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const row = this.#rows[y];
|
|
130
|
+
if (!row || x < 0 || (options?.overflow !== true && x >= this.width)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let leadingColumn = x;
|
|
135
|
+
while (leadingColumn > 0 && row[leadingColumn]?.width === 0) {
|
|
136
|
+
leadingColumn--;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const destination = row[leadingColumn] ?? emptyCell;
|
|
140
|
+
const writeColumn = patch.content ? x : leadingColumn;
|
|
141
|
+
const style: CellStyle = {
|
|
142
|
+
...this.#composeColor(destination.style, patch, "foreground"),
|
|
143
|
+
...this.#composeColor(destination.style, patch, "background"),
|
|
144
|
+
...this.#composeColor(destination.style, patch, "underlineColor"),
|
|
145
|
+
underline: patch.underline ?? destination.style.underline,
|
|
146
|
+
attributes: patch.attributes ?? destination.style.attributes,
|
|
147
|
+
};
|
|
148
|
+
const content = patch.content ?? destination;
|
|
149
|
+
const hyperlink =
|
|
150
|
+
patch.hyperlink === undefined ? destination.hyperlink : (patch.hyperlink ?? undefined);
|
|
151
|
+
|
|
152
|
+
this.setCell(
|
|
153
|
+
writeColumn,
|
|
154
|
+
y,
|
|
155
|
+
createCell(content.grapheme, content.width, style, hyperlink),
|
|
156
|
+
options,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Composes a patch throughout a rectangle, clipped to this screen. */
|
|
161
|
+
fill(bounds: Rect, patch: CellPatch | undefined): void {
|
|
162
|
+
if (!patch) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const startX = Math.max(0, bounds.x);
|
|
167
|
+
const startY = Math.max(0, bounds.y);
|
|
168
|
+
const endX = Math.min(this.width, bounds.x + bounds.width);
|
|
169
|
+
const endY = Math.min(this.height, bounds.y + bounds.height);
|
|
170
|
+
const step = Math.max(1, patch.content?.width ?? 1);
|
|
171
|
+
|
|
172
|
+
for (let y = startY; y < endY; y++) {
|
|
173
|
+
for (let x = startX; x < endX; x += step) {
|
|
174
|
+
this.composeCell(x, y, patch);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Resizes while preserving complete graphemes in the shared top-left area. */
|
|
180
|
+
resize(width: number, height: number): void {
|
|
181
|
+
validateDimensions(width, height);
|
|
182
|
+
|
|
183
|
+
const rows = Array.from({ length: height }, () => Array<Cell>(width).fill(emptyCell));
|
|
184
|
+
const painted = Array.from({ length: height }, () => Array<boolean>(width).fill(false));
|
|
185
|
+
const sharedHeight = Math.min(height, this.height);
|
|
186
|
+
|
|
187
|
+
for (let y = 0; y < sharedHeight; y++) {
|
|
188
|
+
const source = this.#rows[y]!;
|
|
189
|
+
const destination = rows[y]!;
|
|
190
|
+
for (let x = 0; x < Math.min(width, this.width); x++) {
|
|
191
|
+
const cell = source[x]!;
|
|
192
|
+
if (cell.width === 0 || x + cell.width > width) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
destination[x] = cell;
|
|
197
|
+
painted[y]![x] = this.#painted[y]![x]!;
|
|
198
|
+
for (let column = x + 1; column < x + cell.width; column++) {
|
|
199
|
+
destination[column] = source[column]!;
|
|
200
|
+
painted[y]![column] = this.#painted[y]![column]!;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.#width = width;
|
|
206
|
+
this.#height = height;
|
|
207
|
+
this.#rows = rows;
|
|
208
|
+
this.#painted = painted;
|
|
209
|
+
this.#dirty = Array.from({ length: height }, () =>
|
|
210
|
+
width > 0 ? { start: 0, end: width } : undefined,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
clear(): void {
|
|
215
|
+
for (const [y, row] of this.#rows.entries()) {
|
|
216
|
+
row.fill(emptyCell);
|
|
217
|
+
this.#painted[y]!.fill(true);
|
|
218
|
+
this.#markDirty(y, 0, this.rowLength(y));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
toRows(): readonly Line[] {
|
|
223
|
+
return this.#rows;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
dirtySpans(): readonly DirtySpan[] {
|
|
227
|
+
return this.#dirty.flatMap((span, y) => (span ? [{ y, ...span }] : []));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
takeDirtySpans(): readonly DirtySpan[] {
|
|
231
|
+
const spans = this.dirtySpans();
|
|
232
|
+
this.#dirty.fill(undefined);
|
|
233
|
+
return spans;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Extends a row (and its paint mask) so overflow writes land past `width`. */
|
|
237
|
+
#growRow(row: Cell[], y: number, length: number): void {
|
|
238
|
+
const painted = this.#painted[y]!;
|
|
239
|
+
while (row.length < length) {
|
|
240
|
+
row.push(emptyCell);
|
|
241
|
+
painted.push(false);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#clearGraphemeAt(row: Cell[], y: number, x: number): void {
|
|
246
|
+
const existing = row[x];
|
|
247
|
+
if (!existing || existing === emptyCell) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let leadingColumn = x;
|
|
252
|
+
while (leadingColumn > 0 && row[leadingColumn]?.width === 0) {
|
|
253
|
+
leadingColumn--;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const leadingCell = row[leadingColumn];
|
|
257
|
+
const width = Math.max(1, leadingCell?.width ?? 1);
|
|
258
|
+
for (
|
|
259
|
+
let column = leadingColumn;
|
|
260
|
+
column < Math.min(leadingColumn + width, row.length);
|
|
261
|
+
column++
|
|
262
|
+
) {
|
|
263
|
+
row[column] = emptyCell;
|
|
264
|
+
this.#painted[y]![column] = true;
|
|
265
|
+
}
|
|
266
|
+
this.#markDirty(y, leadingColumn, leadingColumn + width);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#composeColor(
|
|
270
|
+
destination: CellStyle,
|
|
271
|
+
patch: CellPatch,
|
|
272
|
+
channel: "foreground" | "background" | "underlineColor",
|
|
273
|
+
): Partial<CellStyle> {
|
|
274
|
+
const value = patch[channel];
|
|
275
|
+
if (value === undefined) {
|
|
276
|
+
return destination[channel] ? { [channel]: destination[channel] } : {};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (value === null) return {};
|
|
280
|
+
const destinationColor = destination[channel];
|
|
281
|
+
if (value.model === "rgb" && value.alpha < 255 && destinationColor) {
|
|
282
|
+
return { [channel]: blend(value, destinationColor) };
|
|
283
|
+
}
|
|
284
|
+
return { [channel]: value };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#markDirty(y: number, start: number, end: number): void {
|
|
288
|
+
if (y < 0 || y >= this.height || start >= end) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const clippedStart = Math.max(0, start);
|
|
293
|
+
const clippedEnd = Math.min(this.rowLength(y), end);
|
|
294
|
+
const current = this.#dirty[y];
|
|
295
|
+
this.#dirty[y] = current
|
|
296
|
+
? { start: Math.min(current.start, clippedStart), end: Math.max(current.end, clippedEnd) }
|
|
297
|
+
: { start: clippedStart, end: clippedEnd };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function validateDimensions(width: number, height: number): void {
|
|
302
|
+
if (!Number.isInteger(width) || width < 0 || !Number.isInteger(height) || height < 0) {
|
|
303
|
+
throw new Error("Screen dimensions must be non-negative integers");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getEndCode,
|
|
3
|
+
styledCharsToString,
|
|
4
|
+
type AnsiCode,
|
|
5
|
+
type StyledChar,
|
|
6
|
+
} from "#/ansi/tokenize.ts";
|
|
7
|
+
import { cellAttributes, type Cell, type Color, type Hyperlink } from "#/screen/cell.ts";
|
|
8
|
+
import { quantizeColor, type ColorProfile } from "#/screen/color-profile.ts";
|
|
9
|
+
import type { Line, Screen } from "#/screen/screen.ts";
|
|
10
|
+
|
|
11
|
+
const ESC = "\u001B";
|
|
12
|
+
const BEL = "\u0007";
|
|
13
|
+
|
|
14
|
+
export type SerializeOptions = {
|
|
15
|
+
readonly colorProfile: ColorProfile;
|
|
16
|
+
readonly trimEnd?: boolean;
|
|
17
|
+
readonly styles?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Serializes one structured cell run with minimal style transitions. */
|
|
21
|
+
export function serializeLine(line: Line, options: SerializeOptions): string {
|
|
22
|
+
const characters = line.map((cell) =>
|
|
23
|
+
styledCharFromCell(cell, options.colorProfile, options.styles ?? true),
|
|
24
|
+
);
|
|
25
|
+
const output = styledCharsToString(characters);
|
|
26
|
+
return options.trimEnd === false ? output : output.trimEnd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serializeScreen(screen: Screen, options: SerializeOptions): string {
|
|
30
|
+
return screen
|
|
31
|
+
.toRows()
|
|
32
|
+
.map((line) => serializeLine(line, options))
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Converts a semantic cell to the canonical ANSI compatibility style state. */
|
|
37
|
+
export function styledCharFromCell(cell: Cell, profile: ColorProfile, styles = true): StyledChar {
|
|
38
|
+
return {
|
|
39
|
+
type: "char",
|
|
40
|
+
value: cell.grapheme,
|
|
41
|
+
fullWidth: cell.width > 1,
|
|
42
|
+
styles: styles ? ansiCodesFromCell(cell, profile) : [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ansiCodesFromCell(cell: Cell, profile: ColorProfile): AnsiCode[] {
|
|
47
|
+
const result: AnsiCode[] = [];
|
|
48
|
+
const { style } = cell;
|
|
49
|
+
|
|
50
|
+
if (cell.hyperlink) {
|
|
51
|
+
result.push(codePair(hyperlinkStart(cell.hyperlink)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// This order mirrors the nesting produced by the Ink-compatible Text API,
|
|
55
|
+
// while remaining deterministic for cells produced by custom drawables.
|
|
56
|
+
addAttribute(result, style.attributes, cellAttributes.inverse, 7);
|
|
57
|
+
addAttribute(result, style.attributes, cellAttributes.hidden, 8);
|
|
58
|
+
addAttribute(result, style.attributes, cellAttributes.strikethrough, 9);
|
|
59
|
+
|
|
60
|
+
if (style.underline !== "none") {
|
|
61
|
+
const variant = {
|
|
62
|
+
single: "4",
|
|
63
|
+
double: "4:2",
|
|
64
|
+
curly: "4:3",
|
|
65
|
+
dotted: "4:4",
|
|
66
|
+
dashed: "4:5",
|
|
67
|
+
}[style.underline];
|
|
68
|
+
result.push(codePair(`${ESC}[${variant}m`));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
addAttribute(result, style.attributes, cellAttributes.italic, 3);
|
|
72
|
+
addAttribute(result, style.attributes, cellAttributes.bold, 1);
|
|
73
|
+
addAttribute(result, style.attributes, cellAttributes.rapidBlink, 6);
|
|
74
|
+
addAttribute(result, style.attributes, cellAttributes.blink, 5);
|
|
75
|
+
|
|
76
|
+
const background = quantizeColor(style.background, profile);
|
|
77
|
+
if (background) {
|
|
78
|
+
result.push(codePair(colorCode(background, "background")));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const foreground = quantizeColor(style.foreground, profile);
|
|
82
|
+
if (foreground) {
|
|
83
|
+
result.push(codePair(colorCode(foreground, "foreground")));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
addAttribute(result, style.attributes, cellAttributes.faint, 2);
|
|
87
|
+
|
|
88
|
+
const underlineColor = quantizeColor(style.underlineColor, profile);
|
|
89
|
+
if (underlineColor) {
|
|
90
|
+
result.push(codePair(colorCode(underlineColor, "underline")));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addAttribute(
|
|
97
|
+
result: AnsiCode[],
|
|
98
|
+
attributes: number,
|
|
99
|
+
attribute: number,
|
|
100
|
+
sgr: number,
|
|
101
|
+
): void {
|
|
102
|
+
if (Math.floor(attributes / attribute) % 2 === 1) {
|
|
103
|
+
result.push(codePair(`${ESC}[${sgr}m`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function colorCode(color: Color, channel: "foreground" | "background" | "underline"): string {
|
|
108
|
+
const prefix = { foreground: 38, background: 48, underline: 58 }[channel];
|
|
109
|
+
|
|
110
|
+
if (color.model === "rgb") {
|
|
111
|
+
return `${ESC}[${prefix};2;${color.red};${color.green};${color.blue}m`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (channel === "underline" || color.index >= 16 || color.encoding === "ansi256") {
|
|
115
|
+
return `${ESC}[${prefix};5;${color.index}m`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const basic = color.index < 8 ? color.index : color.index - 8 + 60;
|
|
119
|
+
const channelOffset = channel === "background" ? 40 : 30;
|
|
120
|
+
return `${ESC}[${channelOffset + basic}m`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hyperlinkStart(hyperlink: Hyperlink): string {
|
|
124
|
+
return `${ESC}]8;${hyperlink.parameters ?? ""};${hyperlink.url}${BEL}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function codePair(code: string): AnsiCode {
|
|
128
|
+
return { code, endCode: getEndCode(code) };
|
|
129
|
+
}
|
package/src/screen.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "#/screen/index.ts";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { hexToRgb } from "#/ansi/sgr.ts";
|
|
2
|
+
import type { Paint } from "#/color/paint.ts";
|
|
3
|
+
import { cellAttributes, type CellStyle, type Color } from "#/screen/cell.ts";
|
|
4
|
+
|
|
5
|
+
export type SemanticTextStyle = {
|
|
6
|
+
readonly foreground?: Paint;
|
|
7
|
+
readonly background?: Paint;
|
|
8
|
+
readonly resetForeground?: boolean;
|
|
9
|
+
readonly resetBackground?: boolean;
|
|
10
|
+
readonly underline?: CellStyle["underline"];
|
|
11
|
+
readonly attributes: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const emptySemanticTextStyle: SemanticTextStyle = {
|
|
15
|
+
attributes: cellAttributes.none,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const namedColors: Record<string, number> = {
|
|
19
|
+
black: 0,
|
|
20
|
+
red: 1,
|
|
21
|
+
green: 2,
|
|
22
|
+
yellow: 3,
|
|
23
|
+
blue: 4,
|
|
24
|
+
magenta: 5,
|
|
25
|
+
cyan: 6,
|
|
26
|
+
white: 7,
|
|
27
|
+
blackBright: 8,
|
|
28
|
+
gray: 8,
|
|
29
|
+
grey: 8,
|
|
30
|
+
redBright: 9,
|
|
31
|
+
greenBright: 10,
|
|
32
|
+
yellowBright: 11,
|
|
33
|
+
blueBright: 12,
|
|
34
|
+
magentaBright: 13,
|
|
35
|
+
cyanBright: 14,
|
|
36
|
+
whiteBright: 15,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const rgbPattern = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/;
|
|
40
|
+
const ansi256Pattern = /^ansi256\(\s?(\d+)\s?\)$/;
|
|
41
|
+
|
|
42
|
+
export function parseSemanticColor(value: string | Color | undefined): Color | undefined {
|
|
43
|
+
if (!value) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value !== "string") return value;
|
|
48
|
+
|
|
49
|
+
const named = namedColors[value];
|
|
50
|
+
if (named !== undefined) {
|
|
51
|
+
return { model: "indexed", index: named, encoding: "ansi16" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (value.startsWith("#")) {
|
|
55
|
+
const [red, green, blue] = hexToRgb(value);
|
|
56
|
+
return { model: "rgb", red, green, blue, alpha: 255 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rgb = rgbPattern.exec(value);
|
|
60
|
+
if (rgb) {
|
|
61
|
+
return {
|
|
62
|
+
model: "rgb",
|
|
63
|
+
red: Number(rgb[1]),
|
|
64
|
+
green: Number(rgb[2]),
|
|
65
|
+
blue: Number(rgb[3]),
|
|
66
|
+
alpha: 255,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ansi256 = ansi256Pattern.exec(value);
|
|
71
|
+
if (ansi256) {
|
|
72
|
+
return { model: "indexed", index: Number(ansi256[1]), encoding: "ansi256" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mergeSemanticTextStyles(
|
|
79
|
+
parent: SemanticTextStyle,
|
|
80
|
+
child: SemanticTextStyle | undefined,
|
|
81
|
+
): SemanticTextStyle {
|
|
82
|
+
if (!child) {
|
|
83
|
+
return parent;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Attribute flags are intentionally additive across nested Text nodes.
|
|
87
|
+
// eslint-disable-next-line no-bitwise
|
|
88
|
+
const attributes = parent.attributes | child.attributes;
|
|
89
|
+
const foreground = child.resetForeground ? undefined : (child.foreground ?? parent.foreground);
|
|
90
|
+
const background = child.resetBackground ? undefined : (child.background ?? parent.background);
|
|
91
|
+
return {
|
|
92
|
+
...(foreground ? { foreground } : {}),
|
|
93
|
+
...(background ? { background } : {}),
|
|
94
|
+
...(child.resetForeground ? { resetForeground: true } : {}),
|
|
95
|
+
...(child.resetBackground ? { resetBackground: true } : {}),
|
|
96
|
+
...((child.underline ?? parent.underline)
|
|
97
|
+
? { underline: child.underline ?? parent.underline }
|
|
98
|
+
: {}),
|
|
99
|
+
attributes,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function semanticTextStyleToCellStyle(style: SemanticTextStyle): CellStyle {
|
|
104
|
+
const foreground = solidColor(style.foreground);
|
|
105
|
+
const background = solidColor(style.background);
|
|
106
|
+
return {
|
|
107
|
+
...(foreground ? { foreground } : {}),
|
|
108
|
+
...(background ? { background } : {}),
|
|
109
|
+
underline: style.underline ?? "none",
|
|
110
|
+
attributes: style.attributes,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function solidColor(paint: Paint | undefined): Color | undefined {
|
|
115
|
+
if (!paint) return undefined;
|
|
116
|
+
if (typeof paint === "string") return parseSemanticColor(paint);
|
|
117
|
+
return "model" in paint ? paint : undefined;
|
|
118
|
+
}
|
package/src/squash-text-nodes.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { type DOMElement } from "#/dom.ts";
|
|
2
2
|
import { sanitizeAnsi } from "#/sanitize-ansi.ts";
|
|
3
3
|
|
|
4
|
-
// Squashing text nodes
|
|
5
|
-
// to `Output` instance only once. For example, <Text>hello{' '}world</Text>
|
|
6
|
-
// is actually 3 text nodes, which would result 3 writes to `Output`.
|
|
4
|
+
// Squashing text nodes combines the host text nodes into one compatibility run.
|
|
7
5
|
//
|
|
8
6
|
// Also, this is necessary for libraries like ink-link (https://github.com/sindresorhus/ink-link),
|
|
9
7
|
// which need to wrap all children at once, instead of wrapping 3 text nodes separately.
|
|
@@ -26,8 +24,7 @@ export const squashTextNodes = (node: DOMElement): string => {
|
|
|
26
24
|
nodeText = squashTextNodes(childNode);
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
//
|
|
30
|
-
// apply children transform, so we have to do it manually here for each text node
|
|
27
|
+
// Nested compatibility transforms must run before the enclosing transform.
|
|
31
28
|
if (nodeText.length > 0 && typeof childNode.internal_transform === "function") {
|
|
32
29
|
nodeText = childNode.internal_transform(nodeText, index);
|
|
33
30
|
}
|